diff --git a/public/app.js b/public/app.js
index 1da26e7..2c2b3e1 100755
--- a/public/app.js
+++ b/public/app.js
@@ -15,8 +15,107 @@ const overlay = document.querySelector(".overlay");
navToggle.addEventListener("click", () => sidebar.classList.toggle("open"));
overlay.addEventListener("click", () => sidebar.classList.remove("open"));
-// ... (rest same as v2.2.1 up to renderMarkdown)
-
+async function loadIndex() {
+ const res = await fetch("/index.json", { cache: "no-store" });
+ INDEX = await res.json();
+ populateFilters();
+ rebuildTree();
+ window.addEventListener("popstate", () => { const hp = location.hash.startsWith("#=") ? location.hash.slice(2) : null; if (hp) openPath(hp); });
+ const init = location.hash.startsWith("#=") ? location.hash.slice(2) : INDEX.flat.sort((a, b) => b.mtime - a.mtime)[0]?.path;
+ openPath(init);
+}
+function populateFilters() {
+ filterSel.innerHTML = '';
+ for (const cat of INDEX.sections) {
+ const opt = document.createElement("option");
+ opt.value = opt.textContent = cat;
+ filterSel.appendChild(opt);
+ }
+}
+function rebuildTree() {
+ treeEl.innerHTML = "";
+ PATH_TO_EL.clear();
+ const filter = filterSel.value;
+ const sort = sortSel.value;
+ const query = searchBox.value.trim().toLowerCase();
+ const root = { type: "dir", children: INDEX.tree };
+ const pruned = filterTree(root, f => (filter === "all" || f.path.split("/")[0] === filter) && (!query || (f.title || f.name).toLowerCase().includes(query)));
+ sortDir(pruned, sort);
+ for (const c of pruned.children) treeEl.appendChild(renderNode(c));
+ treeEl.querySelectorAll(".dir").forEach(d => d.classList.add("open"));
+}
+function filterTree(node, keep) {
+ if (node.type === "file") return keep(node) ? node : null;
+ const kids = node.children.map(c => filterTree(c, keep)).filter(Boolean);
+ return kids.length ? { ...node, children: kids } : null;
+}
+function sortDir(node, sort) {
+ const cmp = sort === "name" ? (a, b) => a.name.localeCompare(b.name) :
+ sort === "old" ? (a, b) => a.mtime - b.mtime : (a, b) => b.mtime - a.mtime;
+ node.children.sort((a, b) => (a.type === "dir" && b.type !== "dir") ? -1 : (a.type !== "dir" && b.type === "dir") ? 1 : cmp(a, b));
+ node.children.forEach(c => c.type === "dir" && sortDir(c, sort));
+}
+function renderNode(node) {
+ if (node.type === "dir") {
+ const div = document.createElement("div");
+ div.className = "dir";
+ div.setAttribute("aria-expanded", "false");
+ const lbl = document.createElement("span");
+ lbl.className = "label";
+ lbl.textContent = node.name || "/";
+ lbl.addEventListener("click", () => {
+ const idx = node.children.find(c => c.type === "file" && /^index\.(md|html)$/i.test(c.name));
+ if (idx) openPath(idx.path);
+ else div.classList.toggle("open");
+ });
+ div.appendChild(lbl);
+ const kids = document.createElement("div");
+ kids.className = "children";
+ node.children.forEach(c => kids.appendChild(renderNode(c)));
+ div.appendChild(kids);
+ return div;
+ }
+ const a = document.createElement("a");
+ a.className = "file";
+ a.innerHTML = `${node.pinned ? '๐' : ''}${iconForExt(node.ext)} ${node.title} (${fmtDate(node.mtime)} ยท ${node.name})`;
+ a.addEventListener("click", e => { e.preventDefault(); openPath(node.path); });
+ PATH_TO_EL.set(node.path, a);
+ return a;
+}
+function iconForExt(ext) { return ext === ".md" ? "๐" : "๐งฉ"; }
+function fmtDate(ms) { return new Date(ms).toISOString().slice(0, 10); }
+function findDir(path) {
+ path = path.replace(/\/$/, '');
+ function search(node) {
+ if (node.type === "dir" && node.path === path) return node;
+ for (const c of node.children || []) {
+ const found = search(c);
+ if (found) return found;
+ }
+ }
+ return search({ children: INDEX.tree });
+}
+async function openPath(path) {
+ if (path === CURRENT_PATH) return;
+ CURRENT_PATH = path;
+ if (location.hash !== `#=${path}`) history.pushState(null, "", `#=${path}`);
+ let f = INDEX.flat.find(x => x.path === path);
+ if (!f) {
+ const dir = findDir(path);
+ if (dir) {
+ const idx = dir.children.find(c => c.type === "file" && /^index\.(md|html)$/i.test(c.name));
+ if (idx) return openPath(idx.path);
+ }
+ metaLine.textContent = "Path not found: " + path;
+ return;
+ }
+ metaLine.textContent = `${f.pinned ? "๐ " : ""}${fmtDate(f.mtime)} โข ${f.name}`;
+ if (f.ext === ".md") await renderMarkdown(f.path);
+ else renderHTML(f.path);
+ setActive(path);
+ updatePager();
+ if (window.innerWidth < 900) sidebar.classList.remove("open");
+}
async function renderMarkdown(path) {
mdView.style.display = "none";
const res = await fetch("/" + path);
@@ -34,5 +133,47 @@ async function renderMarkdown(path) {
requestAnimationFrame(() => { mdView.style.display = "block"; htmlView.style.display = "none"; });
if (usedFallback) console.warn("Markdown rendered as plain text: marked.js not loaded. Check CDN/SRI.");
}
-
-// ... (rest same)
\ No newline at end of file
+function renderHTML(path) {
+ htmlView.src = "/" + path;
+ htmlView.style.display = "block";
+ mdView.style.display = "none";
+}
+function setActive(path) {
+ document.querySelectorAll(".file.active").forEach(el => el.classList.remove("active"));
+ const el = PATH_TO_EL.get(path);
+ if (el) {
+ el.classList.add("active");
+ let p = el.parentElement;
+ while (p && p !== treeEl) {
+ if (p.classList.contains("children")) p.parentElement.classList.add("open");
+ p = p.parentElement;
+ }
+ }
+}
+function updatePager() {
+ const query = searchBox.value.trim().toLowerCase();
+ const list = INDEX.flat.filter(f => (filterSel.value === "all" || f.path.split("/")[0] === filterSel.value) && (!query || f.title.toLowerCase().includes(query)));
+ const cmp = sortSel.value === "name" ? (a, b) => a.name.localeCompare(b.name) :
+ sortSel.value === "old" ? (a, b) => a.mtime - b.mtime : (a, b) => b.mtime - a.mtime;
+ list.sort(cmp);
+ const i = list.findIndex(x => x.path === CURRENT_PATH);
+ prevBtn.disabled = i <= 0;
+ nextBtn.disabled = i >= list.length - 1 || i < 0;
+ prevBtn.onclick = () => i > 0 && openPath(list[i - 1].path);
+ nextBtn.onclick = () => i < list.length - 1 && openPath(list[i + 1].path);
+}
+let searchTimer;
+searchBox.addEventListener("input", () => { clearTimeout(searchTimer); searchTimer = setTimeout(rebuildTree, 300); });
+sortSel.addEventListener("change", rebuildTree);
+filterSel.addEventListener("change", rebuildTree);
+document.body.addEventListener("click", e => {
+ const a = e.target.closest("a[href]");
+ if (!a) return;
+ const href = a.getAttribute("href");
+ if (href.startsWith("/") && !href.startsWith("//") && !a.target) {
+ e.preventDefault();
+ openPath(href.replace(/^\//, ""));
+ }
+});
+window.addEventListener("resize", () => { if (window.innerWidth < 900) sidebar.classList.remove("open"); });
+window.addEventListener("DOMContentLoaded", loadIndex);
\ No newline at end of file