Codify the spec-vs-playbook boundary: cite behavior, don't restate it

This commit is contained in:
2026-06-09 15:02:12 +03:00
parent 81f563a3e7
commit 29b0d1adb0
2 changed files with 74 additions and 46 deletions
@@ -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