mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Codify the spec-vs-playbook boundary: cite behavior, don't restate it
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user