sheet organization: pin and reorder with up/down arrows

This commit is contained in:
Horizontal42 2026-07-01 20:31:43 +03:00
parent f8f9095fca
commit 4b5381d7e7
4 changed files with 71 additions and 6 deletions

View file

@ -58,6 +58,10 @@ const dict: Record<string, Record<string, string>> = {
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<string, Record<string, string>> = {
closeSheet: "Закрыть этот лист?",
close: "Закрыть",
cancel: "Отмена",
pin: "Закрепить",
unpin: "Открепить",
moveUp: "Выше",
moveDown: "Ниже",
},
};

View file

@ -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<typeof setTimeout> | 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);

View file

@ -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 {

View file

@ -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;