Compare commits

...
2 Commits
Author SHA1 Message Date
luxick b14941b3e0 Adjust sidebar tree 2026-09-07 16:23:38 +02:00
luxick adf1540cf9 Update link colors 2026-09-07 15:37:43 +02:00
3 changed files with 211 additions and 206 deletions
+4 -12
View File
@@ -72,10 +72,10 @@ body {
* { box-sizing: border-box; margin: 0; padding: 0; }
a { color: var(--text); text-decoration: none; }
a:hover { color: var(--primary-hover); }
a:hover { color: var(--link-hover); }
.content a { color: var(--link); }
.content a:hover { color: var(--link-hover); }
.content a.broken { color: var(--primary-hover); text-decoration: line-through; }
.content a.broken { color: var(--link-hover); text-decoration: line-through; }
.content a.broken:hover { color: var(--link-hover); }
hr { border: none; border-top: var(--border-dashed); margin: var(--space-4) 0; }
@@ -714,14 +714,13 @@ aside.sidebar:empty { display: none; }
border-left: 3px solid var(--primary);
padding-left: calc(var(--space-2) - 3px);
}
.tree-row.is-active > .tree-name { color: var(--secondary); }
.tree-row.is-disabled { color: var(--text-muted); cursor: default; }
.tree-row.is-disabled:hover { background: none; }
.tree-chevron { text-align: center; flex-shrink: 0; }
.tree-chevron { width: 1.25rem; color: var(--secondary); }
.tree-chevron.is-leaf { visibility: hidden; }
.tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Each nesting level gets a vertical guide rail down its left edge so depth
stays scannable in the compact view (cf. VS Code / file-explorer trees). */
.tree-children {
margin-left: var(--space-2);
padding-left: var(--space-2);
@@ -732,14 +731,7 @@ aside.sidebar:empty { display: none; }
padding: var(--space-1) 0;
word-break: break-all;
}
/* Current-page row in the navigation rail. Mirrors .tree-row.is-selected so the
two tree surfaces stay visually consistent. */
.tree-row.is-active {
background: var(--bg-panel-hover);
border-left: 3px solid var(--primary);
padding-left: calc(var(--space-2) - 3px);
}
.tree-row.is-active > .tree-name { color: var(--primary-hover); }
/* Tree sidebar (persistent navigation) */
.tree-sidebar {
+141 -126
View File
@@ -1,33 +1,29 @@
(function () {
;(function () {
// This script owns the .tree-scroll child of the aside, not the aside itself.
var container = document.querySelector('aside.tree-sidebar .tree-scroll');
if (!container) return;
var container = document.querySelector("aside.tree-sidebar .tree-scroll")
if (!container) return
function joinPath(parent, name) {
if (parent === '/' || parent === '') return '/' + name;
return parent.replace(/\/+$/, '') + '/' + name;
if (parent === "/" || parent === "") return "/" + name
return parent.replace(/\/+$/, "") + "/" + name
}
function encodeSegments(p) {
return p.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/');
return p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/")
}
function folderHref(p) {
if (p === '/' || p === '') return '/';
return '/' + encodeSegments(p) + '/';
}
function fileHref(p) {
return '/' + encodeSegments(p);
if (p === "/" || p === "") return "/"
return "/" + encodeSegments(p) + "/"
}
function fetchFolder(path, expandTo) {
var url = folderHref(path) + '?tree=1';
if (expandTo) url += '&expandTo=' + encodeURIComponent(expandTo);
return fetch(url, { credentials: 'same-origin' }).then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
});
var url = folderHref(path) + "?tree=1&foldersOnly=1"
if (expandTo) url += "&expandTo=" + encodeURIComponent(expandTo)
return fetch(url, { credentials: "same-origin" }).then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status)
return r.json()
})
}
// Current page as a canonical, DECODED wiki path ("/" for root, no trailing
@@ -35,166 +31,185 @@
// thus fullPath) are decoded, so we must decode here too or paths containing
// spaces / non-ASCII never match a row and the chain never expands.
var activePath = (function () {
var p = window.location.pathname.replace(/\/+$/, '');
if (p === '') return '/';
try { return decodeURIComponent(p); } catch (e) { return p; }
})();
var activeRow = null;
var p = window.location.pathname.replace(/\/+$/, "")
if (p === "") return "/"
try {
return decodeURIComponent(p)
} catch (e) {
return p
}
})()
var activeRow = null
function disabledRow(text) {
var row = document.createElement('div');
row.className = 'tree-row is-disabled';
row.textContent = text;
return row;
var row = document.createElement("div")
row.className = "tree-row is-disabled"
row.textContent = text
return row
}
// renderInto fills containerEl with rows for `entries` (children of
// parentPath). Folders carrying a `children` array (the pre-expanded
// ancestor chain from ?expandTo) are opened immediately and recurse.
function renderInto(containerEl, parentPath, entries) {
if (!entries || entries.length === 0) {
containerEl.appendChild(disabledRow('(empty)'));
return;
}
entries.forEach(function (entry) {
var built = buildRow(parentPath, entry);
containerEl.appendChild(built.row);
if (built.preExpand) built.open();
});
var built = buildRow(parentPath, entry)
containerEl.appendChild(built.row)
if (built.preExpand) built.open()
})
}
function buildRow(parentPath, entry) {
var fullPath = joinPath(parentPath, entry.name);
var isFolder = entry.kind === 'folder';
var fullPath = joinPath(parentPath, entry.name)
var row = document.createElement("div")
row.className = "tree-row"
var row = document.createElement('div');
row.className = 'tree-row';
var chevron = document.createElement("span")
chevron.className = "tree-chevron"
chevron.textContent = "▸"
row.appendChild(chevron)
var chevron = document.createElement('span');
chevron.className = 'tree-chevron';
if (isFolder) chevron.textContent = '▸'; // ▸
else chevron.classList.add('is-leaf');
row.appendChild(chevron);
var link = document.createElement("a")
link.className = "tree-name tree-folder"
link.textContent = entry.name
link.href = folderHref(fullPath)
row.appendChild(link)
var link = document.createElement('a');
link.className = 'tree-name ' + (isFolder ? 'tree-folder' : 'tree-file');
link.textContent = entry.name;
link.href = isFolder ? folderHref(fullPath) : fileHref(fullPath);
row.appendChild(link);
if (isFolder && fullPath === activePath) {
row.classList.add('is-active');
activeRow = row;
if (fullPath === activePath) {
row.classList.add("is-active")
activeRow = row
}
var preChildren = (isFolder && entry.children) ? entry.children : null;
var childrenEl = null;
var loaded = false;
var isOpen = false;
var preChildren = entry.children ? entry.children : null
var childrenEl = null
var loaded = false
var isOpen = false
var isLeaf = false
function ensureChildrenEl() {
if (!childrenEl) {
childrenEl = document.createElement('div');
childrenEl.className = 'tree-children';
childrenEl = document.createElement("div")
childrenEl.className = "tree-children"
}
return childrenEl
}
function markLeaf() {
isLeaf = true
isOpen = false
chevron.textContent = "▸"
chevron.classList.add("is-leaf")
if (childrenEl && childrenEl.parentNode) {
childrenEl.parentNode.removeChild(childrenEl)
}
return childrenEl;
}
function loadChildren() {
var el = ensureChildrenEl();
el.textContent = '';
el.appendChild(disabledRow('…'));
fetchFolder(fullPath).then(function (resp) {
el.textContent = '';
renderInto(el, fullPath, resp.entries);
loaded = true;
}).catch(function () {
el.textContent = '';
var err = document.createElement('div');
err.className = 'tree-row';
err.textContent = '(failed — tap to retry)';
err.addEventListener('click', function (e) {
e.stopPropagation();
loadChildren();
});
el.appendChild(err);
});
var el = ensureChildrenEl()
el.textContent = ""
el.appendChild(disabledRow("…"))
fetchFolder(fullPath)
.then(function (resp) {
el.textContent = ""
loaded = true
if (!resp.entries || resp.entries.length === 0) {
markLeaf()
return
}
renderInto(el, fullPath, resp.entries)
})
.catch(function () {
el.textContent = ""
var err = document.createElement("div")
err.className = "tree-row"
err.textContent = "(failed — tap to retry)"
err.addEventListener("click", function (e) {
e.stopPropagation()
loadChildren()
})
el.appendChild(err)
})
}
function open() {
if (!isFolder || isOpen) return;
var el = ensureChildrenEl();
row.parentNode.insertBefore(el, row.nextSibling);
chevron.textContent = '▾'; // ▾
isOpen = true;
if (isOpen || isLeaf) return
var el = ensureChildrenEl()
row.parentNode.insertBefore(el, row.nextSibling)
chevron.textContent = "▾" // ▾
isOpen = true
if (!loaded) {
if (preChildren) {
renderInto(el, fullPath, preChildren);
loaded = true;
renderInto(el, fullPath, preChildren)
loaded = true
} else {
loadChildren();
loadChildren()
}
}
}
function close() {
if (!isFolder || !isOpen) return;
if (!isOpen) return
if (childrenEl && childrenEl.parentNode) {
childrenEl.parentNode.removeChild(childrenEl);
childrenEl.parentNode.removeChild(childrenEl)
}
chevron.textContent = '▸';
isOpen = false;
chevron.textContent = "▸"
isOpen = false
}
// Clicking the name link navigates (folder) or opens the file; clicking
// anywhere else on the row toggles expansion for folders.
row.addEventListener('click', function (e) {
if (e.target.closest('a.tree-name')) return;
if (!isFolder) return;
if (isOpen) close(); else open();
});
row.addEventListener("click", function (e) {
if (e.target.closest("a.tree-name")) return
if (isOpen) close()
else open()
})
return { row: row, open: open, preExpand: !!preChildren };
return { row: row, open: open, preExpand: !!preChildren }
}
function render() {
container.textContent = '';
var listing = document.createElement('div');
container.appendChild(listing);
listing.appendChild(disabledRow('…'));
container.textContent = ""
var listing = document.createElement("div")
container.appendChild(listing)
listing.appendChild(disabledRow("…"))
fetchFolder('/', activePath === '/' ? '' : activePath).then(function (resp) {
listing.textContent = '';
renderInto(listing, '/', resp.entries);
fetchFolder("/", activePath === "/" ? "" : activePath)
.then(function (resp) {
listing.textContent = ""
renderInto(listing, "/", resp.entries)
if (activeRow) {
try { activeRow.scrollIntoView({ block: 'nearest' }); } catch (e) {}
try {
activeRow.scrollIntoView({ block: "nearest" })
} catch (e) {}
}
}).catch(function () {
listing.textContent = '';
var err = document.createElement('div');
err.className = 'tree-row';
err.textContent = '(failed — tap to retry)';
err.addEventListener('click', function () { render(); });
listing.appendChild(err);
});
})
.catch(function () {
listing.textContent = ""
var err = document.createElement("div")
err.className = "tree-row"
err.textContent = "(failed — tap to retry)"
err.addEventListener("click", function () {
render()
})
listing.appendChild(err)
})
}
function setupMenuItem() {
var specials = document.querySelector('.actions-special');
if (!specials) return;
var specials = document.querySelector(".actions-special")
if (!specials) return
var item = document.createElement("button");
item.type = "button";
item.className = "btn btn-block";
item.title = "Folders";
item.textContent = "FOLDERS";
var item = document.createElement("button")
item.type = "button"
item.className = "btn btn-block"
item.title = "Folders"
item.textContent = "FOLDERS"
item.addEventListener("click", function () {
if (typeof openOverlay === "function") openOverlay(container);
});
specials.appendChild(item);
if (typeof openOverlay === "function") openOverlay(container)
})
specials.appendChild(item)
}
render();
setupMenuItem();
})();
render()
setupMenuItem()
})()
+10 -12
View File
@@ -40,18 +40,16 @@ func (h *handler) handleTree(w http.ResponseWriter, r *http.Request, urlPath, fs
return
}
entries, err := listTreeEntries(fsPath)
foldersOnly := r.URL.Query().Has("foldersOnly")
entries, err := listTreeEntries(fsPath, foldersOnly)
if err != nil {
http.Error(w, "read failed", http.StatusInternalServerError)
return
}
// expandTo asks for a nested listing: each folder along the ancestor chain
// carries its own children, recursively, down to the target. Used by the
// tree sidebar to render the current page's chain in one request. The flat
// picker omits expandTo and is unaffected.
if expandTo := r.URL.Query().Get("expandTo"); expandTo != "" {
expandTreeChain(fsPath, entries, treePathSegments(expandTo))
expandTreeChain(fsPath, entries, treePathSegments(expandTo), foldersOnly)
}
resp := treeResponse{Path: canonicalTreePath(urlPath), Entries: entries}
@@ -70,8 +68,8 @@ func canonicalTreePath(urlPath string) string {
// listTreeEntries returns the immediate children of fsPath, filtering hidden
// entries and index.md. Folders are listed before files; both groups are
// sorted alphabetically.
func listTreeEntries(fsPath string) ([]treeEntry, error) {
// sorted alphabetically. foldersOnly will not list any individual files at all.
func listTreeEntries(fsPath string, foldersOnly bool) ([]treeEntry, error) {
raw, err := os.ReadDir(fsPath)
if err != nil {
return nil, err
@@ -85,7 +83,7 @@ func listTreeEntries(fsPath string) ([]treeEntry, error) {
if e.IsDir() {
folders = append(folders, treeEntry{Name: name, Kind: "folder"})
} else {
if name == "index.md" {
if foldersOnly || name == "index.md" {
continue
}
files = append(files, treeEntry{Name: name, Kind: "file"})
@@ -113,7 +111,7 @@ func treePathSegments(p string) []string {
// far as it still exists. Segments only ever match real directory names from
// listTreeEntries (no "." or ".." entries), so this cannot traverse outside the
// listed tree.
func expandTreeChain(fsPath string, entries []treeEntry, segs []string) {
func expandTreeChain(fsPath string, entries []treeEntry, segs []string, foldersOnly bool) {
if len(segs) == 0 {
return
}
@@ -122,12 +120,12 @@ func expandTreeChain(fsPath string, entries []treeEntry, segs []string) {
continue
}
childFs := filepath.Join(fsPath, segs[0])
kids, err := listTreeEntries(childFs)
kids, err := listTreeEntries(childFs, foldersOnly)
if err != nil {
return
}
entries[i].Children = kids
expandTreeChain(childFs, entries[i].Children, segs[1:])
expandTreeChain(childFs, entries[i].Children, segs[1:], foldersOnly)
return
}
}