hue gallery & multi-document navigation — Feature Requirements
Status: shipped (G0–G5) · Date: 2026-07-29 · Scope: rendering a set of documents rather than a single file — the static HTML gallery (index.html + one page per file, with a prev/next header, a physical-line gutter and selection domains) and the interactive equivalent in the GUI/TUI (prev/next, an index view, a name+summary header). Replaced the branch-only JS preview harness (render-html.mjs), which is deleted.
NOTE
Since the sparkles:docs extraction, the static-gallery rows here are owned by the docs (SSG) spec — each DOC* row cites the GAL*/HTM* rows it absorbs, and these IDs remain valid citations. The interactive half (GAL5, GNV*) stays hue's.
NOTE
Shipped end to end: the static gallery (GAL1–GAL4, GAL6–GAL9, GAL11, GAL12) and the interactive half (GAL5, GNV*). Status legend and IDs: see the overview. The document set itself is specified in the general spec (SRC4–SRC6).
Design & rationale
hue renders one file today: it reads args[1], highlights it, and emits one ANSI stream / HTML fragment / window. Everything needed to present a collection — an index, prev/next links, a per-file header, a line-number gutter — lived outside the app, in a Node dev script that shelled out to hue --twoslash --html once per fixture and wrapped each fragment in a page shell it built in JavaScript.
That split is the problem this spec closes. The page shell is hue's own output, so it belongs in hue: the repo's D-over-scripting rule (AGENTS.md) makes the port the default, and doing it in D makes the shell testable (pure string builders over the fragment) instead of eyeball-only, drops the node dependency from the preview loop, and — because the same document set drives the interactive backends — turns a dev-only HTML trick into a feature of every rendering mode.
Two deliberate scope choices:
- Any directory, not just fixtures. The set is extension-filtered (
SRC5), so a directory of*.twoslash.jsonfixtures yields the twoslash gallery and a directory of source files yields a highlighted-source gallery. The per-file summary (GAL8) is what specializes: a twoslash node-kind tally, else language + line count. - A thin substrate, not a rival to the planned components. The document set is what the tab view
TBU1turns into tabs and what navigationLNK3/LNK4reuses to open and revisit files; the index view (GAL5) is a deliberately minimal list that the file-tree explorerTVU1replaces. This spec ships the least that makes a set navigable, shaped so those specs consume it.
Document set & gallery (GAL)
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| GAL1 | A document set must carry, per entry, the path, a display name (the file stem), and a summary (GAL8), in a stable order, plus the currently-selected index; it is acquired once (SRC5) and consumed by every mode (SRC6). | full (77a476f3) | source_set.SourceSet |
| GAL2 | With hue gallery <dir> [--out <dir>] (or --html on a directory target), write a static gallery to --out (CLI12): one standalone page per entry (<name>.html) plus an index.html listing every entry with its name and summary, each linking to its page. | full (b61f4701) | page_shell.writeGallery/galleryIndex; HTM6 |
| GAL3 | Each page must carry a navigation header: the entry name, its summary, prev and next links (disabled, not omitted, at the ends so the header does not reflow), and a link back to the index. | full (b61f4701) | page_shell.pageShell; HTM7 |
| GAL4 | Each page must number physical (source) lines in a gutter: every physical line wrapped in a .ln span whose number is a CSS ::before counter — never selected or copied (GAL7). Below-line overlay annotations must carry no number and must not advance the counter; blank lines must keep their height and survive a copied selection; the gutter width must come from the line count. | full (b61f4701) | fragment.relayoutGutter; HTM4; NUM1 parity |
| GAL5 | Interactively, a directory target must open an index view — a selectable list of entries (name + summary) that opens the chosen one in the viewer. This is a minimal list, superseded by the file-tree explorer (tree-view.md TVU1) when it lands. | full (6d86054e) | gui.runGui index mode; SRC4 |
| GAL6 | A page must use exactly one scroll container — the code pane fills the remaining height, so no nested page + <pre> scrollbars appear — and its page background must match the theme's .syn-root background so the pane and the surround are one surface. The colour must come from the theme (GalleryOptions.chrome / themeChrome), not be read back out of the emitted fragment: a fragment need not carry a <style> block at all (HTM1), and a scraper's silent fallback would ship as a mismatched page with every test still green. This holds for the whole surround, not just the page background — header, its rule, the nav links and the line-number gutter are all derived from the theme's default background and foreground, so a light theme gets light chrome instead of a light pane inside a fixed dark one. Under --dark-theme every one of them must follow html.dark as well (HTM10). | full (b61f4701; theme-sourced 1a653def; whole chrome pending) | page_shell.pageShell/options.themeChrome; HTM7 |
| GAL7 | A page must implement selection domains: a drag must be confined to the domain it starts in — the code, or one overlay annotation — so a copy never mixes the two. Layers on the pure-CSS code-only default (TWH6). Known limits: hover-popup content is not independently selectable (it is hover-ephemeral), and Ctrl/Cmd+A uses the default state. | full (b61f4701) | page_shell.pageShell (selection CSS + mousedown handler); HTM8 |
| GAL8 | Each entry must carry a one-line summary: for a twoslash payload the node-kind tally (each kind, ×n when repeated — e.g. hover×2 query); for a plain source file the language and line count. An unreadable or empty entry must summarize as such, not fail the run. | full (77a476f3) | source_set.twoslashTally/plainTally |
| GAL9 | Degradation: an empty set must produce an index saying so (not a crash or an empty directory); a file that fails to load must be reported and skipped, leaving the rest of the gallery intact; a directory in a piped non---html mode must degrade per SRC4. | full (b61f4701) | totality; DEG |
| GAL10 | Diff gallery: a diff/PR session with --html --out renders the static review site (HTM10) — the index lists changed files with ± stats, each page is one file's diff with prev/next in session order (GNV), the physical-line gutter (GAL4) generalizes to the dual old/new gutter, and split panes keep selection domains (GAL7). | not started | diff-view.md DVL4; GAL2/GAL4/GAL7 |
| GAL11 | With a shared stylesheet (HTM1) and no explicit --stylesheet, a gallery must be self-hosting: write assets/hue.css beside the pages and link it from each, so the flag combination works with no extra step. That sheet must carry the theme rules only — pageShell writes its own chrome, and the shared chrome's .syn-root .ln rules out-specify the shell's .ln ones, which would pin every page's gutter to the 3ch default. | full (8044e739) | app.runDirectoryTarget; assets.writeStylesheetAsset |
| GAL12 | A recursive set (SRC9) must render a mirrored output tree, not a flat one: one page per document at <out>/<rel-path>.html (SourceEntry.outPath) plus an index.html in every directory the pages occupy — including an intermediate directory that holds no document of its own, so no parent link dangles. A flat layout cannot express a set at repo scale at all: foo/app.d and bar/app.d are one page name, and the second silently overwrites the first. Every inter-page href must be computed relative to the page (prev/next stay the flat-sorted neighbours of GAL3/GNV1, reached by climbing out and back down; the index.html link resolves to whichever directory index sits beside the page), and a root-relative asset href — the shared stylesheet of GAL11 — must be re-based per page depth, or every page below the root loads no styles. A one-directory-deep set must keep the flat layout byte for byte: it is the depth-0 case of the same renderer, not a second one. | full (037e1e1a) | site_tree.buildSiteTree/directoryIndex; page_shell.writeGallery/pageHref/depthAdjustedHref; HTM6 |
| GAL13 | With --twoslash, a .d source is batch-extracted through twoslash-extract --dub --stdout (eager payload, no --serve — the page has no oracle to resolve tips later) and rendered as a twoslash overlay; an existing *.twoslash.json loads as today. hue still does not link the analyzer (LIV5). A missing extractor or a failed analysis is reported and that file is skipped (GAL9), and each run is bounded by a timeout so one wedged analysis cannot hang the gallery. --jobs N (0 = one per usable CPU) runs N extractions at once — one whole DMD process each (PRJ13), so nothing is shared; results fold back in set order, so the page set and index order never depend on completion order. | full | live_types.extractTwoslash; app.extractTwoslashSources |
| GAL14 | A page in a mirrored set (GAL12) must carry a breadcrumb trail: one segment per path component, each but the last linking that directory's index.html relative to the page (so the tree is navigable from a file:// URL, with no base href and no server), a per-segment hover control that copies the path up to and including that segment, and an always-visible control that copies the whole path. Without it a page five directories deep can only reach its own directory index — the header's all link — and the browser's back button is the only way further up. With --repo-url (and --repo-prefix when the gallery root is not the repository root) each segment also links the forge. The markup, class names and behaviour reproduce the VitePress Breadcrumbs.vue this replaces, so the two are interchangeable while the docs site still has both kinds of page; hue is the single implementation. | full | breadcrumbs.breadcrumbsFor/renderBreadcrumbs; page_shell.pageShell |
Interactive navigation (GNV)
The same set, navigated live. The single-file entry points keep their current behavior when no set is supplied.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| GNV1 | The GUI and TUI must navigate the set prev/next ([ / ], and the mouse back/forward buttons where available), reloading the newly-selected document in place — re-read, re-highlight, re-layout — without re-creating the window, font atlas, or grammar cache. | full (6d86054e) | loadSelected in runGui/runGuiTwoslash/TwoslashTui |
| GNV2 | A header bar must show the current entry's name and summary plus its position in the set (i/n), in every interactive backend. | full (6d86054e) | header bar in gui.d; status bar in twoslash_tui.d |
| GNV3 | Navigation must preserve per-document view state sensibly: scroll position resets to the top of the newly-opened document, while user-level toggles (theme, line numbers, preview mode) persist across the move. | full (6d86054e) | loadSelected resets scroll/search; theme + view toggles persist |
| GNV4 | The twoslash overlay view must offer the same physical-line gutter as the raw/markdown view (NUM1), so the GUI, the TUI and the HTML gallery (GAL4) all number lines alike. | full (1238123d) | runGuiTwoslash gutter (mirrors the raw view's NUM1 rule) |
| GNV5 | These keys and the reload path must be the primitive navigation LNK3 (open a linked file) and LNK4 (back/forward history) build on — one document-open path, not two. | full (6d86054e) | DocLoader/loadSelected — the single document-open path navigation.md LNK3/LNK4 will reuse |
Milestones
| Milestone | Scope | Status | Requirements |
|---|---|---|---|
| G0 | Document set: directory → ordered filtered list + summaries; --out; reusable HTML-fragment functions | full (77a476f3) | GAL1, GAL8, SRC5/SRC6, CLI12 |
| G1 | Static HTML gallery: page shell (header/nav, single scroll, theme bg), gutter, selection domains, index | full (b61f4701) | GAL2–GAL4, GAL6, GAL7, GAL9 |
| G2 | Interactive: prev/next + header + index view + twoslash gutter | full (6d86054e) | GNV1–GNV4, GAL5 |
| G3 | Retire the JS harness; the D gallery becomes the preview + docs-showcase path | full | TWD3 |
| G4 | Shared stylesheet: fragments without a <style> block, a self-hosted assets/hue.css, light/dark | full (8044e739) | GAL11, GAL6; HTM1/HTM10 |
| G5 | Whole trees: a recursive .gitignore-aware set and a path-mirroring output tree (one index per directory) | full (037e1e1a) | GAL12; SRC9/CLI32/CLI33 |
Relationship to existing specs
| Piece | Role |
|---|---|
feature-requirements.md SRC4–SRC6, CLI12 | the document set + the CLI surface this spec renders |
feature-requirements.md HTM4, HTM6–HTM8 | the HTML-sink half (gutter, gallery, page shell, selection domains) |
gui.md NUM, SEL, RND2 | the GUI gutter, selection model and single wrapped-line list this mirrors in HTML |
twoslash.md TWH6–TWH8, TWD3 | the pure-CSS code-only selection this layers on; TWD3 is the JS harness this replaces |
tree-view.md TVU1 | the richer file explorer that supersedes the minimal index view (GAL5) |
tab-view.md TBU1 | turns the same document set into open-file tabs |
navigation.md LNK3/LNK4 | reuses the document-open/reload primitive (GNV1/GNV5) for link-following and history |
→ Feature requirements · Twoslash · Tree / DAG view · Overview