Исправлены create UI, PWA-иконки, tooltip A2HS и breakpoint админ-меню 1015px.
This commit is contained in:
@@ -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
@@ -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 |
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user