diff --git a/main.py b/main.py
index 2ba05f9..24b8362 100644
--- a/main.py
+++ b/main.py
@@ -656,12 +656,17 @@ def build_index_page(
created_key_attr = "" if created_key is None else str(created_key)
modified_ts = str(int(f.mtime))
pin_icon = "\U0001F4CC " if f.pinned else ""
+ display_title = f"{pin_icon}TODAY" if f.pinned else safe_title
+ pinned_attr = "server" if f.pinned else ""
nav_items.append(
f""
- f"{pin_icon}{safe_title}"
+ f"data-modified-ts='{modified_ts}' data-pinned='{pinned_attr}' "
+ f"data-file-path='{full_path}' href='{safe_href}'>"
+ f"{display_title}"
f"{safe_path}"
+ f""
""
)
diff --git a/static/style.css b/static/style.css
index 9f100f7..c04b209 100644
--- a/static/style.css
+++ b/static/style.css
@@ -106,6 +106,41 @@ nav {
scrollbar-gutter: stable;
}
+.nav-toolbar-header {
+ margin-bottom: 0.6rem;
+}
+
+.toggle-toolbar-btn {
+ border: 1px solid var(--line);
+ border-radius: 0.45rem;
+ background: var(--panel);
+ color: var(--ink-soft);
+ padding: 0.3rem 0.6rem;
+ font-size: 0.8rem;
+ cursor: pointer;
+ width: 100%;
+ text-align: left;
+}
+
+.toggle-toolbar-btn:hover {
+ border-color: var(--accent);
+ color: var(--ink);
+}
+
+.nav-toolbar {
+ overflow: hidden;
+ transition: max-height 0.2s ease, opacity 0.2s ease;
+ max-height: 500px;
+ opacity: 1;
+}
+
+.nav-toolbar-hidden {
+ max-height: 0;
+ opacity: 0;
+ pointer-events: none;
+ margin: 0;
+}
+
.search-wrap {
display: grid;
gap: 0.35rem;
@@ -156,11 +191,12 @@ nav {
.file-link {
display: block;
+ position: relative;
width: 100%;
max-width: 100%;
min-width: 0;
margin-bottom: 0.5rem;
- padding: 0.6rem 0.7rem;
+ padding: 0.6rem 2rem 0.6rem 0.7rem;
border: 1px solid var(--line);
border-radius: 0.5rem;
color: var(--ink-soft);
@@ -168,6 +204,11 @@ nav {
background: var(--panel);
}
+.file-link[data-pinned="user"] {
+ border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
+ background: color-mix(in srgb, var(--accent-soft) 30%, var(--panel));
+}
+
.file-title {
display: block;
font-weight: 600;
@@ -199,6 +240,37 @@ nav {
font-weight: 600;
}
+.pin-toggle {
+ position: absolute;
+ top: 0.45rem;
+ right: 0.35rem;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ font-size: 0.82rem;
+ line-height: 1;
+ padding: 0.15rem;
+ border-radius: 0.25rem;
+ opacity: 0;
+ transition: opacity 0.15s;
+ color: var(--ink-soft);
+}
+
+.file-link:hover .pin-toggle,
+.file-link:focus-within .pin-toggle {
+ opacity: 0.5;
+}
+
+.file-link:hover .pin-toggle:hover,
+.file-link:focus-within .pin-toggle:hover {
+ opacity: 1;
+}
+
+.file-link[data-pinned="user"] .pin-toggle,
+.file-link[data-pinned="server"] .pin-toggle {
+ opacity: 1;
+}
+
main {
padding: 1.2rem;
overflow: auto;
diff --git a/templates/index.html b/templates/index.html
index 56675c8..5195a13 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -17,12 +17,17 @@
Search titles
-
-
-
-
-
-
+
+
{{NAV_ITEMS}}
@@ -70,6 +75,43 @@
applyTheme(next);
});
+ var navToolbar = document.getElementById("nav-toolbar");
+ var toggleBtn = document.getElementById("toggle-nav-toolbar");
+
+ function getCookie(name) {
+ var match = document.cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]*)"));
+ return match ? decodeURIComponent(match[1]) : null;
+ }
+
+ function setCookie(name, value, days) {
+ var expires = "";
+ if (days) {
+ var d = new Date();
+ d.setTime(d.getTime() + days * 86400000);
+ expires = "; expires=" + d.toUTCString();
+ }
+ document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/; SameSite=Lax";
+ }
+
+ function applyToolbarState(collapsed) {
+ if (collapsed) {
+ navToolbar.classList.add("nav-toolbar-hidden");
+ toggleBtn.innerHTML = "▶ Filters";
+ } else {
+ navToolbar.classList.remove("nav-toolbar-hidden");
+ toggleBtn.innerHTML = "▼ Filters";
+ }
+ }
+
+ var toolbarCollapsed = getCookie("org_web_nav_toolbar") === "hidden";
+ applyToolbarState(toolbarCollapsed);
+
+ toggleBtn.addEventListener("click", function () {
+ toolbarCollapsed = !toolbarCollapsed;
+ setCookie("org_web_nav_toolbar", toolbarCollapsed ? "hidden" : "visible", 365);
+ applyToolbarState(toolbarCollapsed);
+ });
+
const searchInput = document.getElementById("title-search");
const fileList = document.getElementById("file-list");
const shuffleNotesBtn = document.getElementById("shuffle-notes");
@@ -79,22 +121,110 @@
const jumpCurrentBtn = document.getElementById("jump-current");
const fileLinks = Array.from(document.querySelectorAll("#file-list .file-link"));
const activeFileLink = document.querySelector("#file-list .file-link.active");
- searchInput.addEventListener("input", function () {
- const query = searchInput.value.trim().toLowerCase();
- for (const link of fileLinks) {
- const haystack = link.getAttribute("data-search") || "";
- link.style.display = haystack.includes(query) ? "" : "none";
+
+ var PIN_STORAGE_KEY = "org_web_viewer_pins";
+
+ function loadUserPins() {
+ try {
+ var raw = localStorage.getItem(PIN_STORAGE_KEY);
+ return raw ? new Set(JSON.parse(raw)) : new Set();
+ } catch (e) {
+ return new Set();
}
- });
+ }
+
+ function saveUserPins(pinSet) {
+ localStorage.setItem(PIN_STORAGE_KEY, JSON.stringify(Array.from(pinSet)));
+ }
+
+ var userPins = loadUserPins();
+
+ function applyUserPins() {
+ for (var i = 0; i < fileLinks.length; i++) {
+ var link = fileLinks[i];
+ var filePath = link.getAttribute("data-file-path") || "";
+ var serverPinned = link.getAttribute("data-pinned") === "server";
+ if (serverPinned) {
+ continue;
+ }
+ if (userPins.has(filePath)) {
+ link.setAttribute("data-pinned", "user");
+ var titleSpan = link.querySelector(".file-title");
+ if (titleSpan && titleSpan.textContent.indexOf("\uD83D\uDCCC") !== 0) {
+ titleSpan.textContent = "\uD83D\uDCCC " + titleSpan.textContent;
+ }
+ } else {
+ link.setAttribute("data-pinned", "");
+ var titleSpan2 = link.querySelector(".file-title");
+ if (titleSpan2 && titleSpan2.textContent.indexOf("\uD83D\uDCCC") === 0) {
+ titleSpan2.textContent = titleSpan2.textContent.replace(/^\uD83D\uDCCC\s*/, "");
+ }
+ }
+ }
+ }
+
+ function pinRank(link) {
+ var p = link.getAttribute("data-pinned") || "";
+ if (p === "server") return 0;
+ if (p === "user") return 1;
+ return 2;
+ }
+
+ function stableSortWithPins(links, compareFn) {
+ links.sort(function (a, b) {
+ var ra = pinRank(a);
+ var rb = pinRank(b);
+ if (ra !== rb) return ra - rb;
+ if (ra < 2) return 0;
+ return compareFn(a, b);
+ });
+ }
function reorderLinks(links) {
- const fragment = document.createDocumentFragment();
- for (const link of links) {
- fragment.appendChild(link);
+ var fragment = document.createDocumentFragment();
+ for (var i = 0; i < links.length; i++) {
+ fragment.appendChild(links[i]);
}
fileList.appendChild(fragment);
}
+ function reorderWithPins() {
+ var links = Array.from(fileList.querySelectorAll(".file-link"));
+ stableSortWithPins(links, function () { return 0; });
+ reorderLinks(links);
+ }
+
+ applyUserPins();
+ reorderWithPins();
+
+ fileList.addEventListener("click", function (e) {
+ var btn = e.target.closest(".pin-toggle");
+ if (!btn) return;
+ e.preventDefault();
+ e.stopPropagation();
+ var link = btn.closest(".file-link");
+ if (!link) return;
+ var filePath = link.getAttribute("data-file-path") || "";
+ if (link.getAttribute("data-pinned") === "server") return;
+ if (userPins.has(filePath)) {
+ userPins.delete(filePath);
+ } else {
+ userPins.add(filePath);
+ }
+ saveUserPins(userPins);
+ applyUserPins();
+ reorderWithPins();
+ });
+
+ searchInput.addEventListener("input", function () {
+ var query = searchInput.value.trim().toLowerCase();
+ for (var i = 0; i < fileLinks.length; i++) {
+ var link = fileLinks[i];
+ var haystack = link.getAttribute("data-search") || "";
+ link.style.display = haystack.includes(query) ? "" : "none";
+ }
+ });
+
jumpCurrentBtn.addEventListener("click", function () {
if (!activeFileLink) {
return;
@@ -103,38 +233,47 @@
});
shuffleNotesBtn.addEventListener("click", function () {
- const links = Array.from(fileList.querySelectorAll(".file-link"));
- for (let i = links.length - 1; i > 0; i -= 1) {
- const j = Math.floor(Math.random() * (i + 1));
- const tmp = links[i];
- links[i] = links[j];
- links[j] = tmp;
+ var links = Array.from(fileList.querySelectorAll(".file-link"));
+ var pinned = [];
+ var rest = [];
+ for (var i = 0; i < links.length; i++) {
+ if (pinRank(links[i]) < 2) {
+ pinned.push(links[i]);
+ } else {
+ rest.push(links[i]);
+ }
}
- reorderLinks(links);
+ for (var j = rest.length - 1; j > 0; j -= 1) {
+ var k = Math.floor(Math.random() * (j + 1));
+ var tmp = rest[j];
+ rest[j] = rest[k];
+ rest[k] = tmp;
+ }
+ reorderLinks(pinned.concat(rest));
});
sortBacklinksBtn.addEventListener("click", function () {
- const links = Array.from(fileList.querySelectorAll(".file-link"));
- links.sort(function (a, b) {
- const aCount = Number.parseInt(a.getAttribute("data-backlinks") || "0", 10);
- const bCount = Number.parseInt(b.getAttribute("data-backlinks") || "0", 10);
+ var links = Array.from(fileList.querySelectorAll(".file-link"));
+ stableSortWithPins(links, function (a, b) {
+ var aCount = Number.parseInt(a.getAttribute("data-backlinks") || "0", 10);
+ var bCount = Number.parseInt(b.getAttribute("data-backlinks") || "0", 10);
if (aCount !== bCount) {
return bCount - aCount;
}
- const aKey = a.getAttribute("data-search") || "";
- const bKey = b.getAttribute("data-search") || "";
+ var aKey = a.getAttribute("data-search") || "";
+ var bKey = b.getAttribute("data-search") || "";
return aKey.localeCompare(bKey);
});
reorderLinks(links);
});
sortCreatedBtn.addEventListener("click", function () {
- const links = Array.from(fileList.querySelectorAll(".file-link"));
- links.sort(function (a, b) {
- const aRaw = a.getAttribute("data-created-ts") || "";
- const bRaw = b.getAttribute("data-created-ts") || "";
- const aMissing = aRaw === "";
- const bMissing = bRaw === "";
+ var links = Array.from(fileList.querySelectorAll(".file-link"));
+ stableSortWithPins(links, function (a, b) {
+ var aRaw = a.getAttribute("data-created-ts") || "";
+ var bRaw = b.getAttribute("data-created-ts") || "";
+ var aMissing = aRaw === "";
+ var bMissing = bRaw === "";
if (aMissing && !bMissing) {
return -1;
}
@@ -142,29 +281,29 @@
return 1;
}
if (!aMissing && !bMissing) {
- const aTs = Number.parseInt(aRaw, 10);
- const bTs = Number.parseInt(bRaw, 10);
+ var aTs = Number.parseInt(aRaw, 10);
+ var bTs = Number.parseInt(bRaw, 10);
if (aTs !== bTs) {
return aTs - bTs;
}
}
- const aKey = a.getAttribute("data-search") || "";
- const bKey = b.getAttribute("data-search") || "";
+ var aKey = a.getAttribute("data-search") || "";
+ var bKey = b.getAttribute("data-search") || "";
return aKey.localeCompare(bKey);
});
reorderLinks(links);
});
sortModifiedBtn.addEventListener("click", function () {
- const links = Array.from(fileList.querySelectorAll(".file-link"));
- links.sort(function (a, b) {
- const aTs = Number.parseInt(a.getAttribute("data-modified-ts") || "0", 10);
- const bTs = Number.parseInt(b.getAttribute("data-modified-ts") || "0", 10);
+ var links = Array.from(fileList.querySelectorAll(".file-link"));
+ stableSortWithPins(links, function (a, b) {
+ var aTs = Number.parseInt(a.getAttribute("data-modified-ts") || "0", 10);
+ var bTs = Number.parseInt(b.getAttribute("data-modified-ts") || "0", 10);
if (aTs !== bTs) {
return bTs - aTs;
}
- const aKey = a.getAttribute("data-search") || "";
- const bKey = b.getAttribute("data-search") || "";
+ var aKey = a.getAttribute("data-search") || "";
+ var bKey = b.getAttribute("data-search") || "";
return aKey.localeCompare(bKey);
});
reorderLinks(links);