Frontend: style guide
The fourth frontend document — where concepts says what the pieces are, design choices says how they're built and why, and the ledger records what it cost to learn, this page says how a notebook page should look and read: the conventions an author (human or agent) follows so every page in a lab feels like one publication. The reference implementations are the notebooks in demos/gallery; the components live in @habemus-papadum/aiui-viz.
Plumbing and porcelain
The library splits along a deliberate seam:
- Plumbing — dataflow semantics, no opinions about appearance:
cell,CellView's behavior, the worker protocol,durable,agentToolkit. This is the layer the design-choices document is about. - Porcelain — the conveniences this style guide is about: the Plot bridge (
aiui-viz/plot), the Mosaic bridge and DuckDB glue (aiui-viz/mosaic,aiui-viz/duckdb), the page chrome, math, and theming machinery (aiui-viz/site). Each porcelain surface sits on its own subpath so its heavyweight dependency (Plot, Mosaic, DuckDB-WASM, KaTeX) stays an optional peer.
Porcelain grows by extraction, not speculation. A pattern is first built and proven inside a reference notebook (where it may be rough), then promoted to the library once a second page wants it. TeX, TocRail, SiteNav, and the Mosaic/DuckDB pair (proven in seismos) all followed that path. One package holds both layers for now — if porcelain ever outgrows it, the seam is already drawn.
Document structure: the page is a short paper
Every notebook page has the same skeleton, in this order:
- Title block —
<h1>with the notebook name and a one-line thesis under it. - The overview section first (
§the-laboratory,§the-tiling, …): the complete dashboard — every visualization and control on screen at load, in a compact grid. A visitor who never scrolls has still seen the whole experiment. - Explanatory sections (
§observables,§structure-analysis, domain sections): prose that says what the numbers mean, each re-rendering its own live instance of the same widgets shown in the overview. Double-mounting is free (see design choices) and intended: the reader watches the same cell from inside the explanation, and controls stay in sync across copies. §theory— the governing mathematics, honestly tied to what's on screen (the equations whose behavior the page demonstrates, ideally with a live-computed quantity linking them).§experiments— 4–6 concrete things to try, each naming the exact controls to touch (regrow,kill k, thefoldscrubber). This section turns a viewer into an experimenter; it is not optional.
Mechanics: each section is <section id="kebab-slug"> with one <h2>. Ids are stable (they're anchor targets and TOC keys). Prose lives in the page component next to the panels it explains — it is part of the notebook, not decoration. Canvases are durable singletons and appear only in the overview; everything else may double-mount.
Table of contents
Use TocRail (aiui-viz/site) on every page: it builds itself from the page's section[id] > h2 headings after mount, highlights the section in the reading band via IntersectionObserver, and hides below ~1280px. Conventions:
- Section titles are short and lowercase (
theory, notThe Underlying Theory) — the rail is a map, not a syllabus. - 4–6 sections is the sweet spot; if a page needs more, it probably wants to be two pages.
- The rail is the page's outline; the site nav is the collection's nav. Don't mix the levels — no cross-page links in the rail, no section links in the nav items.
- Layout: the rail is a sticky grid column beside the content (not fixed-position), so it never overlaps and needs no coordinate math.
The site nav
SiteNav (aiui-viz/site) on every page — a left sidebar on desktop, collapsing to a top bar + drawer on a phone — fed from one app-level module (in the gallery, the nav items derive from the demo-discovery registry): brand, the item list, GitHub/docs links. Rules:
- Every item carries a one-line descriptor ("reaction–diffusion lab") — the nav is how a visitor learns the site has more than one experiment; a bare name doesn't teach that.
- Hrefs are relative (
./,./aztec.html) so pages survive a hosting prefix (the demo publishes under/aiui/). - Adding a page = one entry in
nav.ts+ one Vite entry. Nothing else.
Mathematics
Use TeX (aiui-viz/site; KaTeX under the hood, optional peer):
- Display math for the equations a section is about; inline math for symbols in prose.
- Math must be honest: every displayed equation is one the page actually demonstrates or computes against — no decoration mathematics. The gold standard is an equation with a live-computed quantity beside it (the measured wavelength next to the Gray-Scott system; a fitted exponent next to a power law).
- The component stamps
data-texwith the raw source — part of the DOM attribution contract (selections over rendered math recover their TeX). Don't bypass the component for one-offkatex.rendercalls; you'd lose the stamp. throwOnError: falseis the component default: a TeX typo renders red instead of blanking the page.
Plotting
Observable Plot is the default for statistical graphics, always through the PlotFigure bridge (aiui-viz/plot) — reactive options in, figure out, one seam around the imperative library. Conventions (the dataviz procedure governs color):
- Options are a function reading cells/signals — including the color-mode signal — so charts re-render on data and theme changes.
- Series colors come from the app's validated per-mode palette (the demo's
src/site/theme.ts); Plot cosmetics (axis ink, rule gray) come from the same module. Never hardcode a hex in a component. - d3 may contribute scales and tick math to plain JSX (the regime atlas) — but no d3-selection; Solid renders the SVG.
- Simulation canvases are not charts: they render as self-contained dark figures (journal plates) identical in both color modes, framed by the panel border.
Mosaic + vgplot is the direction for dataframe-shaped work — cross-filtering, aggregation over large tables, DuckDB-WASM-backed exploration. The division of labor: Plot for a chart of a cell's value; Mosaic when the data lives in a table and views coordinate through selections (brushing one view filters the others, aggregation pushed down to the database). The seismos notebook is the reference for this stack (Parquet → DuckDB-WASM → Mosaic selections → coordinated vgplot views, with a live Gutenberg–Richter fit off the filtered selection) and its bridge follows the same island discipline as PlotFigure: Mosaic owns its plots' internals and reactivity; Solid renders the shells; the shared Selection objects and the database connection are durable roots. The proven pattern has graduated: aiui-viz/mosaic exports MosaicView (coordinator + reactive directive-list spec in, a connected Plot out, marks disconnected on dispose), and aiui-viz/duckdb exports instantiateDuckDB + fetchWithProgress — the app keeps only the four ?url asset imports that make the wasm/worker bundles its own same-origin files (a library cannot own those; see the module docblock). Both are optional-peer subpaths, like aiui-viz/plot.
Phones and desktops
A notebook is one component tree, reflowed — never a separate mobile build, and never a JavaScript isMobile branch. A CSS media query is the device-conditional logic, and unlike a runtime check it cannot drift from the viewport it reasons about. The reference notebooks all render on a phone from the same markup the desktop uses.
- Desktop is the base; narrow screens are overrides. Keep the wide-screen rules as the unqualified declarations and layer phone changes inside
@media (max-width: …)blocks, so the desktop layout stays byte-identical — the same "never an automatic flip" discipline as theming. Prove it: screenshot the page at ~1440px before and after and confirm it is unchanged. - Fluid first; breakpoints only for structural switches. Prefer intrinsic sizing —
minmax(),clamp(),auto-fit,fr,%,max-widthcaps — over fixed pixel widths, so a layout degrades instead of overflowing. A fixed two-column track (540px 1fr) becomesminmax(0, 540px) minmax(0, 1fr): identical when there's room, shrinking when there isn't. Add a breakpoint only where the structure must change — the overview's figure/data split stacking to one column, a paired chart going from side-by-side to stacked. - Breakpoints belong to content, not device names. Put each one where this page's content stops fitting, not at a named phone width. The existing ones are content-derived: the
TocRailhides below ~1280px, seismos collapses its dashboard at 860px, circle stacks its board at 600px. Target the CSS-pixel band 360–414 as the phone floor (the viewport meta maps device pixels to CSS pixels; device-pixel-ratio governs sharpness, not layout) — read cleanly across that band and it works on essentially every phone. - Imperative islands re-fit themselves — let them. The simulation canvases and the pencil
PencilSurfacesize to their container through aResizeObserver, so a CSS reflow that changes an island's box is enough; the resize path is the same one a desktop window-resize already exercises. Give a stacked drawing surface an explicit height so its canvas has a box to observe. Reach for JavaScript only when an island genuinely cannot re-measure, or the interaction itself must differ — not merely because the layout changed. - Overlays that float beside a wide figure must rejoin the flow on a phone. Circle's readout and dock are
position: absoluteover a wide board on desktop; on a ~360px board that same panel buries the canvas, so below the breakpoint they becomeposition: staticand stack vertically (toggle, square stage, score, dock). An overlay that's fine over a wide plate swallows a narrow one. touch-action. A drawing surface keepstouch-action: none(or the browser eats pen/finger drags as scroll); once it shares a stacked board with normal panels, the container must allowtouch-action: pan-yso the page still scrolls when a touch lands off the canvas.- Preview on the real thing. Drive an actual browser at a phone viewport — Chrome DevTools device/responsive mode, or the session browser via the Chrome DevTools MCP (
emulate/resize_page+ a screenshot loop) — and sweep 360/390/414, then re-shoot at ~1440px to prove desktop is untouched. A real phone over the LAN (vite --host, then the machine's LAN IP on the same Wi-Fi) is the gold standard.
Theming
Covered in depth in design choices §8; the style-guide rules:
- The in-repo notebooks are system-following (owner, 2026-08-12 — restoring design-choices §8 after a dark-only period from 2026-07-19): the shared journal theme (
demos/journal) carries dark tokens as the base plus oneprefers-color-scheme: lightblock,mode()delegates to the reactivecolorMode()fromaiui-viz/site/color-mode, and there is no head stamp and nodata-theme— the media query is the single source of truth, resolved by the UA before first paint. There is no user-facing toggle, and a deviation is documented where it was decided. - The plate rule is absolute: simulation canvases and boards render on
--figure-bg, a cross-mode dark constant deliberately absent from the light block — a WebGL shader, canvas island, or worker raster never repaints on a theme flip. Do not give one demo's plate a light variant; the convention is only cheap while it has no exceptions. - All stylesheet color goes through
:roottokens (dark default, light under the media query). Literal colors (charts, SVG strokes) read the app's theme module, keyed oncolorMode()fromaiui-viz/site. - Palettes are validated per mode against that mode's actual surface — never an automatic flip. Figure colors (canvas + its legend chips) are cross-mode constants; panel-chart colors are per-mode.
Voice
Prose is compact, declarative, and specific to what's on screen ("the frozen-fraction curve is that fact as a number"), in sentence-case headings. Numbers in prose are live reads of cells wherever possible, never copied constants. Explain meaning, not mechanics — the code explains mechanics.