docs(standard): screenshot-free UI documentation standard + UI-label gate

The planned Playwright screenshot docs (SCREENSHOT_AUTOMATION.md) are
replaced by a text standard: screenshots rot silently and cannot be
gate-checked, while this UI's labels are server-rendered templ literals
that can. Layers: pattern-first prose (concepts survive redesigns) >
capability inventory > label->outcome flows > quoted labels > generated
references where a registry exists (filters/fields.md precedent). The
filter docs stay hands-off exemplars; user-doc tone is plain language by
contract, dev/admin tone explicitly deferred.

writing-ui-docs.md is the standard (page template, findability notes,
tone-by-audience, process rule: a UI change is not done until the docs
audit runs). ui-controls.md is the canonical control-label glossary the
docs quote from; TestUIControlLabels greps every entry against
templates/*.templ (source: ts override; also:-fragments for
Alpine-composed labels like the Match/Group header) so renames fail
make test in the same commit as the rename. Gate proven both ways: a
planted bogus label failed; restored set passes.
This commit is contained in:
John O'Keefe committed 2026-11-02 08:18:19 -05:00
1 parent fbdd4ab9f9
commit e07301f74a
3 files changed
+498

No files matched your search

+166
View File
@@ -0,0 +1,166 @@
<!-- Gate-checked by TestUIControlLabels (internal/docs/ui_labels_test.go):
every `controls` entry must exist as a literal in templates/*.templ
(or web/src for source: ts). Renames fail make test. Entry text is the
canonical spelling cited across the docs. -->
# UI controls glossary
The canonical spellings for controls cited across the UI documentation. Docs
quote the label exactly as this page lists it — one entry per control; two
spellings of one control is a defect. The `controls` block is machine-read:
when a doc first names a control, add its entry in the same commit; when a
rename lands, update the entry in the same commit and the gate goes green
again.
Entry fields:
- `label` — the exact rendered text users see
- `where` — semantic path (page → section/card), never spatial; see the
[writing standard](writing-ui-docs.md)
- `does` — what interacting with it produces (the outcome)
- `source` — optional; default `templ` (`templates/*.templ`); `ts` searches `web/src`
- `also` — optional, pipe-separated extra literal fragments to search when
the visible label is Alpine-composed rather than static (OR-checked with `label`)
- `note` — optional clarifier
Form-field labels inside a single modal (Name, Description…) are cited
inline in docs and not tracked here; this list carries the controls people
navigate by.
## Collections
```controls
label: New Collection
where: Collections page
does: Opens the create-collection modal.
note: the empty page offers the same action as "Create Your First Collection"
label: Create Your First Collection
where: Collections page, when no collections exist
does: Opens the create-collection modal.
label: Create Collection
where: create-collection modal → submit
does: Saves the new collection and returns to the Collections page.
label: Update Collection
where: edit-collection modal → submit
does: Saves your changes to the collection.
label: Static
where: create-collection modal → Type
does: Chooses a hand-picked collection — you add and remove items yourself.
label: ⚡ Smart
where: create-collection modal → Type; also shown as a badge on smart collections page and detail
does: Chooses a rules-matched collection — its membership stays current by itself.
label: Rules scope
where: create-collection modal, when Smart is chosen
does: Asks what the rules ask about — the two buttons below it.
also: 📚 Books|🎧 Podcast episodes
note: "Books" and "Podcast episodes" as the two scope buttons
label: Restore System
where: Collections page
does: Opens a pick-a-shelf modal to bring back a system collection you deleted.
label: Restore
where: Restore System Collection modal → submit
does: Brings the selected system collection back.
label: Edit Rules
where: collection detail page
does: Opens the smart-collection rules editor for this collection.
label: Filter tree
where: smart-collection rules editor
does: The area where the group and rules are built.
note: read/write rules — the how is the filter guide
label: Apply to Collection
where: smart-collection rules editor → save
does: Saves the filter tree and makes the collection match those rules from now on.
label: Add Items
where: collection detail page
does: Opens the item picker window.
label: Add Selected
where: item picker → footer
does: Adds everything checked in the picker to the collection.
label: Remove Selected
where: collection detail page
does: Opens a confirm dialog to remove the checked items from the collection.
label: Search within collection...
where: collection detail page → search box
does: Filters the displayed items — nothing is removed.
label: Play as Sequence
where: collection detail page (when it has a downloaded episode)
does: Plays the collection's episodes one after another in the listening order you pick.
label: Hide finished books (reading-queue mode)
where: edit-collection modal
does: Finished books disappear from the collection's shelf.
label: Show Up Next strip
where: edit-collection modal
does: Adds this collection's Up Next strip to the podcast page.
```
## Everywhere else
```controls
label: Filters
where: All Books / Podcasts page → toolbar
does: Opens the filter drawer for building and applying rules.
label: Register Device
where: Devices page
does: Submits a device registration request for the pending-approval flow.
label: Download All
where: Podcast page → episodes area
does: Queues every episode of the feed for download.
label: Download order
where: Podcast page → feed settings
does: Sets the ordering used when feed episodes are downloaded.
label: Match ALL / Group ANY header
where: filter drawer → group header (also smart collections → Rules and dashboard sections)
does: Sets how the group combines its rules; clicking the header flips ALL/ANY.
also: ? 'Match' : 'Group'|? 'ANY' : 'ALL'
note: label is Alpine-composed — 'Match' for the whole builder, 'Group' for nested; docs write it as MATCH ALL / MATCH ANY
label: Reset to Scanned
where: book detail → edit modal → scan information area
does: Restores scanner-extracted metadata over manual edits.
label: Auto-Scan
where: Settings page → Scanning section
does: Enables automatic periodic library scanning on the interval below it.
label: Scan Interval (seconds)
where: Settings page → Scanning section
does: Sets the seconds between automatic scans.
label: Resume rewind
where: audio player → player settings; profile settings
does: Rewinds playback a configured few seconds when you resume.
also: Resume Rewind
note: profile page spells it capitalized
label: Reading Direction
where: book detail → edit modal
does: Chooses the direction pages advance (publisher convention — e.g. manga).
label: Scan Info
where: book detail page → info block
does: Displays scanner-supplied notes attached to the item.
also: Scan Information
note: the editable form label in the modal reads "Scan Information"
```
+117
View File
@@ -0,0 +1,117 @@
# 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.