From 4b5381d7e78e3fd35da074ae846c7ea3d1651428 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:31:43 +0300 Subject: [PATCH] sheet organization: pin and reorder with up/down arrows --- src/i18n.ts | 8 ++++++++ src/main.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++++--- src/storage.ts | 1 + src/ui/app.css | 17 ++++++++++++++--- 4 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 6dcc77b..2e1a5f3 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -58,6 +58,10 @@ const dict: Record> = { closeSheet: "Close this sheet?", close: "Close", cancel: "Cancel", + pin: "Pin", + unpin: "Unpin", + moveUp: "Move up", + moveDown: "Move down", }, ru: { settings: "Настройки", @@ -116,6 +120,10 @@ const dict: Record> = { closeSheet: "Закрыть этот лист?", close: "Закрыть", cancel: "Отмена", + pin: "Закрепить", + unpin: "Открепить", + moveUp: "Выше", + moveDown: "Ниже", }, }; diff --git a/src/main.ts b/src/main.ts index dc9f2fe..47a531e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -54,19 +54,65 @@ function syncTitleField(): void { if (document.activeElement !== field) field.value = activeDoc()?.title ?? ""; } +function pinDoc(id: string): void { + const doc = data.docs.find((d) => d.id === id); + if (!doc) return; + doc.pinned = !doc.pinned; + if (doc.pinned) { + // move to end of pinned group + data.docs = data.docs.filter((d) => d.id !== id); + let lastPinned = -1; + for (let i = data.docs.length - 1; i >= 0; i--) { if (data.docs[i].pinned) { lastPinned = i; break; } } + data.docs.splice(lastPinned + 1, 0, doc); + } else { + // move to start of non-pinned group + data.docs = data.docs.filter((d) => d.id !== id); + const firstNonPinned = data.docs.findIndex((d) => !d.pinned); + data.docs.splice(firstNonPinned === -1 ? data.docs.length : firstNonPinned, 0, doc); + } + saveAppData(data); + renderDocList(); +} + +function moveDoc(id: string, dir: -1 | 1): void { + const idx = data.docs.findIndex((d) => d.id === id); + if (idx < 0) return; + const doc = data.docs[idx]; + const target = data.docs[idx + dir]; + if (!target || !!target.pinned !== !!doc.pinned) return; // can't cross pin boundary + data.docs.splice(idx, 1); + data.docs.splice(idx + dir, 0, doc); + saveAppData(data); + renderDocList(); +} + function renderDocList(): void { const list = $("#doc-list"); list.replaceChildren(); for (const doc of data.docs) { const el = document.createElement("div"); - el.className = "doc-item" + (doc.id === data.activeId ? " active" : ""); + el.className = "doc-item" + (doc.id === data.activeId ? " active" : "") + (doc.pinned ? " pinned" : ""); const name = document.createElement("span"); + name.className = "doc-name"; name.textContent = doc.title || t("untitled"); el.appendChild(name); + + const mkBtn = (cls: string, text: string, title: string, cb: () => void): HTMLButtonElement => { + const b = document.createElement("button"); + b.className = cls; + b.textContent = text; + b.title = title; + b.addEventListener("click", (e) => { e.stopPropagation(); cb(); }); + return b; + }; + + el.appendChild(mkBtn("pin-btn" + (doc.pinned ? " active" : ""), "📌", doc.pinned ? t("unpin") : t("pin"), () => pinDoc(doc.id))); + el.appendChild(mkBtn("move-btn", "↑", t("moveUp"), () => moveDoc(doc.id, -1))); + el.appendChild(mkBtn("move-btn", "↓", t("moveDown"), () => moveDoc(doc.id, 1))); + const del = document.createElement("button"); del.className = "del"; del.textContent = "✕"; - // two-step inline confirmation instead of the native dialog let confirmTimer: ReturnType | null = null; del.addEventListener("click", (e) => { e.stopPropagation(); @@ -81,7 +127,6 @@ function renderDocList(): void { return; } if (confirmTimer) clearTimeout(confirmTimer); - // the sheet goes to backups/deleted/ — restore by dragging it back void backupDeletedSheet(settings.dataDir, doc.title, data.contents[doc.id] ?? ""); delete data.contents[doc.id]; data.docs = data.docs.filter((d) => d.id !== doc.id); diff --git a/src/storage.ts b/src/storage.ts index fad1231..d4cf55d 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -5,6 +5,7 @@ export interface DocMeta { title: string; /** user renamed the document — stop deriving the title from the first line */ customTitle?: boolean; + pinned?: boolean; } export interface AppData { diff --git a/src/ui/app.css b/src/ui/app.css index ea4ff6f..7b5bc08 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -83,23 +83,34 @@ body { } .doc-item:hover { background: var(--hover); } .doc-item.active { background: var(--sel); } +.doc-name { flex: 1; overflow: hidden; text-overflow: ellipsis; } +.doc-item .pin-btn, +.doc-item .move-btn, .doc-item .del { - margin-left: auto; + flex-shrink: 0; opacity: 0; color: var(--fg-muted); border: none; background: none; cursor: pointer; - font-size: 12px; + font-size: 11px; border-radius: 4px; - padding: 2px 6px; + padding: 2px 4px; + line-height: 1; } +.doc-item:hover .pin-btn, +.doc-item:hover .move-btn, +.doc-item:hover .del { opacity: 0.7; } +.doc-item .pin-btn.active { opacity: 1; } +.doc-item:hover .pin-btn.active { opacity: 1; } +.doc-item .del { font-size: 12px; padding: 2px 6px; } .doc-item:hover .del { opacity: 1; } .doc-item .del.confirm { opacity: 1; background: #d63a3a; color: #fff; } +.doc-item.pinned .doc-name { font-weight: 500; } #new-doc { border: 1px dashed var(--border); background: none;