From 7b8115f623270c5eac9c0ffb4f0ed8bce7a8f7d7 Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Fri, 5 Jun 2026 12:33:00 +0300 Subject: [PATCH] Add design council skill and interaction-and-feedback contract --- .claude/skills/council/SKILL.md | 80 ++++++++ AGENTS.md | 6 +- CLAUDE.md | 13 +- .../10-interaction-and-feedback.md | 180 ++++++++++++++++++ docs/spec/10-non-functional.md | 2 + 5 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 .claude/skills/council/SKILL.md create mode 100644 docs/architecture/10-interaction-and-feedback.md diff --git a/.claude/skills/council/SKILL.md b/.claude/skills/council/SKILL.md new file mode 100644 index 0000000..943375a --- /dev/null +++ b/.claude/skills/council/SKILL.md @@ -0,0 +1,80 @@ +--- +name: council +description: Consult the design council — the external interaction, content, and accessibility canon (IBM Carbon, GOV.UK Design System, WAI-ARIA APG, Nielsen Norman) — before committing a user-facing design decision. Auto-invoke when writing user-facing error / empty-state / notification copy, or when designing the keyboard / focus / ARIA behavior of an interactive widget (modal, menu, listbox, toast, splitter, disclosure). Also invokable on demand for any interaction, content, accessibility, or usability decision. +disable-model-invocation: false +--- + +# Design Council + +A standing panel of external design authorities, consulted the way we already consult +Carbon: **local corpora, grepped — not WebFetched.** The council **advises**; Astrolabe's +own contract **decides**. When a source conflicts with `SOUL.md`, +`docs/architecture/09-visual-design.md` (visual contract), or +`docs/architecture/10-interaction-and-feedback.md` (interaction contract), **our contract +wins** — note the divergence and move on. + +## Two modes + +- **Pointwise (now).** Consult on a single decision in front of you, via the routing table + below. Cheap, precise, fits the editing flow — this is the default, and what the + auto-triggers fire. +- **Audit (future).** Periodically sweep the **whole codebase** against the roster — every + interactive widget vs. APG, every user-facing string vs. GOV.UK/Carbon content rules, + every data surface vs. the loading/empty/error triad, every latency path vs. NN/g budgets + — and emit a prioritized gap list into `docs/architecture/10` + `// TODO`s. This is a + fan-out review (closer in shape to `/alignment`, but graded against external canon), best + run at **milestone boundaries**, not per change — and much of it is milestone-gated anyway + (you can't audit modals or datasets that don't exist yet). + +## How to convene (the routing rule) + +Pull the **minimal** relevant member(s) for the decision at hand — do **not** sweep all +four; that wastes tokens and dilutes the answer. Map the decision to its seat(s): + +| Decision in front of you | Primary seat → then | +| ---------------------------------------------------------- | ------------------------------------------------------ | +| Error / failure / empty-state / notification **copy** | **GOV.UK** → Carbon notifications → NN/g heuristic #9 | +| Keyboard / focus / ARIA roles of an **interactive widget** | **WAI-ARIA APG** → Carbon component | +| **Latency / feedback / loading / progress** budgets | **NN/g** response-time limits → Carbon loading pattern | +| **Forms / validation / destructive-action** flow | **GOV.UK** → Carbon | +| General **usability** gut-check on a flow | **NN/g** 10 heuristics | +| **Visual** styling (type, spacing, colour, component look) | **Carbon** + our `docs/architecture/09` | + +Then: read the cited file(s), extract the **specific** principle, and report it back with +a **citation (member + file path)** and a one-line "how it lands in Astrolabe." Don't +paraphrase the whole source — quote the rule that decides the question. + +## The roster + +All paths are under `/Users/oleh/code/reference/`. Treat clones as **inspiration, not +law** — they drift; the published guidance is the truth, the clone is the fast index. + +| Member | Path | Authoritative for | How to query | +| ------------------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **IBM Carbon** | `carbon-website/src/pages/` | Notification taxonomy, status levels, empty/loading states, content basics, data-viz styling | grep `.mdx` under `components/notification`, `patterns/{empty-states,loading,status-indicator}-pattern`, `guidelines/content` | +| **GOV.UK Design System** | `govuk-design-system/src/` | Error & validation messages, failure pages, forms, plain-language content, accessibility | `index.md` under `components/{error-message,error-summary,notification-banner}`, `patterns/{problem-with-the-service-pages,service-unavailable-pages,check-answers}`, `accessibility/` | +| **WAI-ARIA APG** | `aria-practices/content/patterns/` | Keyboard interaction, focus management, ARIA roles/states for widgets | `/-pattern.html` — e.g. `dialog-modal`, `alertdialog`, `alert`, `listbox`, `menu-button`, `disclosure`, `switch`, `tabs`, `tooltip`, `windowsplitter` | +| **Nielsen Norman (distilled)** | `principles/nielsen-norman.md` | 10 usability heuristics; response-time / feedback budgets (0.1s / 1s / 10s) | read directly — it is short and curated | + +## Close the loop + +The council exists so we **don't re-derive the same decision twice**. When a consultation +settles a _recurring_ question (not a one-off), capture the resolution into our own +contract — `docs/architecture/10-interaction-and-feedback.md` for interaction/feedback, +`09-visual-design.md` for visual — via the `/doc-update` skill. Upstream canon informs; +the downstream contract records. A code site that embeds such a rule should cite our +contract, not the external source. + +## Adding a seat + +1. If it's an open-source repo, shallow-clone it into `/Users/oleh/code/reference/` + (`git clone --depth 1 …`). If it's articles/blogs (not cloneable), **distill** the + stable parts into `reference/principles/.md` with attribution + links, like + the NN/g note. +2. Add one row to **the roster** and one routing entry above. +3. Keep it lean — a seat earns its place only if it's authoritative for a decision the + others don't cover well. + +Candidate future seats (not yet seated): **FT Visual Vocabulary / Datawrapper** (chart +choice — our domain), **Shopify Polaris** (UX-writing depth), **web.dev** (perceived +performance / PWA / offline UX). diff --git a/AGENTS.md b/AGENTS.md index 59d781c..ac2053f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,7 @@ src/ styles/ # Global CSS (tokens, base) docs/ ├── spec/ # Authoritative behavioral specification (00–10) — the WHAT -├── architecture/ # Architecture playbook (00–09) — the HOW (self-contained) +├── architecture/ # Architecture playbook (00–10) — the HOW (self-contained) │ └── visual-specimen.html # Standalone token sandbox + reusable-primitive catalog ├── IMPLEMENTATION-PLAN.md # Milestone sequence (M0–M6) └── WHY-A-SEPARATE-REBUILD.md @@ -115,6 +115,10 @@ Invoke with `/` (defined in `.claude/skills/`): `// TODO:` breadcrumbs at code sites for out-of-scope observations. - **`/doc-update`** — capture session-discovered knowledge gaps into the right doc layer (`docs/spec/` for behavior, `docs/architecture/` for patterns). +- **`/council`** — consult the design council (Carbon, GOV.UK, WAI-ARIA APG, Nielsen + Norman, cloned under `reference/`) before a user-facing interaction/content/a11y + decision. Auto-fires on error/empty-state copy and new interactive-widget keyboard/focus + work; on demand otherwise. It advises; `docs/architecture/09`+`10` decide. - **`/release`** — bump version, update the changelog, prepare a git tag. ### Versioning diff --git a/CLAUDE.md b/CLAUDE.md index 454652f..842bc58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,10 +8,15 @@ See @AGENTS.md for project overview, architecture rules, and the AI developer pr - **[docs/spec/](docs/spec/)** — authoritative behavioral specification (sections 00–10): the **what**. This is the contract; implement to it. - **[docs/architecture/](docs/architecture/00-overview.md)** — architecture playbook - (00–09): the **how** (state, persistence, modals, routing, rendering, inference, - relationships, vega-editor techniques, visual design). Self-contained — no external - repo needed. Companion: **[visual-specimen.html](docs/architecture/visual-specimen.html)** — - token sandbox + reusable-primitive catalog (open in a browser). + (00–10): the **how** (state, persistence, modals, routing, rendering, inference, + relationships, vega-editor techniques, visual design, interaction & feedback). + Self-contained — no external repo needed. Companion: + **[visual-specimen.html](docs/architecture/visual-specimen.html)** — token sandbox + + reusable-primitive catalog (open in a browser). +- **[`/council`](.claude/skills/council/SKILL.md)** — the design council: consult external + interaction/content/a11y canon (Carbon, GOV.UK, WAI-ARIA APG, Nielsen Norman, cloned + under `reference/`) before a user-facing decision. It advises; our contract (architecture + 09/10) decides. Resolutions are recorded back into the contract. - **[docs/IMPLEMENTATION-PLAN.md](docs/IMPLEMENTATION-PLAN.md)** — incremental milestone plan (M0–M6), MVP boundary, per-milestone tests + manual checks, and an architecture reference index. diff --git a/docs/architecture/10-interaction-and-feedback.md b/docs/architecture/10-interaction-and-feedback.md new file mode 100644 index 0000000..7a9defe --- /dev/null +++ b/docs/architecture/10-interaction-and-feedback.md @@ -0,0 +1,180 @@ +# 10 · Interaction & Feedback + +> **Status:** interaction contract. Where [09 · Visual Design](09-visual-design.md) is the +> _visual_ contract (how the app **looks**), this is the _interaction_ contract (how the +> app **behaves while the user works in it**). It is the HOW for the cross-cutting, +> non-feature-specific behavior that spec [§10 Non-Functional](../spec/10-non-functional.md) +> mandates as the WHAT. + +These rules already live, scattered, as one-off comments across the codebase (the +confirm-dialog's "transactional-modal rule," the toaster's "non-blocking outcomes are +toasts," the preview's "empty is not an error"). This document consolidates them into one +referenceable contract so a new feature **checks the rule instead of re-deriving it** — +and diverges only on purpose. + +**Upstream sources.** These principles are drawn from the design council (`/council`): +IBM Carbon, the **GOV.UK Design System**, the **WAI-ARIA Authoring Practices Guide**, and +**Nielsen Norman Group**. The council advises; this contract decides. When you face a new +interaction decision this doc doesn't cover, consult the council, then record the +resolution back here. + +--- + +## 1. The feedback-channel decision table + +Astrolabe has four distinct ways to tell the user something. They are **not** +interchangeable; picking the wrong one is the most common interaction bug. Choose by the +nature of the message, not by convenience. + +| Channel | Use when | Blocks? | Dismissal | Implemented by | +| -------------------- | --------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------- | -------------------------------------------------------- | +| **Confirm dialog** | A **destructive or irreversible** action needs explicit consent (delete, revert, reset) | Yes — modal | User must choose; Escape/Cancel = no; backdrop click does **not** dismiss | `ConfirmStore` + `ConfirmDialog` | +| **Toast** | A **non-blocking outcome** happened the user should know about (save failed, published, imported) | No | Auto for success/info; persists for error/warning; always a close button | `NotificationStore` + `Toaster` | +| **Inline error** | A problem is **tied to a specific surface** and recovers in place (invalid spec → editor + preview) | No | Clears automatically when the cause is fixed | `PreviewStore`, surfaced in `SpecEditor` + `LivePreview` | +| **Status indicator** | **Passive, ambient** state worth glancing at (draft vs. published, storage usage) | No | N/A — it just reflects state | library draft dot; storage monitor (later) | + +**Rules.** + +- **One blocking question at a time.** Confirm dialogs and feature modals are mutually + exclusive (the modal coordinator enforces this); a confirm may layer _over_ a modal + (discard-changes prompt), nothing else stacks. +- **Match disruptiveness to urgency** (Carbon). A toast interrupts less than a dialog; + don't use a dialog for something a toast can carry, and don't bury a + consent-for-destruction in a toast. +- **Errors persist; success fades.** An error/warning toast waits for the user (a critical + message must not vanish on a timer); success/info auto-dismiss (~6s). +- **The same failure can light up two channels.** An unrenderable spec shows the _same_ + message inline in both the editor (§03E) and the preview (§04) — one producer + (`PreviewStore`), two subscribers. That's intentional, not duplication. + +## 2. Latency & feedback budgets + +From NN/g's response-time limits (`reference/principles/nielsen-norman.md`). These are not +aspirations — they're the basis of the render pipeline's shape. + +| Budget | Feels like | Owed feedback | Astrolabe surfaces | +| ---------- | --------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| **≤ 0.1s** | Instantaneous | None beyond showing the result | Keystrokes into the buffer, hovers, toggles, selection | +| **≤ 1s** | Uninterrupted thought | None needed, but direct-manipulation feel is lost | A typical render after the debounce; opening a modal; switching snippets | +| **> 1s** | Noticeably waiting | **Must not block input**; show a busy indication | A heavy spec / large-dataset render | +| **> 10s** | Attention lost | **Progress indicator + stay cancellable**; let the user work elsewhere | (guard for large M3+ dataset work) | + +**Rules.** + +- **Input is sacred.** Typing and navigation never block on rendering or persistence (spec + §10). The render pipeline is debounced (`RENDER_DEBOUNCE_MS`), runs async, and uses a + **generation token** so a slow render can't overwrite a newer one. +- **Auto-save is cheap and silent** (`AUTOSAVE_DEBOUNCE_MS`) — it never stalls typing and + produces no toast on success (only on failure). +- **A busy indication may overlay the preview but must not freeze the editor** (spec §10). + When a render _might_ exceed ~1s, owe a non-blocking indicator; never a frozen UI. + +## 3. The non-happy-path triad + +Every surface that shows data owes **three** designed states, not one. The empty and error +states are part of the feature, not an afterthought (Carbon empty-states, GOV.UK). + +- **Loading** — when data isn't ready yet. Prefer a skeleton/placeholder over a spinner for + structural loads; show it only for a beat. +- **Empty** — when there is legitimately nothing. **Empty ≠ error**: a blank editor renders + a clean, calm empty preview, never an error (`LivePreview` treats blank as success). An + empty list says what would be here and how to add it. +- **Error** — when something went wrong. Split by **who can fix it**: + - **User-fixable** → state the consequence and the **next step** ("Storage full — delete + snippets to free space, then edit again"). A user action is mandatory (Carbon/GOV.UK). + - **Not user-fixable** → explain plainly **and** attach a reportable **diagnostic** (the + operation + underlying error) so it can be traced. See `services/storage-errors.ts` — + this split is the module's whole reason for being. +- **Wording** (NN/g #9, GOV.UK, Carbon content): plain language, **no error codes in the + user-facing line**, second person, name what stopped in the title, one or two sentences + in the body, never flippant. + +## 4. Recovery & data-safety contract + +The user must never silently lose work, and must always have a way back (NN/g #3 "user +control and freedom," #5 "error prevention"; spec §10 Reliability). + +- **No silent data loss.** Edits auto-save as a **draft**; a known-good **published** + version is always preserved separately (§03D). A failed persist **surfaces as a toast**, + never a swallowed promise (`orchestration/persistence.ts`). +- **A marked exit from every committed change.** Revert restores the published spec; + Escape closes modals; delete/revert/reset require confirmation first. +- **Resilient rendering recovers on its own.** An invalid spec shows a readable error and + **auto-recovers when fixed** — it never leaves the app wedged (§04). +- **Non-destructive import.** Import merges, never overwrites; on failure the existing + workspace is left exactly as it was (§08). + +## 5. Keyboard & focus contract + +Astrolabe is keyboard-operable end to end (spec §10 Accessibility). The interaction +patterns follow **WAI-ARIA APG** (`reference/aria-practices/content/patterns/`); the wiring +lives in [04 · Routing & Global Events](04-routing-and-events.md). + +- **One global router** owns document-level `keydown`/`paste`; components never attach + their own `window` listeners. +- **Escape is an ordered priority chain** that returns on first consumption (blocking + message → modal → menu → selection) and is checked **before** the interactive-context + gate, so it dismisses a modal even while the editor has focus. All _other_ shortcuts are + gated by `isInInteractiveContext()` so they never fire mid-typing. +- **Shortcuts claim their key** with `preventDefault()` so they override the browser + default (⌘S doesn't "save page," ⌘K doesn't focus the URL bar). +- **Focus moves into an overlay on open and returns to the trigger on close**; focus is + **trapped** within it (`useFocusTrap`). Destructive confirms focus Cancel first. +- **Match the APG pattern** for any new interactive widget — a modal is `dialog-modal`, a + destructive confirm is `alertdialog`, a resize handle is `windowsplitter`, a toast region + uses `alert`/`status` roles by severity. Don't invent keyboard models; adopt the + documented one. + +**Resolved — pane resize handle (window splitter).** A `ResizeHandle` is a focusable +`role="separator"` that **reports the controlled pane's size**, per APG → Window Splitter: + +- `aria-valuenow` on a **0–100 scale** (0 = pane at its minimum, 100 = at its maximum), + with `aria-valuemin=0`, `aria-valuemax=100`, and `aria-valuetext="%"` for a clean + announcement. The 0–100 normalization (APG's "typical") beats raw pixels: it's stable and + announces as a percentage. The math is the pure `sideWidthValue()` in `PanesStore` (so it + is unit-tested, not trapped in the component); the live value needs the container width, + observed via `ResizeObserver` so it tracks window resizes, not just drags. +- `aria-controls` points at the pane it sizes (`pane-library` / `pane-preview`). +- **Keyboard**: ←/→ nudge; **Home** → smallest pane size, **End** → largest. **Enter-to-collapse + is deferred to M6** with the pane-visibility / toggle strip — collapsing needs a + hidden-pane state that doesn't exist yet, so we don't fake it. + +_(Consulted via `/council` → WAI-ARIA APG `windowsplitter`. This bullet is the contract; +cite it, not the APG file.)_ + +## 6. Motion & accessibility as default + +Not features to add later — the baseline every surface is built on. + +- **Reduced motion is honored globally.** Animations/transitions are neutralized under + `prefers-reduced-motion` (`styles/base.css`); never gate meaning on motion. +- **Colour is never the sole signal** (WCAG; Carbon status pattern). Pair it with a label, + icon, shape, or text — a red toast border also has a title and an `alert` role; the + draft dot has a `title`/`aria-label`. +- **Every control is labelled.** Icon-only buttons, toggles, and fields carry accessible + names so assistive tech can announce them. +- **Contrast holds in every theme.** A theme that can't meet legible contrast in part of + the UI is not complete (spec §10 / §07). + +--- + +## Do / Don't + +**Do** + +- Pick the feedback channel from §1's table by the _nature_ of the message. +- Treat loading/empty/error as three designed states for every data surface. +- Adopt the APG keyboard pattern for new widgets; route all global keys through the one + router. +- Consult `/council` when this contract is silent — then record the answer back here. + +**Don't** + +- Don't show a blocking dialog for something a toast can carry, or hide a + consent-for-destruction in a toast. +- Don't let a render or a save block typing. +- Don't treat "empty" as "error." +- Don't put error codes in the user-facing line — put diagnostics in the detail + disclosure, the next step in the message. +- Don't invent a keyboard model, attach ad-hoc `window` listeners, or gate Escape behind + the typing check. diff --git a/docs/spec/10-non-functional.md b/docs/spec/10-non-functional.md index f98ac55..9c41b57 100644 --- a/docs/spec/10-non-functional.md +++ b/docs/spec/10-non-functional.md @@ -2,6 +2,8 @@ This section defines quality attributes the rebuild must satisfy — performance, accessibility, reliability, privacy, and platform posture — independent of any single feature. Feature behavior lives in the other sections; this one constrains _how well_ that behavior must work. +> The cross-cutting **interaction patterns** that satisfy these attributes — the feedback-channel decision table, latency budgets, the loading/empty/error triad, the recovery and keyboard/focus contracts — are specified in [Architecture 10 · Interaction & Feedback](../architecture/10-interaction-and-feedback.md). This section is the _what_ (the quality bar); that doc is the _how_ (the patterns that meet it). + ## Platform & Form Factor - **Target**: modern evergreen desktop browsers. The app is a single-page application that loads once and then runs locally.