#Frontend
The viewer is a Svelte 5 SPA in app/, written entirely in runes mode and bundled by Bun.build with bun-plugin-svelte — there is no Vite or separate bundler config; the server builds the bundle in memory at startup (src/build-app.ts, see Build & infra). Entry is app/main.ts: it initializes theme, font scale, pane widths, and hash routing on the app singleton, kicks off the manifest load, and mounts app/App.svelte. The data it renders is described in Data schema.
#State architecture
All state lives in one AppState class instance exported as app from app/lib/state.svelte.ts. Two rules keep it fast on large flakes:
- Big payloads are
$state.raw. The manifest, per-config data blobs, and file contents are immutable snapshots held in$state.rawso Svelte never deep-proxies them; updates replace the whole record (this.configs = { ...this.configs, [id]: slot }). - Index structures live outside reactivity. The
ConfigIndexes/FlakeIndexeslookup maps are built once per load inapp/lib/indexes.tsand swapped in as raw references — they are large and never mutated, so proxying them would be pure overhead.
Loading is modeled as slots: a ConfigSlot is "loading", an error object, or { data, indexes }. Errors carry an optional permanent flag — in a static export a missing document can never be fetched, so components hide the retry button. Small UI state (search query, expanded sets, hover, tooltip, pane widths, theme) uses plain $state and SvelteSet.
#Dual data mode
app/lib/data.ts gives static export and serve mode one code path. loadJson(name) first looks for an embedded <script type="application/json" id="data:<name>"> tag; if present its text is parsed, otherwise the document is fetched from ./data/<name>. isStatic() is defined as "does an embedded manifest.json tag exist" — a single-file export always embeds the manifest and the serve mode never does (it only embeds about.json), so that one tag is the static-mode signal. hasEmbedded() lets state.svelte.ts fail fast with a permanent error when a static export omits a config blob or file source instead of issuing a doomed fetch.
#URL hash deep-linking
app/lib/hash.ts encodes the current selection as the hash path and view filters behind ?:
| Form | Selection |
|---|---|
#/o/<output.path.dots> |
outputs-tree node (non-module) |
#/c/<configId> |
configuration |
#/c/<configId>/m/<moduleId> |
module within a configuration |
#/f/<fileId> |
file |
#/i/<inputName> |
flake input |
?q=<search>&all=1 |
filters: search text, "all options" toggle |
%, ?, and / are escaped in every segment; output-path segments additionally escape . because it is the path separator there. History semantics live in state.svelte.ts: select() compares old and new selection with sameSelection() — a genuine selection change calls pushState, while filter-only changes (and setFilters) call replaceState, so Back walks selections without replaying keystrokes. initRouting() applies the hash at startup and on hashchange; a deep link decoded before the manifest arrives is re-followed once loadManifest() completes.
#Supporting modules
| Module | Role |
|---|---|
app/lib/indexes.ts |
Pure data-shaping: file identity resolution (self / input / patched-input / unknown), the left-pane module tree, and the O(1) maps behind hover cross-highlighting (fileToNodes, imports/importedBy) |
app/lib/segments.ts |
Source-view segmentation: unions server-computed tree-sitter highlight runs with client-computed per-line file-reference intervals so one segment can be both colored and a clickable link |
app/lib/color.ts |
Stable colors: first ~12 registered keys get the theme's curated CVD-validated slots (--s1..--s12); beyond that FNV-1a hash → golden-angle hue → OKLCH at theme-tuned lightness/chroma |
app/lib/themes.ts |
The two THEMES stops (light/dark warm-paper palettes) as complete CSS custom-property sets applied inline on :root, plus the gen params color.ts uses for generated colors |
app/lib/url.ts |
Web links for locked inputs: webUrl() normalizes git+https etc., commitUrl() builds per-host commit permalinks (github.com, gitlab.com, codeberg.org) |
#Component map
App.svelte lays out a header, a three-pane grid with draggable splitters, and overlays. The 18 components in app/components/ group as:
| Area | Components |
|---|---|
| Chrome | Header, Splitter, Tooltip (option hover card), AboutModal |
| Left pane | OutputsTree → OutputBranch (generic outputs), TreeNode (module trees of nixos/darwin configurations) |
| Center pane | Stage switches on the selection → ModuleDetail, FileDetail, InputDetail, with Legend as the no-selection view; shared pieces OptionRow, SourceView, InputProvenance |
| Right pane | FileList → FileTreeBranch |
| Shared | Dot ("the one true dot" — colored marker with optional disclosure triangle), tree-connectors.css |
Cross-highlighting works through the state singleton: hovering a file sets app.hover, and highlightedNodes / highlightedFiles (derived from the precomputed indexes) tint the matching tree nodes in the other panes.
See Architecture for how this fits the extractor and server, and Testing for how components are tested under happy-dom.