diff --git a/src/App.tsx b/src/App.tsx index 4d4225d..6b6c419 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -126,9 +126,7 @@ const App: Component = () => {
- addRevealedClasses(dom, stream.revealedPaths[currentPath()]) - } + onDom={(dom) => addRevealedClasses(dom, location.pathname)} /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 1d3c889..22eb335 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -61,9 +61,6 @@ const SidebarContent: Component = (props) => { if (node.children?.some((child) => !isHeadingHidden(child))) return false; return true; }; - createEffect(() => { - console.log(currentFileHeadings(), revealedHeadings()); - }); return (
diff --git a/src/components/stores/journalStream.ts b/src/components/stores/journalStream.ts index 6e22a39..2d33b4a 100644 --- a/src/components/stores/journalStream.ts +++ b/src/components/stores/journalStream.ts @@ -630,49 +630,83 @@ export function isPathRevealed(path: string, section?: string): boolean { /** * Walk every element in `root` and tag it as revealed or concealed. * - * Headings (h1–h6) update a current-heading tracker. When a higher-level - * heading is encountered (e.g. h2 after h1), all sub-headings at deeper - * levels are cleared. Each non-heading element receives a `revealed` or - * `concealed` CSS class depending on whether any of its current heading - * ancestors is present in the `revealed` set. + * Two-pass approach: + * 1. Collect all headings with their ancestor chain. + * 2. Cascade revealed status upward — if a heading is revealed, all its + * parent headings are marked revealed as well. + * 3. Walk the tree again, applying `revealed` or `concealed` classes to + * every element (headings included) based on whether any current + * heading ancestor is in the cascaded revealed set. */ -export function addRevealedClasses( - root: HTMLDivElement, - revealed: Set, -) { +export function addRevealedClasses(root: HTMLDivElement, path: string) { + if (!state.connected) return; + if (state.myRole === "gm") return; + + const normalized = normalizePath(path); + const revealed = state.revealedPaths[normalized]; if (!revealed) return; - // Current heading text for each level (h1..h6, index 1..6) - const cur: Record = {}; const HEADING_TAGS = new Set(["H1", "H2", "H3", "H4", "H5", "H6"]); - const walk = (el: Element) => { - const tag = el.tagName.toUpperCase(); - - if (HEADING_TAGS.has(tag)) { - const level = Number(tag.charAt(1)); // 1-6 - const text = el.textContent?.trim() ?? ""; - - // Update current heading at this level - cur[level] = text; - // Clear all deeper heading levels - for (let l = level + 1; l <= 6; l++) delete cur[l]; - } else { - // Non-heading: check if any current heading ancestor is revealed - const isRevealed = Object.values(cur).some((h) => revealed.has(h)); - el.classList.add(isRevealed ? "revealed" : "concealed"); - } - - // Recurse into children - for (const child of el.children) { - walk(child); - } - }; - - // Start walk from root's children (skip the container itself) - for (const child of root.children) { - walk(child); + // ---- Pass 1: collect heading hierarchy ---- + interface HeadingEntry { + el: Element; + level: number; + text: string; + /** Texts of all ancestor headings (shallowest first) */ + parents: string[]; } + const headings: HeadingEntry[] = []; + const cur: Record = {}; + + const collect = (el: Element) => { + const tag = el.tagName.toUpperCase(); + if (HEADING_TAGS.has(tag)) { + const level = Number(tag.charAt(1)); + const text = el.id || el.textContent?.trim() || ""; + const parents: string[] = []; + for (let l = 1; l < level; l++) { + if (cur[l]) parents.push(cur[l]); + } + headings.push({ el, level, text, parents }); + cur[level] = text; + for (let l = level + 1; l <= 6; l++) delete cur[l]; + } + for (const child of el.children) collect(child); + }; + for (const child of root.children) collect(child); + + // ---- Cascade: if a heading is revealed, its parents are too ---- + const revealedSet = new Set(); + for (const h of headings) { + if (revealed.has(h.text) || revealedSet.has(h.text)) { + revealedSet.add(h.text); + for (const p of h.parents) revealedSet.add(p); + } + } + + // ---- Pass 2: apply classes using the cascaded set ---- + const stack: string[] = []; + const apply = (el: Element) => { + let pushed = false; + for (const child of el.children) { + const tag = child.tagName.toUpperCase(); + if (HEADING_TAGS.has(tag)) { + if (pushed) stack.pop(); + const text = child.id || child.textContent?.trim() || ""; + stack.push(text); + pushed = true; + } + + const current = stack.length > 0 ? stack[stack.length - 1] : null; + const isRevealed = current !== null && revealedSet.has(current); + child.classList.add(isRevealed ? "revealed" : "concealed"); + + apply(child); + } + if (pushed) stack.pop(); + }; + apply(root); } /** Normalize a path for lookup: strip .md, leading ./ or / */ diff --git a/src/styles.css b/src/styles.css index 53d7428..bc905d6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -138,3 +138,14 @@ icon.big .icon-label-stroke { .col-5 { @apply lg:flex-5; } + +/* concealed / revealed */ + +.concealed { + opacity: 0; +} + +.revealed, +.concealed .revealed { + opacity: unset; +}