Add design council skill and interaction-and-feedback contract

This commit is contained in:
2026-06-05 12:33:00 +03:00
parent 3839f92f1d
commit 7b8115f623
5 changed files with 276 additions and 5 deletions
+80
View File
@@ -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>/<pattern>-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/<source>.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).
+5 -1
View File
@@ -62,7 +62,7 @@ src/
styles/ # Global CSS (tokens, base)
docs/
├── spec/ # Authoritative behavioral specification (0010) — the WHAT
├── architecture/ # Architecture playbook (0009) — the HOW (self-contained)
├── architecture/ # Architecture playbook (0010) — the HOW (self-contained)
│ └── visual-specimen.html # Standalone token sandbox + reusable-primitive catalog
├── IMPLEMENTATION-PLAN.md # Milestone sequence (M0M6)
└── WHY-A-SEPARATE-REBUILD.md
@@ -115,6 +115,10 @@ Invoke with `/<name>` (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
+9 -4
View File
@@ -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 0010):
the **what**. This is the contract; implement to it.
- **[docs/architecture/](docs/architecture/00-overview.md)** — architecture playbook
(0009): 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).
(0010): 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 (M0M6), MVP boundary, per-milestone tests + manual checks, and an architecture
reference index.
@@ -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 **0100 scale** (0 = pane at its minimum, 100 = at its maximum),
with `aria-valuemin=0`, `aria-valuemax=100`, and `aria-valuetext="<n>%"` for a clean
announcement. The 0100 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.
+2
View File
@@ -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.