diff --git a/docs/architecture/00-overview.md b/docs/architecture/00-overview.md index e19f058..30d33e9 100644 --- a/docs/architecture/00-overview.md +++ b/docs/architecture/00-overview.md @@ -7,7 +7,26 @@ > > They are the architectural counterpart to [`docs/spec/`](../spec/): the **spec** says > _what the app does_ (behavior, acceptance points); this **playbook** says _how we build -> it_ (state, persistence, modals, routing, rendering, inference, relationships). +> it_ (state, persistence, modals, routing, rendering, inference, relationships, and the +> visual + interaction language). + +## Spec vs playbook: cite, don't restate + +The two documents overlap most in §09 (visual) and §10 (interaction), which necessarily talk +about user-facing widgets. At that overlap, one rule keeps them from drifting: + +- The **spec owns product behavior** — what features exist, what surfaces appear when, what + the data does. The **playbook owns the _how_** — state shape, persistence, ARIA roles, + keyboard models, focus, tokens, motion. +- **For product behavior, the spec is the source: cite it (`spec §NN`), don't restate it, + and never contradict it.** A playbook bullet may _name_ the behavior in one clause and cite + the spec, then spend its words on the _how_ (the role, the keys, the focus move) and the + _why_ (the council/canon citation). When a playbook bullet and the spec disagree, the + **spec wins** and the bullet is the bug. + +_(This bit us once: arch §10 described "keep the library list, title-only" while spec §02 +said "replace the entire list with the onboarding canvas" — the two halves were authored from +opposite mental models. The restatement, not the separation, was the leak.)_ ## How to use this playbook @@ -19,17 +38,18 @@ ## The documents -| # | Doc | Covers | -| --- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 01 | [State & Stores](01-state-and-stores.md) | Zustand stores; one source of truth; selector derivations; central `useAppStore` vs per-feature stores; testable action functions; debounced auto-save. | -| 02 | [Persistence](02-persistence.md) | The infrastructure-adapter boundary; promise-wrapped IndexedDB wrapper; lazy data loading; per-record schema versioning + migration; localStorage prefs with fallback; storage tiers + quota monitoring. | -| 03 | [Modal System](03-modal-system.md) | Registry + coordinator + shell; one modal at a time; unsaved-change detection via snapshot; focus trap; backdrop/Escape/close dismissal. | -| 04 | [Routing & Events](04-routing-and-events.md) | URL hash as view-state (restore/sync, Back/Forward); global keyboard routing; Escape priority chain; the single-source `isInInteractiveContext()` helper (Monaco-aware). | -| 05 | [Rendering, Theming & Preview](05-rendering-theming-preview.md) | vega-embed integration (`actions:false`, `view.finalize()`); field-name escaping; theme→config mapping; debounced non-blocking renderer; resilient error display. | -| 06 | [Type Inference & Profiling](06-type-inference.md) | Pure, portable column-type inference (number/text/date/boolean) and the dataset profile shape. | -| 07 | [Naming & Relationships](07-naming-and-relationships.md) | Unique-name enforcement + import auto-suffix; the bidirectional snippet↔dataset name link; rename propagation into specs. | -| 08 | [vega/editor Techniques](08-vega-editor-techniques.md) | Reference brief: borrowable Monaco-schema wiring, vega-embed lifecycle, two-tier validation, and data-flow/debounce techniques distilled from the official Vega-Lite editor — plus where we do better. | -| 09 | [Visual Design Language](09-visual-design.md) | The _visual_ contract: principles inspired by IBM/Carbon, deliberate divergences (square chrome, free color/theming), the token system (Plex type, 8px spacing, role-based color, motion), component conventions, and where to mine the Carbon/IBM source repos for more. Companion: [`visual-specimen.html`](visual-specimen.html). | +| # | Doc | Covers | +| --- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 01 | [State & Stores](01-state-and-stores.md) | Zustand stores; one source of truth; selector derivations; central `useAppStore` vs per-feature stores; testable action functions; debounced auto-save. | +| 02 | [Persistence](02-persistence.md) | The infrastructure-adapter boundary; promise-wrapped IndexedDB wrapper; lazy data loading; per-record schema versioning + migration; localStorage prefs with fallback; storage tiers + quota monitoring. | +| 03 | [Modal System](03-modal-system.md) | Registry + coordinator + shell; one modal at a time; unsaved-change detection via snapshot; focus trap; backdrop/Escape/close dismissal. | +| 04 | [Routing & Events](04-routing-and-events.md) | URL hash as view-state (restore/sync, Back/Forward); global keyboard routing; Escape priority chain; the single-source `isInInteractiveContext()` helper (Monaco-aware). | +| 05 | [Rendering, Theming & Preview](05-rendering-theming-preview.md) | vega-embed integration (`actions:false`, `view.finalize()`); field-name escaping; theme→config mapping; debounced non-blocking renderer; resilient error display. | +| 06 | [Type Inference & Profiling](06-type-inference.md) | Pure, portable column-type inference (number/text/date/boolean) and the dataset profile shape. | +| 07 | [Naming & Relationships](07-naming-and-relationships.md) | Unique-name enforcement + import auto-suffix; the bidirectional snippet↔dataset name link; rename propagation into specs. | +| 08 | [vega/editor Techniques](08-vega-editor-techniques.md) | Reference brief: borrowable Monaco-schema wiring, vega-embed lifecycle, two-tier validation, and data-flow/debounce techniques distilled from the official Vega-Lite editor — plus where we do better. | +| 09 | [Visual Design Language](09-visual-design.md) | The _visual_ contract: principles inspired by IBM/Carbon, deliberate divergences (square chrome, free color/theming), the token system (Plex type, 8px spacing, role-based color, motion), component conventions, and where to mine the Carbon/IBM source repos for more. Companion: [`visual-specimen.html`](visual-specimen.html). | +| 10 | [Interaction & Feedback](10-interaction-and-feedback.md) | The _interaction_ contract: the feedback-channel decision table, latency/feedback budgets, the non-happy-path triad, the recovery & data-safety contract, the keyboard/focus contract, and the resolved widget patterns (window splitter, toolbar, segmented controls, selectable lists, search, sort, empty states, modals). Cites `spec/` for behavior; owns the _how_. | ## The non-negotiable layering (every doc assumes this) diff --git a/docs/architecture/10-interaction-and-feedback.md b/docs/architecture/10-interaction-and-feedback.md index 368aaf7..64ced98 100644 --- a/docs/architecture/10-interaction-and-feedback.md +++ b/docs/architecture/10-interaction-and-feedback.md @@ -18,6 +18,14 @@ IBM Carbon, the **GOV.UK Design System**, the **WAI-ARIA Authoring Practices Gui interaction decision this doc doesn't cover, consult the council, then record the resolution back here. +**Cite the spec for behavior; own the _how_.** Each `Resolved —` bullet below is the contract +for an interaction's _mechanics_ — the ARIA role, the keyboard model, the focus move — and +records _why_ (the council citation). It is **not** the source for **product behavior**: what +a surface shows, when it appears, what the data does. That lives in `docs/spec/`; a bullet +**names** the behavior in a clause, cites `(spec §NN)`, and never restates or overrides it. On +any disagreement, the spec wins and the bullet is the bug (see `00-overview` → "cite, don't +restate"). + --- ## 1. The feedback-channel decision table @@ -224,9 +232,11 @@ forbids interactive children in a listbox. Mark the active row with `aria-curren **only on that row** (don't emit `aria-current="false"` everywhere). Arrow-key roving _between_ rows is a later enhancement; button-per-row tab stops are the acceptable baseline. -**Resolved — snippet-list status indicator.** The library row flags only the _exceptional_ -state: a single **accent dot** when a snippet has unpublished draft changes; a fully-published -snippet shows **no dot** (presence = draft, absence = published). We do **not** give "published" +**Resolved — snippet-list status indicator.** Spec §02 owns the behavior — the row +distinguishes a snippet with **unpublished draft changes** from a **fully-published** one; this +bullet owns the _how_. The row flags only the _exceptional_ state: a single **accent dot** for +the unpublished case; a fully-published snippet shows **no dot** (presence = draft, absence = +published). We do **not** give "published" its own glyph — GOV.UK's Tag guidance notes one status suffices when absence is self-evident, and Carbon's status-indicator pattern says not to highlight what isn't significant. Meaning never rests on **hue** (WCAG 1.4.1): it rides on presence/absence **plus** the dot's accessible @@ -266,37 +276,35 @@ _(Consulted via /council → WAI-ARIA APG disclosure + menu-button, NN/g #6. Thi contract; cite it, not the source.)_ **Resolved — the empty-library onboarding canvas (and the list's single empty state).** The -snippet list owes **one empty state**: **no search matches** — "No snippets match your search" - -- a hint to try a different term. There is **no separate "empty library" list state**, because - a genuinely empty library never shows the list at all: rather than seeding a placeholder - snippet (the old behavior), the workspace replaces the **entire pane chrome — toggle strip, - library list, editor, and preview — with a full-width onboarding canvas** (spec §02 → - First-Run & Empty Workspace): with no snippets, the library controls and pane toggles have - nothing to act on. (So the list's own empty copy is reached only mid-search, never on a cold - start.) The canvas is a welcome, a primary "Create your first - snippet", and a gallery of example snippets; leaving it (creating the first snippet) lays the - panes out at a default **25·25·50** split via `PanesStore.applyOnboardingSplit` so the first - chart opens with a generous preview. Each card - **renders live through the shared `chart-renderer`** (no parallel embed path; each card owns - its `RenderHandle` and finalizes on unmount — the per-card nodes are independent, so they - don't touch `LivePreview`'s single-host serialization), and adds as an ordinary snippet. - Empty stays calm and positive, never an error (NN/g aesthetic-and-minimalist; §3 empty ≠ - error). Council resolutions for the canvas: (1) **each card preview is decorative** — - `aria-hidden`, skipped by screen readers (Carbon empty-states a11y / WCAG decorative - images); the card **name + one-line description + a uniquely-labelled `Add` button** ("Add - Bar chart", APG button) carry the meaning, so AT users reach no dead end. (2) The canvas is - the **single empty surface** — because it replaces the library list outright, there is no - competing "No snippets yet" status elsewhere to keep in sync. Its heading **owns the app - identity** ("Welcome to Astrolabe") and it carries the only Create nudge, so the empty-state - message lives in exactly one place (Carbon "keep words to a minimum"; no duplication). - (3) The **primary action dominates** (accent "Create your first snippet" first; the example - gallery is framed as a secondary "Or start from an example") — Carbon sanctions starter - content as an in-depth first-use empty state only when one action stays primary. (4) - **Domain vocabulary stands**: "Vega-Lite", "JSON", "snippet" are kept despite Carbon's - avoid-jargon rule, because SOUL #2 (Vega-Lite Native) makes them the user's real language — - a deliberate divergence. _(Consulted via /council → Carbon empty-state + content, GOV.UK - headings, WAI-ARIA APG button, NN/g. This bullet is the contract; cite it, not the source.)_ +snippet list owes **one empty state** — **no search matches** ("No snippets match your search", +with a hint to try a different term). There is **no separate "empty library" list state**, +because a genuinely empty library never shows the list at all: rather than seeding a placeholder +snippet (the old behavior), the workspace replaces the **entire pane chrome — toggle strip, +library list, editor, and preview — with a full-width onboarding canvas** (spec §02 → +First-Run & Empty Workspace): with no snippets, the library controls and pane toggles have +nothing to act on. (So the list's own empty copy is reached only mid-search, never on a cold +start.) The canvas is a welcome, a primary "Create your first snippet", and a gallery of +example snippets; leaving it (creating the first snippet) lays the panes out at a default +**25·25·50** split via `PanesStore.applyOnboardingSplit` so the first chart opens with a +generous preview. Each card **renders live through the shared `chart-renderer`** (no parallel +embed path; each card owns its `RenderHandle` and finalizes on unmount — the per-card nodes are +independent, so they don't touch `LivePreview`'s single-host serialization), and adds as an +ordinary snippet. Empty stays calm and positive, never an error (NN/g aesthetic-and-minimalist; +§3 empty ≠ error). Council resolutions for the canvas: (1) **each card preview is decorative** — +`aria-hidden`, skipped by screen readers (Carbon empty-states a11y / WCAG decorative images); +the card **name + one-line description + a uniquely-labelled `Add` button** ("Add Bar chart", +APG button) carry the meaning, so AT users reach no dead end. (2) The canvas is the **single +empty surface** — because it replaces the library list outright, there is no competing "No +snippets yet" status elsewhere to keep in sync. Its heading **owns the app identity** ("Welcome +to Astrolabe") and it carries the only Create nudge, so the empty-state message lives in exactly +one place (Carbon "keep words to a minimum"; no duplication). (3) The **primary action +dominates** (accent "Create your first snippet" first; the example gallery is framed as a +secondary "Or start from an example") — Carbon sanctions starter content as an in-depth +first-use empty state only when one action stays primary. (4) **Domain vocabulary stands**: +"Vega-Lite", "JSON", "snippet" are kept despite Carbon's avoid-jargon rule, because SOUL #2 +(Vega-Lite Native) makes them the user's real language — a deliberate divergence. _(Consulted +via /council → Carbon empty-state + content, GOV.UK headings, WAI-ARIA APG button, NN/g. This +bullet is the contract; cite it, not the source.)_ **Resolved — one live region per shared message.** When the same error feeds two surfaces (the §1 "one producer, two subscribers" case — render errors via `PreviewStore`), exactly