Исправлены create UI, PWA-иконки, tooltip A2HS и breakpoint админ-меню 1015px.
devops-tools/wrapped/wrapped-build/pipeline/head This commit looks good
devops-tools/wrapped/wrapped-deploy/pipeline/head This commit looks good

This commit is contained in:
Sergey Antropoff
2026-07-29 21:03:51 +03:00
parent 54749e5e12
commit eef8c4ec57
9 changed files with 80 additions and 106 deletions
+1 -1
View File
@@ -341,7 +341,7 @@ services:
### UX (v0.1.3+) ### UX (v0.1.3+)
- После создания: **QR** на share-link, иконки **скачать QR (PNG)** и **Web Share** (если есть `navigator.share`) под QR; отдельные кнопки Copy для ссылки / токена / пароля; пароль не советуется слать в той же переписке. - После создания: **QR** на share-link, иконки **скачать QR (PNG)** и **Web Share** (если есть `navigator.share`) под QR; отдельные кнопки Copy для ссылки / токена / пароля; пароль не советуется слать в той же переписке.
- Create: счётчик размера `≈ used / max`; **число открытий** 1–3 (потолок в админке); опционально **«доступно с»** (дата+время); человеческие TTL (в т.ч. «до вечера»); шаблоны текста; подписи элементов пакета. - Create: счётчик размера `≈ used / max`; **число открытий** 1–3 (потолок в админке); опционально **«доступно с»** (дата + время); человеческие TTL (в т.ч. «до вечера»).
- Unwrap: Enter в поле пароля; focus+select при ошибках пароля; zip all; trust-строка; экран **ещё недоступно** до `available_from`; при N>1 ciphertext остаётся до последнего открытия. - Unwrap: Enter в поле пароля; focus+select при ошибках пароля; zip all; trust-строка; экран **ещё недоступно** до `available_from`; при N>1 ciphertext остаётся до последнего открытия.
- Картинки: lightbox. Тема: `prefers-color-scheme` при первом визите; haptic на Copy. PWA manifest + кнопка **На экран Домой** (Chrome install / iOS подсказка). - Картинки: lightbox. Тема: `prefers-color-scheme` при первом визите; haptic на Copy. PWA manifest + кнопка **На экран Домой** (Chrome install / iOS подсказка).
- Страница [`/verify`](/verify) — как проверить ZK-модель. - Страница [`/verify`](/verify) — как проверить ZK-модель.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

+35 -37
View File
@@ -178,7 +178,8 @@ html[data-theme="light"] body {
50% { filter: saturate(1.2); transform: scale(1.04); } 50% { filter: saturate(1.2); transform: scale(1.04); }
} }
.top-actions { display: flex; gap: 0.5rem; } .top-actions { display: flex; gap: 0.5rem; overflow: visible; position: relative; z-index: 5; }
.topbar { overflow: visible; }
.icon-btn { .icon-btn {
min-width: 42px; min-width: 42px;
@@ -271,15 +272,22 @@ html[data-theme="dark"] .icon-btn:hover {
overflow: visible; overflow: visible;
} }
#install-app .ui-tooltip { #install-app .ui-tooltip {
top: calc(100% + 0.5rem);
bottom: auto;
left: 50%; left: 50%;
right: auto; right: auto;
transform: translateX(-50%) translateY(4px); transform: translateX(-50%) translateY(-4px);
white-space: nowrap; white-space: nowrap;
} }
#install-app .ui-tooltip::after { #install-app .ui-tooltip::after {
top: auto;
bottom: 100%;
left: 50%; left: 50%;
right: auto; right: auto;
transform: translateX(-50%); transform: translateX(-50%);
border: 5px solid transparent;
border-bottom-color: var(--line);
border-top-color: transparent;
} }
#install-app:hover .ui-tooltip, #install-app:hover .ui-tooltip,
#install-app:focus-visible .ui-tooltip { #install-app:focus-visible .ui-tooltip {
@@ -287,6 +295,25 @@ html[data-theme="dark"] .icon-btn:hover {
visibility: visible; visibility: visible;
transform: translateX(-50%); transform: translateX(-50%);
} }
.datetime-split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.45rem;
}
.datetime-split input {
min-width: 0;
}
.field-label {
display: block;
margin-bottom: 0.35rem;
font-weight: 600;
font-size: 0.88rem;
}
@media (max-width: 720px) {
.datetime-split {
grid-template-columns: 1fr;
}
}
.composer, .success-panel, .result-panel { .composer, .success-panel, .result-panel {
display: grid; display: grid;
@@ -834,51 +861,22 @@ body.busy-open { overflow: hidden; }
} }
.file-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; } .file-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; }
.file-list li, .file-list li {
.file-list-item { display: flex;
display: grid; justify-content: space-between;
grid-template-columns: minmax(0, 1fr) minmax(5.5rem, 9rem) auto; gap: 0.75rem;
gap: 0.45rem;
align-items: center; align-items: center;
padding: 0.55rem 0.75rem; padding: 0.55rem 0.75rem;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 10px; border-radius: 10px;
font-size: 0.9rem; font-size: 0.9rem;
} }
.file-label-input {
width: 100%;
min-width: 0;
padding: 0.35rem 0.5rem;
font-size: 0.82rem;
}
.file-list button { .file-list button {
border: 0; border: 0;
background: transparent; background: transparent;
color: var(--danger); color: var(--danger);
cursor: pointer; cursor: pointer;
} }
.template-chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin: 0 0 0.35rem;
}
.template-chip {
border: 1px solid var(--line);
background: transparent;
color: var(--muted);
border-radius: 999px;
padding: 0.3rem 0.7rem;
font-size: 0.78rem;
font-weight: 600;
cursor: pointer;
}
.template-chip:hover,
.template-chip:focus-visible {
color: var(--accent-2);
border-color: rgba(75, 134, 240, 0.4);
outline: none;
}
.success-meta-badges { .success-meta-badges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -1519,7 +1517,7 @@ html[data-theme="light"] .admin-nav {
box-shadow: 0 8px 24px rgba(20, 40, 70, 0.08); box-shadow: 0 8px 24px rgba(20, 40, 70, 0.08);
} }
@media (max-width: 860px) { @media (max-width: 1015px) {
.admin-top { .admin-top {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
@@ -1617,7 +1615,7 @@ html[data-theme="light"] .admin-nav {
} }
} }
@media (min-width: 861px) { @media (min-width: 1016px) {
.admin-top > .brand { .admin-top > .brand {
flex: 0 0 auto; flex: 0 0 auto;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

+1 -1
View File
@@ -4,7 +4,7 @@
const nav = document.getElementById("admin-nav"); const nav = document.getElementById("admin-nav");
if (!top || !toggle || !nav) return; if (!top || !toggle || !nav) return;
const mq = window.matchMedia("(max-width: 860px)"); const mq = window.matchMedia("(max-width: 1015px)");
const syncLabel = () => { const syncLabel = () => {
const open = top.classList.contains("is-nav-open"); const open = top.classList.contains("is-nav-open");
+17 -57
View File
@@ -1,14 +1,12 @@
(() => { (() => {
const t = (k, vars) => window.WrappedI18n.t(k, vars); const t = (k, vars) => window.WrappedI18n.t(k, vars);
/** @type {{ file: File, label: string }[]} */ /** @type {File[]} */
const files = []; const files = [];
let settings = null; let settings = null;
let captchaWidgetId = null; let captchaWidgetId = null;
const els = { const els = {
text: document.getElementById("payload-text"), text: document.getElementById("payload-text"),
textLabel: document.getElementById("text-label"),
templateChips: document.getElementById("template-chips"),
lang: document.getElementById("text-language"), lang: document.getElementById("text-language"),
langChip: document.getElementById("lang-chip"), langChip: document.getElementById("lang-chip"),
langChipText: document.getElementById("lang-chip-text"), langChipText: document.getElementById("lang-chip-text"),
@@ -18,7 +16,8 @@
editor: document.getElementById("code-editor"), editor: document.getElementById("code-editor"),
ttl: document.getElementById("ttl-seconds"), ttl: document.getElementById("ttl-seconds"),
maxOpens: document.getElementById("max-opens"), maxOpens: document.getElementById("max-opens"),
availableFrom: document.getElementById("available-from"), availableFromDate: document.getElementById("available-from-date"),
availableFromTime: document.getElementById("available-from-time"),
password: document.getElementById("password"), password: document.getElementById("password"),
generatePassword: document.getElementById("generate-password"), generatePassword: document.getElementById("generate-password"),
dropzone: document.getElementById("dropzone"), dropzone: document.getElementById("dropzone"),
@@ -114,7 +113,7 @@
function estimatePayloadBytes() { function estimatePayloadBytes() {
const textBytes = new TextEncoder().encode(els.text?.value || "").length; const textBytes = new TextEncoder().encode(els.text?.value || "").length;
const fileBytes = files.reduce((sum, entry) => sum + (Number(entry.file.size) || 0), 0); const fileBytes = files.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
return textBytes + fileBytes; return textBytes + fileBytes;
} }
@@ -142,21 +141,9 @@
function renderFiles() { function renderFiles() {
els.fileList.innerHTML = ""; els.fileList.innerHTML = "";
files.forEach((entry, idx) => { files.forEach((f, idx) => {
const f = entry.file;
const li = document.createElement("li"); const li = document.createElement("li");
li.className = "file-list-item"; li.innerHTML = `<span>${escapeHtml(f.name)} <small>(${escapeHtml(f.type || "file")} · ${window.WrappedUI.formatBytes(f.size)})</small></span>`;
const meta = document.createElement("span");
meta.innerHTML = `${escapeHtml(f.name)} <small>(${escapeHtml(f.type || "file")} · ${window.WrappedUI.formatBytes(f.size)})</small>`;
const labelInput = document.createElement("input");
labelInput.type = "text";
labelInput.className = "file-label-input";
labelInput.maxLength = 120;
labelInput.placeholder = t("create.itemLabelPlaceholder");
labelInput.value = entry.label || "";
labelInput.addEventListener("input", () => {
entry.label = labelInput.value;
});
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.type = "button"; btn.type = "button";
btn.textContent = "×"; btn.textContent = "×";
@@ -164,8 +151,6 @@
files.splice(idx, 1); files.splice(idx, 1);
renderFiles(); renderFiles();
}); });
li.appendChild(meta);
li.appendChild(labelInput);
li.appendChild(btn); li.appendChild(btn);
els.fileList.appendChild(li); els.fileList.appendChild(li);
}); });
@@ -186,7 +171,7 @@
showError(t("create.mimeDenied") + `: ${f.type || f.name}`); showError(t("create.mimeDenied") + `: ${f.type || f.name}`);
continue; continue;
} }
files.push({ file: f, label: "" }); files.push(f);
} }
renderFiles(); renderFiles();
} }
@@ -245,31 +230,11 @@
.join(""); .join("");
} }
function renderTemplates() { function availableFromToIso() {
if (!els.templateChips) return; const date = (els.availableFromDate?.value || "").trim();
els.templateChips.innerHTML = ""; if (!date) return null;
const keys = [ const time = (els.availableFromTime?.value || "").trim() || "00:00";
{ id: "access", key: "create.tpl.access" }, const d = new Date(`${date}T${time}`);
{ id: "code", key: "create.tpl.code" },
{ id: "fileNote", key: "create.tpl.fileNote" },
];
for (const item of keys) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "template-chip";
btn.textContent = t(`create.tpl.${item.id}.name`);
btn.addEventListener("click", () => {
els.text.value = t(`create.tpl.${item.id}.body`);
refreshHighlight();
refreshSizeMeter();
});
els.templateChips.appendChild(btn);
}
}
function datetimeLocalToIso(value) {
if (!value) return null;
const d = new Date(value);
if (Number.isNaN(d.getTime())) return null; if (Number.isNaN(d.getTime())) return null;
return d.toISOString(); return d.toISOString();
} }
@@ -351,7 +316,6 @@
settings = await resp.json(); settings = await resp.json();
fillTtl(); fillTtl();
fillMaxOpens(); fillMaxOpens();
renderTemplates();
loadCaptcha(); loadCaptcha();
setLanguage("plaintext", { silent: true }); setLanguage("plaintext", { silent: true });
refreshHighlight(); refreshHighlight();
@@ -525,18 +489,15 @@
const items = []; const items = [];
const contentTypes = []; const contentTypes = [];
if (text.trim()) { if (text.trim()) {
const textItem = { items.push({
type: "text", type: "text",
language: els.lang.value, language: els.lang.value,
content: text, content: text,
}; });
const label = (els.textLabel?.value || "").trim();
if (label) textItem.label = label.slice(0, 120);
items.push(textItem);
contentTypes.push("text/plain"); contentTypes.push("text/plain");
} }
for (const entry of files) { for (const f of files) {
const item = await window.WrappedCrypto.fileToItem(entry.file, entry.label); const item = await window.WrappedCrypto.fileToItem(f);
items.push(item); items.push(item);
contentTypes.push(item.mime); contentTypes.push(item.mime);
} }
@@ -550,7 +511,7 @@
const password = els.password.value || ""; const password = els.password.value || "";
const maxOpens = Number(els.maxOpens?.value || 1); const maxOpens = Number(els.maxOpens?.value || 1);
const availableFromIso = datetimeLocalToIso(els.availableFrom?.value || ""); const availableFromIso = availableFromToIso();
els.wrapBtn.disabled = true; els.wrapBtn.disabled = true;
window.WrappedUI.showBusy(t("create.working")); window.WrappedUI.showBusy(t("create.working"));
try { try {
@@ -638,7 +599,6 @@
window.WrappedI18n.onChange(() => { window.WrappedI18n.onChange(() => {
fillTtl(); fillTtl();
fillMaxOpens(); fillMaxOpens();
renderTemplates();
refreshExpiresMeta(); refreshExpiresMeta();
refreshSuccessBadges(); refreshSuccessBadges();
refreshSizeMeter(); refreshSizeMeter();
+21 -3
View File
@@ -8,10 +8,28 @@
"theme_color": "#0b1020", "theme_color": "#0b1020",
"icons": [ "icons": [
{ {
"src": "/static/favicon.svg", "src": "/static/icon-192.png",
"sizes": "any", "sizes": "192x192",
"type": "image/svg+xml", "type": "image/png",
"purpose": "any" "purpose": "any"
},
{
"src": "/static/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
} }
] ]
} }
+5 -7
View File
@@ -14,11 +14,6 @@
</div> </div>
<label class="sr-only" for="payload-text" data-i18n="create.text">Text</label> <label class="sr-only" for="payload-text" data-i18n="create.text">Text</label>
<div class="template-chips" id="template-chips" role="group" aria-label="Templates"></div>
<div class="field">
<label for="text-label" data-i18n="create.itemLabel">Label (optional)</label>
<input id="text-label" type="text" maxlength="120" data-i18n-placeholder="create.itemLabelPlaceholder" placeholder="e.g. Instructions" />
</div>
<div class="code-editor" id="code-editor"> <div class="code-editor" id="code-editor">
<pre class="code-highlight" aria-hidden="true"><code id="code-highlight" class="hljs"></code></pre> <pre class="code-highlight" aria-hidden="true"><code id="code-highlight" class="hljs"></code></pre>
<textarea id="payload-text" class="code-input" spellcheck="false" data-i18n-placeholder="create.textPlaceholder" placeholder="Paste secrets, configs, notes…"></textarea> <textarea id="payload-text" class="code-input" spellcheck="false" data-i18n-placeholder="create.textPlaceholder" placeholder="Paste secrets, configs, notes…"></textarea>
@@ -43,8 +38,11 @@
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="field"> <div class="field">
<label for="available-from" data-i18n="create.availableFrom">Available from (optional)</label> <span class="field-label" data-i18n="create.availableFrom">Available from (optional)</span>
<input id="available-from" type="datetime-local" /> <div class="datetime-split">
<input id="available-from-date" type="date" aria-label="Date" />
<input id="available-from-time" type="time" step="60" aria-label="Time" />
</div>
<p class="hint" data-i18n="create.availableFromHint">Empty = available immediately</p> <p class="hint" data-i18n="create.availableFromHint">Empty = available immediately</p>
</div> </div>
<div class="field"> <div class="field">