# Writing UI documentation without screenshots Bookhoard's UI documentation uses no screenshots. This page is the standard for writing them. The flagship example of the style is the [filter system documentation](../user/filters/index.md): one system taught once, the places it appears listed as a *pattern*, samples for every field in a [generated reference](../user/filters/fields.md) that cannot drift from the UI. ## Why no screenshots Screenshots fail in every way the rest of this repo's rot is already gated against: - **They rot silently.** No gate can diff pixels against the running UI. A renamed button makes the screenshot wrong and nothing fails. - **They cannot be verified.** Shown text can: this app's UI is server-rendered from `templates/*.templ`, so every stable label and heading is a literal in the source — checkable, like routes and API examples are. - **They die with a redesign.** Text keyed to *function* survives a re-layout. Text keyed to a layout dies with its redesign. - **They exclude** (screen readers, text search) and bloat diffs. The one thing screenshots do better — "which button is it?" — text does via **exact quoted labels** plus a semantic path, and the label gate then holds that text against the source. ## The layering — what is load-bearing Write every UI page in this order. The top layers are why users don't get lost when the UI changes: | # | Layer | Content | Lifespan | | --- | --- | --- | --- | | 1 | **The pattern** | What the thing *is*, what is invariant, every place the same pattern appears | Redesign-proof | | 2 | **Function inventory** | What the user *can do*, phrased without control names | Redesign-proof | | 3 | **How-to with examples** | Numbered flows: current label → the outcome it produces | Survives most changes | | 4 | **Current labels** | Exactly quoted, bold | Gate-checked today | | 5 | **Generated references** | Enumerables regenerated from source with a currency test | Cannot drift | Labels are **tips, not the map**. Layer 3 may quote a label; layer 1 must make the page followable even if that label is renamed next week. ## The page template 1. **The pattern first.** The concept, its invariants, and everywhere it appears — "one system, three places". Never page cartography. 2. **Function inventory.** Capabilities without control names: "you can pin any narrowing to a named filter you can reopen later". 3. **How-to with examples.** Each step: the label of what you interact with, then the outcome ("the count at the bottom shows how many books will match"). An example beats an adjective. 4. **Findability note.** One line telling the reader how to re-locate the feature *by function again* after any UI change — deliberately label-independent ("it's the drawer opened from any books or podcasts list — the only place whose header can read Match ALL"). 5. **Current labels.** Quoted exactly as rendered, one canonical spelling per control ([arbitrated by the glossary](ui-controls.md)). ## Writing rules - **Semantic placement, never spatial.** No "top-right" or "at the bottom of the screen" talk. "Devices page → the **Register Device** button" survives any re-layout. - **One canonical label per control.** Two spellings of the same control is a defect, not a synonym. - **Status updates get a what-it-means table**, not a color description. - **Screenshots are never load-bearing.** Default is none. ## Tone by audience - **User Guide (`docs/user/`) — plain language, deliberately.** Short sentences. Everyday words. No developer jargon; any term the reader genuinely needs (say, *rule*) is explained once, in place, then used consistently. The reader is assumed to know books, while the app's own words are the only technical vocabulary allowed. - **Developer and Administration sections — tone is a separate discussion, not yet decided.** Write them plainly and hand-off-rich as well; do not import opinions about their voice until that pass happens. ## Generate what has a registry Anything enumerable gets generated from source with a currency test, not hand-typed. The precedent: every filter field is [generated](../user/filters/fields.md) by `TestFieldReferenceDocIsCurrent` from the same registry the menus render — the doc cannot disagree with the UI. Hand-typed lists drift fastest; when no registry exists yet, say so in the page and lean on the pattern layers. ## The gates - `TestUIControlLabels` (`internal/docs/ui_labels_test.go`): every control in the [glossary](ui-controls.md) must exist as a literal in `templates/*.templ` (or `web/src` for TS-rendered surfaces, marked `source: ts`). Renames fail `make test` in the same commit as the rename. - `TestDocsRelativeLinks`: every relative link in the docs tree resolves to a real file. Naming a new control in prose? Add its glossary entry in the same commit. Form labels inside one modal (Name, Description…) are cited inline and not glossary-tracked; the glossary carries the controls people navigate by — buttons, tabs, badges, page titles. ## The process rule **A UI change is not finished until the docs audit runs.** Moved card, renamed button, restructured page — all of it triggers: labels still resolve, links still resolve, affected pages re-read against the new surface. The pattern layers keep users oriented between audits; the audit keeps layers 3–5 honest. ## What this does not replace The filter documentation (and the older filtering overview) is hands-off exemplar territory — read it as the model, never restyle it here. New UI surfaces (scanning, devices, settings, podcasts, the reader, collections) are written this way from the start.