feat(composer): expand ui-system from placeholder to full interview

ui-system skill is no longer a placeholder. Full design-system and
interaction-design interviews with branching, defaults, batched
question clusters, and an 'accept default / skip' protocol so users
move through fast when they have taste and patient when they don't.

Design system interview covers: visual posture, color (with WCAG
contrast verification before writing), typography, spacing, radii,
shadows, density, visual motion, components, theming, and
accessibility tokens (focus ring, hit targets).

Interaction design interview covers: input model, state-surface
declarations per action class, motion posture and triggers,
breakpoints, i18n, four state postures (empty/loading/error/offline),
time formatting, keyboard policy, optimistic-update strategy with
rollback and conflict resolution, gestures, focus management,
notification patterns, selection, and drag-and-drop.

Reference banks for "make it look like Linear/Notion/Stripe/etc."
translate well-known products into starting tokens — user still
confirms each value, no silent invention.

Templates expanded to hold the full output:
- design-system/SKILL.md grew sections for accessibility tokens,
  density, dark-mode mapping, contrast verification, theming variants
- design-system/tokens.ts now exports color (light + dark), typography
  (family/size/weight/line-height/letter-spacing), spacing + density-
  aware padding tokens, radii, shadows, motion (durations + easings),
  z-index scale, breakpoints, hit-target minimums
- design-system/components.md gained anatomy/state/a11y coverage for
  10 primitives plus an "adding a new component" workflow and a
  "patterns to avoid" list (nested cards, color-only meaning, etc.)
- interaction-design/SKILL.md gained sections for optimistic updates,
  conflict resolution, gestures, focus management, notification
  patterns, selection/multi-select, and drag-and-drop

Hard rules: never invent on the user's behalf; <TBD> instead. Verify
contrast before writing color tokens. Always declare prefers-reduced-
motion as honored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
movq
2026-04-29 21:10:36 -05:00
parent 594fccc80a
commit 92f005895c
5 changed files with 947 additions and 263 deletions

View File

@@ -1,35 +1,15 @@
--- ---
name: ui-system name: ui-system
description: Interview the user and produce two project-local skills — a design-system skill and an interaction-design skill — that auto-load whenever UI work is in progress. Output goes to .claude/skills/<slug>-design-system/ and .claude/skills/<slug>-interaction-design/, NOT docs/planning/. The skills load themselves into context when anyone codes UI (mock or real); a plain reference doc would not. Currently a placeholder skill that captures the minimum needed for downstream phases to proceed; planned for a fuller specification pass. Use when the composer skill reaches phase 4 or 5, or when the user asks to "set up the design system" or "specify interaction patterns." description: Interview the user and produce two project-local skills — a design-system skill and an interaction-design skill — that auto-load whenever UI work is in progress. Output goes to .claude/skills/<slug>-design-system/ and .claude/skills/<slug>-interaction-design/, NOT docs/planning/. The skills load themselves into context when anyone codes UI (mock or real); a plain reference doc would not. Use when the composer skill reaches phase 4 or 5, or when the user asks to "set up the design system", "specify interaction patterns", or "redo the UI system".
--- ---
# UI system (placeholder) # UI system
> **Status: PLACEHOLDER.** This skill currently captures the minimum You run a focused interview that produces two project-local skills:
> design-system and interaction-design content needed for the mocks
> phase to proceed. A fuller specification pass is planned. When the
> user returns to flesh this out, expand the interview sections below
> and the SKILL.md / sidecar templates.
## Why this is a skill, not a doc
A reference document at `docs/planning/design-system.md` gets read once
and then drifts out of context as the conversation grows. UI work that
happens later — generating a mock, writing a component, picking a color —
won't have it loaded.
A **skill** with a description that triggers on UI work is loaded
automatically every time the trigger matches. That's the whole point of
the skill mechanism. The design system and interaction design must be
skills.
## What this skill produces
Two project-local skills, plus their sidecar data files:
``` ```
.claude/skills/<slug>-design-system/ .claude/skills/<slug>-design-system/
├── SKILL.md the skill itself; auto-loads on UI work ├── SKILL.md ← canonical visual reference; auto-loads on UI work
├── tokens.ts ← machine-readable color / type / spacing / motion tokens ├── tokens.ts ← machine-readable color / type / spacing / motion tokens
└── components.md ← component vocabulary with anatomy └── components.md ← component vocabulary with anatomy
@@ -37,101 +17,404 @@ Two project-local skills, plus their sidecar data files:
└── SKILL.md ← interaction patterns, state surfaces, input modes, i18n └── SKILL.md ← interaction patterns, state surfaces, input modes, i18n
``` ```
And two stub pointer files in `docs/planning/` so the planning index Plus pointer stubs in `docs/planning/{design-system,interaction-design}.md`
finds them: so the planning index finds them.
``` ## Why these are skills, not docs
docs/planning/design-system.md ← pointer; tells humans where the skill lives
docs/planning/interaction-design.md ← pointer A reference doc gets read once and drifts out of context as the
``` conversation grows. UI work later — generating a mock, writing a
component, picking a color — wouldn't have it loaded.
A skill with a UI-triggered description loads automatically every time
the trigger matches. The design system and interaction design must be
skills, full stop.
## Slug ## Slug
The skills need a project slug for their `name:` frontmatter, so they Resolve in order:
don't collide with skills from other projects on the same machine. 1. `package.json` `name` — slugify.
2. Git remote name (e.g., `git remote get-url origin` → repo name).
3. Directory name.
4. Ask.
Resolution order: Slug rule: lowercase kebab-case, ASCII only.
1. If `package.json` has a `name`, slugify it. `SourdoughTracker``sourdough-tracker``sourdough-tracker-design-system`.
2. Else if a git remote exists, slugify the repo name.
3. Else ask the user. Default to the directory name.
Slug rule: lowercase, hyphenated, kebab-case, ASCII only. Example: ## Interview shape
`SourdoughTracker``sourdough-tracker` → skill `sourdough-tracker-design-system`.
## design-system interview (placeholder pass) Two parts, **sequential**: design system first (visual), then
interaction design (behavioral). Don't interleave — they're different
conversations and the visual posture often informs interaction choices.
Ask the user, in order, with sensible defaults: Each phase:
1. State what's being decided and why it matters (one line).
2. Show the default if there is one.
3. Ask the question. Batch related questions when they cluster naturally.
4. Confirm the answer back as it'll appear in the artifact.
5. Move on.
1. **Existing material?** Brand assets, mood boards, screenshots, The user can answer **`default`** or **`skip`** at any phase:
"make it look like Linear/Notion/Stripe", or nothing. If material - `default` → accept the offered default and move on.
exists, ingest it before asking the rest. - `skip` → mark `<TBD: ...>` and move on; user can revisit.
2. **Color**: brand color(s), neutral scale (warm/cool/true), dark mode
(yes/no/system). Offer to generate accessible contrast pairs.
3. **Typography**: one family or two (display + body), size scale base
and ratio, line-height policy.
4. **Spacing**: scale base (4px / 8px), key spacing tokens.
5. **Radii**: tight / medium / pillowy.
6. **Shadows**: flat / subtle / generous.
7. **Visual motion**: default duration, default easing.
8. **Components**: confirm vocabulary against `ia.md`.
Mark unanswered with `<TBD: ...>` so the return pass has a worklist. Never invent on the user's behalf. `<TBD>` is a worklist, not a failure.
### Write phase — design-system ---
1. Write `.claude/skills/<slug>-design-system/SKILL.md` from ## Phase 0 — Material ingestion (silent)
`templates/design-system/SKILL.md`. Substitute the slug into the
`name:` field and the project name into the `description:`.
2. Write `.claude/skills/<slug>-design-system/tokens.ts` from
`templates/design-system/tokens.ts`. Fill in the values gathered in
the interview; `<TBD>` for unanswered.
3. Write `.claude/skills/<slug>-design-system/components.md` from
`templates/design-system/components.md`.
4. Write `docs/planning/design-system.md` from the pointer template —
one paragraph saying where the skill lives.
## interaction-design interview (placeholder pass) Before asking anything, scan for material the user already has. Read
without asking; surface findings before the first question.
Ask, in order: | Source | What to extract |
|---|---|
| `package.json` | name (slug), version, dependencies (UI framework hints) |
| `tailwind.config.{js,ts}` | existing tokens, theme extensions |
| `tokens.{ts,json,css}` anywhere in repo | existing token values |
| `*.css`, `*.scss` | brand colors mentioned, font families imported |
| `index.html`, `<head>` of any HTML | font loads, color-scheme meta |
| `assets/`, `public/`, `docs/` for logos/screenshots | brand colors via image inspection |
| `README.md`, `BRANDING.md`, `STYLE.md` | style notes the user wrote |
| Existing `docs/planning/concept.md` | platform hint (web/mobile/native) |
1. **Primary input**: touch / mouse / keyboard / mixed. Primary, supported, not supported. Then before the interview opens:
2. **State surfaces**: pick one pattern per action class.
3. **Motion**: minimal / restrained / expressive. `prefers-reduced-motion` always honored.
4. **Breakpoints**: count, primary design target.
5. **i18n**: locales planned, RTL, string-length budget.
6. **Empty/loading/error/offline**: one declared posture each.
7. **Time formatting**: relative threshold, absolute format.
8. **Keyboard**: global shortcuts, escape behavior.
### Write phase — interaction-design ```
Material I found and will use:
• <thing> — <how it'll inform the interview>
• <thing>
Not found (will ask): brand color, type family, motion posture.
```
1. Write `.claude/skills/<slug>-interaction-design/SKILL.md` from If the user has named a reference like *"make it look like Linear"* in
`templates/interaction-design/SKILL.md`. a previous Composer phase, surface it here and apply the matching
2. Write `docs/planning/interaction-design.md` pointer. preset (see "Reference banks" below).
---
# Part 1 — Design system
## D1 — Visual posture (one paragraph)
The north star sentence for visual decisions. Don't ask for paragraphs;
ask for adjectives and assemble.
```
Three or four words that describe how this should feel.
Examples: "quiet, dense, monochrome, one accent" / "playful,
generous, expressive type" / "clinical, neutral, accessibility-forward".
```
Compose into a single sentence; show; accept.
## D2 — Color
Batch as a single phase with sub-questions; tight loop.
**D2.1 Brand primary.** If material gave a color, propose it. Else:
```
Pick a brand primary. Either name a hex, name a color
("warm orange", "deep teal"), or name a reference ("Linear's purple",
"Stripe's indigo"). I'll generate the variants from there.
```
**D2.2 Brand secondary.** Optional. `default` = none.
**D2.3 Neutral tone.** Warm / cool / true. One question.
```
For grays, do they feel slightly tan (warm), slightly blue (cool),
or truly neutral (true)? Default: cool.
```
**D2.4 Generate the neutral scale.** Compute 50950 from the chosen tone
and brand contrast targets. Show the user; offer `default` or "redo with
warmer/cooler/more contrast".
**D2.5 Semantic colors.** Success / warning / error / info. Show defaults
that contrast against both light and dark backgrounds; user accepts or
overrides one at a time.
**D2.6 Dark mode.** yes / no / system-following. Default: system. If yes,
generate the dark token mapping (invert + adjust saturation per token
class) and show.
**D2.7 Accessibility check.** Compute contrast ratios between primary +
neutral pairs. Flag anything below 4.5:1 for body text or 3:1 for UI.
Don't silently accept failures — surface and confirm or adjust.
## D3 — Typography
**D3.1 One family or two?** Default: one.
**D3.2 Family choice.** If user has no preference, offer presets:
- *Modern sans*: Inter (free, broad weights)
- *Humanist*: Source Sans 3 / IBM Plex Sans
- *System stack*: native to OS (free, no load)
- *Editorial*: Inter + Fraunces (display)
- *Technical*: IBM Plex Sans + IBM Plex Mono
**D3.3 Scale base.** 14, 15, 16. Default: 16 (web), 17 (iOS native), 14 (compact dashboards).
**D3.4 Scale ratio.** 1.125 (subtle), 1.2, 1.25 (default), 1.333 (dramatic), 1.5 (display-heavy).
**D3.5 Line-height policy.**
- Display: 1.11.2 (tight)
- Body: 1.5 (normal)
- Long-form prose: 1.7 (relaxed)
Defaults usually fine; user can override one of three.
## D4 — Spacing
**D4.1 Base unit.** 4 (default — finer control) or 8 (rougher cadence; iOS native often).
**D4.2 Scale.** Use the standard 12-step (0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 12, 16, 24) — almost always accepted as `default`.
## D5 — Radii
**D5.1 Posture.** sharp (04) / tight (48) / medium (816) / pillowy (16+) / mixed.
**D5.2 Generate sm/md/lg/pill/full.** Show; accept or tweak one.
## D6 — Shadows & elevation
**D6.1 Posture.** flat (none) / subtle / generous.
**D6.2 Scale.** xs/sm/md/lg/xl. Generated from posture; show; accept.
## D7 — Density
**D7.1 Default density.** comfortable (default; web app) / cozy / compact (data-heavy dashboard).
This drives component padding tokens — important enough to ask, easy to forget otherwise.
## D8 — Visual motion
**D8.1 Default duration.** fast (150ms), default (250ms), slow (400ms). Default: default.
**D8.2 Default easing.** standard / accel / decel / custom. Default: standard.
Behavioral motion (when things animate) lives in interaction design.
## D9 — Components
Read the entity vocabulary from `ia.md`. Confirm the standard set
(Button, Input, Card, Surface) and ask for project-specific
additions found in IA.
For each project-specific component, ask: variants? states? anatomy?
Default to "standard variants only" if user is unsure — better to add
later than over-spec now.
## D10 — Theme variants (skip by default)
Multi-brand / tenant theming usually no for v1. Default: skip.
## D11 — Accessibility tokens
**D11.1 Focus ring.** Style (solid / double / inner-outer) and color (brand / accent / outline-default). Default: solid 2px brand at 60% opacity.
**D11.2 Contrast targets.** Confirm WCAG AA (4.5:1 text, 3:1 UI). AAA optional. Default: AA.
---
# Part 2 — Interaction design
## I1 — Input model
**I1.1 Primary input.** touch (mobile/tablet) / mouse / keyboard / mixed.
Often determined by platform from `concept.md` — confirm rather than ask cold.
**I1.2 Supported inputs.** Which secondaries must work; which are not in scope.
**I1.3 Voice / switch / assistive.** Defer unless `users.md` has an actor that needs it.
## I2 — State surface declarations
The most important phase. For each action class, pick one pattern. Walk
through them in this order:
| Action class | Common picks | Default |
|---|---|---|
| Quick edit (single field) | inline edit / popover / drawer | inline edit |
| Multi-field create | drawer / modal / full-page route | drawer (web), modal (native) |
| Multi-field edit | drawer / modal / full-page route | same as create |
| Confirm destructive | modal | modal (always — non-negotiable) |
| Settings / configuration | full-page route | full-page route |
| Top-level navigation | sidebar / tab bar / header nav | tab bar (mobile), sidebar (desktop) |
| Detail-from-list | full route / side panel / drawer | full route |
| Filtering / sorting | inline controls / popover / drawer | inline (small set), drawer (large set) |
| Search | header search / command palette / page | header search |
For each, show the default and the alternatives. User confirms or picks.
## I3 — Motion posture
**I3.1 Posture.** minimal / restrained / expressive.
**I3.2 Animation triggers.** For each, yes/no/conditional:
- Page/route transitions
- List item enter/exit
- Modal/drawer open/close
- State changes (toggle, expand)
- Loading→loaded transitions
Defaults from the posture choice — show; accept.
**I3.3 Reduced motion fallbacks.** Confirm `prefers-reduced-motion` is
honored (always yes; this is policy, not preference).
## I4 — Breakpoints
**I4.1 Count.** 2 (mobile / desktop) / 3 (mobile / tablet / desktop) / 4 (with large desktop) / fluid.
**I4.2 Values.** Defaults: 640 / 768 / 1024 / 1280. Tweakable.
**I4.3 Primary target.** mobile-first / desktop-first / fluid. Determined by platform.
## I5 — i18n
**I5.1 Locales at launch.** Default: just English (`en` or `en-US`).
**I5.2 RTL support.** yes / no / planned. Default: no in v1.
**I5.3 Pluralization.** ICU MessageFormat / library / manual. Default: ICU MessageFormat (industry standard).
**I5.4 String length budget.** Default: assume +30% over English for de/fr translations. Always declared.
## I6 — State postures
Four sub-decisions; one declared posture each.
**I6.1 Empty state.**
- Visual: icon + heading + body + CTA / heading + body / heading only.
- Tone: encouraging / neutral / matter-of-fact.
- CTA: present always / present when actionable / never.
**I6.2 Loading state.**
- Default: skeleton / spinner / blocking. Pick one default; assign exceptions.
- Threshold for showing loading at all: 200ms (default) / 300ms / immediate.
**I6.3 Error state.**
- Surface: inline (default) / toast / dialog. By severity class.
- Recovery: retry / clear path forward / report.
- Copy tone: plain language always; what happened + what to do next.
**I6.4 Offline state.**
- Behavior: full subset works / banner only / "go online to continue".
- Sync: queue + flush on reconnect / fail loudly / not in v1.
## I7 — Time formatting
**I7.1 Relative-time threshold.** Within `<7 days>` / `<30 days>` / `<24 hours>` / never. Default: 7 days.
**I7.2 Absolute format.** `MMM d, yyyy h:mm a` / ISO / locale-aware. Default: locale-aware via `Intl.DateTimeFormat`.
**I7.3 Time zones.** user-local / fixed (which?) / configurable. Default: user-local.
## I8 — Keyboard
**I8.1 Global shortcuts.** Y/N for v1. If yes, list them; if no, mark "deferred to vN".
**I8.2 Modal escape.** Always yes; confirm.
**I8.3 Focus trap in modal.** Always yes; confirm. Restore on close.
**I8.4 Form submit.** Enter submits / Cmd+Enter submits / explicit only. Default: Enter submits.
**I8.5 List navigation.** Arrow keys yes/no. Default: no in v1 unless lists are central.
## I9 — Optimistic updates & error recovery
**I9.1 Default posture.**
- Optimistic with rollback (default for most actions — feels fast)
- Pessimistic (default for destructive or money-related actions)
- Mixed (most actions optimistic; some classes pessimistic)
**I9.2 Rollback strategy.** Toast + revert / silent revert / dialog. Default: toast + revert.
**I9.3 Conflict resolution.** Last-write-wins / show conflict UI / append. Default depends on platform.
## I10 — Gestures (touch only)
Skip this phase if I1.1 didn't include touch.
**I10.1 Swipe actions on rows.** Y/N. If yes: which actions, which directions.
**I10.2 Long-press menu.** Y/N. Default: yes (iOS/Android conventional).
**I10.3 Pull-to-refresh.** Y/N for list views.
## I11 — Focus management
**I11.1 After modal close.** Restore previous focus (default — non-negotiable for a11y).
**I11.2 After route change.** Heading focus (default) / container focus / no change.
**I11.3 After form error.** Focus first error field (default).
---
## Reference banks
When the user names a reference ("like Linear", "like Notion"), apply
these presets as starting points — user still confirms each value.
| Reference | Visual posture | Color tone | Type | Density | Motion | Radii |
|---|---|---|---|---|---|---|
| **Linear** | quiet, dense, monochrome with one purple accent | cool neutral | Inter | compact | restrained, fast | tight |
| **Notion** | minimal, generous spacing, friendly | warm-leaning true neutral | Inter / system | comfortable | minimal | medium |
| **Stripe** | clean, technical, indigo accent | cool neutral | Camphor / system | comfortable | restrained | medium |
| **GitHub** | utilitarian, dense, clear hierarchy | cool true neutral | -apple-system stack | compact | minimal | tight |
| **Vercel** | sharp, monochrome, contrasty | true neutral, almost B&W | Geist / Inter | comfortable | restrained, fast | tight |
| **Apple HIG** | spacious, soft, system-native | warm-cool mix per platform | SF Pro | cozy | expressive | medium |
| **Material 3** | adaptive, expressive, dynamic color | tone-based dynamic | Roboto / Plus Jakarta | comfortable | expressive | pillowy |
For "make it look exotic / brutalist / playful / etc.", branch
cleanly: ask for two or three reference URLs the user has in mind,
and reflect them back as a posture sentence rather than picking presets.
## Write phase
After the interview ends, write all six files in one batch:
1. `.claude/skills/<slug>-design-system/SKILL.md` from `templates/design-system/SKILL.md` — substitute slug, project name, and all interview answers.
2. `.claude/skills/<slug>-design-system/tokens.ts` from `templates/design-system/tokens.ts` — fill in all values; `<TBD>` for skipped phases.
3. `.claude/skills/<slug>-design-system/components.md` from `templates/design-system/components.md` — vocabulary matched to `ia.md`.
4. `.claude/skills/<slug>-interaction-design/SKILL.md` from `templates/interaction-design/SKILL.md` — fill in all interview answers.
5. `docs/planning/design-system.md` from `templates/planning-pointers/design-system.md`.
6. `docs/planning/interaction-design.md` from `templates/planning-pointers/interaction-design.md`.
Then tell the user:
```
Wrote two project skills:
.claude/skills/<slug>-design-system/ (SKILL.md, tokens.ts, components.md)
.claude/skills/<slug>-interaction-design/ (SKILL.md)
These auto-load whenever UI work is happening. They should be available
immediately in this session; if you're starting a new session in this
repo, they'll load on the first UI-related prompt.
Items still <TBD> (return-pass worklist):
• <list any phases that were skipped or had unanswered questions>
```
## Re-entry ## Re-entry
When the user returns to flesh this out fully: Re-running the skill on a project that already has the artifacts:
- Expand the interviews above.
- Expand the SKILL.md and sidecar templates.
- Both skills are version-controlled in `.claude/skills/` — re-running
this phase shows the user a diff before overwriting.
The downstream contract is: `composer-mocks` reads the **skills**, not 1. Read both existing SKILL.md files plus tokens.ts and components.md.
the planning pointer files. That contract does not change as the 2. List what's currently set; mark `<TBD>` items prominently.
templates grow. 3. Ask: "Update everything, just the TBDs, or pick a phase?"
4. Run only the chosen scope.
5. Always show a diff before writing.
## Hard rules even in placeholder mode ## Hard rules
- **Always produce both skills with sidecars.** Mocks can't proceed without them. - **Always produce all six files.** Mocks can't proceed without them.
- **Always slug-namespace** so projects on the same machine don't collide. - **Always slug-namespace** so projects on the same machine don't collide.
- **Mark unknowns with `<TBD: ...>`** rather than inventing — TBDs are the return-pass worklist. - **`<TBD: ...>` rather than invent.** TBDs are the return-pass worklist.
- **Don't pretend.** If the user has nothing and no opinions, say so in the SKILL.md: "No design direction set; defaults applied. Revisit before mocks are committed." - **Verify a11y contrast** before writing color tokens. Don't silently accept failures.
- **Tell the user what just happened.** After write, print: - **Honor `prefers-reduced-motion` is policy, not opinion.** Always declare it as honored.
``` - **Confirm before overwriting** existing skill files on re-entry.
Wrote two project skills: - **Tell the user what was written and where**, plus how to verify the skills load.
.claude/skills/<slug>-design-system/SKILL.md
.claude/skills/<slug>-interaction-design/SKILL.md
These auto-load whenever UI work is happening. Verify after restart
with /agents (they should appear as project-local skills).
```

View File

@@ -1,94 +1,140 @@
--- ---
name: <SLUG>-design-system name: <SLUG>-design-system
description: Design system for the <PROJECT NAME> project. Color tokens, typography, spacing, radii, shadows, motion durations, and component vocabulary. Load and apply whenever building UI for this project — generating mocks, writing components, picking colors, configuring Tailwind, styling native screens, building themes, designing CSS, or making any visual decision. The canonical visual reference for this project; if anything visible is being touched, this skill should be in context. Pairs with <SLUG>-interaction-design. description: Design system for the <PROJECT NAME> project. Color tokens, typography, spacing, radii, shadows, density, motion, accessibility tokens, and component vocabulary. Load and apply whenever building UI for this project — generating mocks, writing components, picking colors, configuring Tailwind, styling native screens, building themes, designing CSS, or making any visual decision. The canonical visual reference for this project; if anything visible is being touched, this skill should be in context. Pairs with <SLUG>-interaction-design.
--- ---
# <PROJECT NAME> design system # <PROJECT NAME> design system
> **Status: <draft | reviewed | locked>** > **Status: <draft | reviewed | locked>**
> Generated by Composer's `ui-system` phase. Re-running that phase > Generated by Composer's `ui-system` phase. Re-running that phase
> updates this skill (with confirmation). > updates this skill (with confirmation on overwrite).
## How to use this skill ## How to use this skill
Whenever you generate or edit UI for this project — a mock, a real When generating or editing UI for this project — a mock, a real
component, a theme, a CSS file — read this SKILL.md and the sidecar component, a theme, a CSS file — read this SKILL.md and `tokens.ts`
files first, then use the tokens. Do not hardcode values. Do not first, then use the tokens. Never hardcode visual values. Don't
introduce new components without proposing them here first. introduce new components without proposing them in `components.md`.
Sidecar files in this skill directory: Sidecar files in this skill directory:
- `tokens.ts` — machine-readable token export. Import in mocks/code. - `tokens.ts` — machine-readable token export. Import in mocks/code.
- `components.md` — component vocabulary with anatomy. - `components.md` — component vocabulary, anatomy, states.
## Visual posture ## Visual posture
<!-- One or two sentences describing the feel. "Quiet, dense, monochromatic with a single brand accent." Or "Playful, generous spacing, expressive type." --> <!-- One-sentence north star. -->
<TBD: visual posture> <TBD: visual posture sentence>
## Color ## Color
### Brand ### Brand
- Primary: `<#hex>` — <name> - **Primary**: `<#hex>` — <name>
- (Secondary, if any): `<#hex>` — <name> - **Secondary** *(if any)*: `<#hex>` — <name>
### Neutrals ### Neutrals
- Tone: <warm | cool | true> - **Tone**: <warm | cool | true>
- Scale: 50 / 100 / 200 / 300 / 400 / 500 / 600 / 700 / 800 / 900 / 950 - **Scale**: 50 / 100 / 200 / 300 / 400 / 500 / 600 / 700 / 800 / 900 / 950
- See `tokens.ts` for hex values.
### Semantic ### Semantic
| Role | Value | | Role | Light | Dark *(if dark mode)* |
|---|---| |---|---|---|
| Success | `<#hex>` | | Success | `<#hex>` | `<#hex>` |
| Warning | `<#hex>` | | Warning | `<#hex>` | `<#hex>` |
| Error | `<#hex>` | | Error | `<#hex>` | `<#hex>` |
| Info | `<#hex>` | | Info | `<#hex>` | `<#hex>` |
### Dark mode ### Dark mode
<TBD: yes / no / system-following + token mapping> - **Posture**: <yes | no | system-following>
- **Token mapping**: see `tokens.ts` `colorDark` (if applicable)
### Accessibility contrast
- **Target**: WCAG <AA | AAA>
- **Verified pairings** (against `neutral.50` light bg, `neutral.950` dark bg):
- body text: <ratio> — ✅
- UI components: <ratio> — ✅
- <flag any below threshold>
## Typography ## Typography
- **Display family**: <name>, weights <list> - **Display family**: <name>, weights <list>
- **Body family**: <name>, weights <list> - **Body family**: <name | "(same as display)">, weights <list>
- **Scale base**: <px>, ratio <1.125 | 1.2 | 1.25 | 1.333> - **Mono family** *(if used)*: <name>
- **Sizes**: xs / sm / base / lg / xl / 2xl / 3xl / 4xl - **Scale base**: <px>, ratio <1.125 | 1.2 | 1.25 | 1.333 | 1.5>
- **Line-height**: tight for display, normal for body, relaxed for long-form prose - **Sizes**: see `tokens.ts` `fontSize` — xs / sm / base / lg / xl / 2xl / 3xl / 4xl
- **Line-height**:
See `tokens.ts` for exact px values. - Tight (display): <value>
- Normal (body): <value>
- Relaxed (long-form): <value>
- **Letter-spacing** *(if customized)*: <values per size class>
## Spacing ## Spacing
- **Base unit**: <4px | 8px> - **Base unit**: <4px | 8px>
- **Scale (in base units)**: 0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 12, 16, 24 - **Scale (in base units)**: 0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 12, 16, 24
- See `tokens.ts` `spacing` for px values.
## Radii ## Radii
- `sm`, `md`, `lg`, `pill`, `full` — values in `tokens.ts`. - **Posture**: <sharp | tight | medium | pillowy | mixed>
- `sm` <px> · `md` <px> · `lg` <px> · `pill` 9999px · `full` 9999px
## Shadows ## Shadows & elevation
- `xs / sm / md / lg / xl` — values in `tokens.ts`. - **Posture**: <flat | subtle | generous>
- (Or: "flat — no shadows" if the posture is flat.) - Scale: `xs / sm / md / lg / xl` — see `tokens.ts` `shadow`
- (Or "no shadows in this system" if posture is flat — use borders for elevation hierarchy.)
## Density
- **Default**: <comfortable | cozy | compact>
- **Padding tokens**: see `tokens.ts` `padding``tight / default / loose`
- **Component impact**: list components whose default density differs from the global default.
## Visual motion ## Visual motion
- **Default duration**: <ms> - **Default duration**: <ms> (token: `motion.duration.default`)
- **Default easing**: <named curve or cubic-bezier> - **Default easing**: <named curve> (token: `motion.easing.standard`)
- **Reduced-motion fallback**: linear, instant where appropriate
Behavioral motion (when things animate, what does and doesn't) lives Behavioral motion (when things animate, what triggers them) is in the
in the interaction-design skill. **interaction-design** skill, not here.
## Accessibility tokens
- **Focus ring**: <style — solid 2px brand at 60% opacity / etc.>
- **Focus ring color token**: `color.focus`
- **Focus offset**: <px>
- **Hit-target minimum**: 44×44px (mobile) / 32×32px (desktop pointer)
- **Contrast targets**: WCAG <AA | AAA> — body 4.5:1, UI 3:1
## Components ## Components
See `components.md` in this directory for the full vocabulary and See `components.md` in this directory for the full vocabulary, anatomy,
anatomy. The TL;DR list: and state coverage. The TL;DR list of primitives:
- Button (primary / secondary / ghost / destructive / link) - **Button** (primary / secondary / ghost / destructive / link)
- Input (text / textarea / select / checkbox / radio / toggle) - **Input** (text / textarea / select / checkbox / radio / toggle)
- Card / Surface - **Card / Surface**
- (project-specific components from `ia.md`) - **Badge / Tag**
- **Avatar**
- **Tooltip**
- **Modal / Drawer / Popover** *(behavioral patterns in interaction-design skill)*
- **Tabs**
- **Menu / Dropdown**
- **Toast / Notification**
Plus project-specific components from `ia.md`: see `components.md`.
## Theming variants
- **Multi-brand / tenant theming**: <yes | no — typically no for v1>
- **Theme-switching mechanism** *(if applicable)*: <CSS variables / data-theme attr / etc.>
## TBD on return ## TBD on return
- <TBD: anything the placeholder pass couldn't answer> <!-- Any phases that were skipped or had unanswered questions in the
ui-system interview. This is the worklist for the next pass. -->
- <TBD: ...>

View File

@@ -1,52 +1,174 @@
# Component vocabulary # Component vocabulary
The named components for <PROJECT NAME>. Match names from `ia.md` The named components for <PROJECT NAME>. Component names match
where applicable. Do not introduce new components without proposing `ia.md` where applicable. **Do not introduce new components without
them here first. proposing them here first.**
This file is loaded automatically by the design-system skill — agents This file is a sidecar of the design-system skill — agents generating
generating UI should be aware of every component in this list and UI should be aware of every component in this list and prefer them to
prefer them to ad-hoc one-offs. ad-hoc one-offs. New patterns belong here, not in mock files.
## How to read this
For each component:
- **Variants** — distinct visual flavors with different semantic meaning
- **Sizes** — usually `sm / md / lg`; pick the same scale across components
- **States** — every visible state the component must handle
- **Anatomy** — the named slots / regions the component is composed of
- **Tokens used** — which design-system tokens this component reads
- **A11y notes** — load-bearing accessibility rules for this component
## Primitives ## Primitives
### Button ### Button
- **Variants**: primary | secondary | ghost | destructive | link
- **Sizes**: sm | md | lg - **Variants**: `primary` | `secondary` | `ghost` | `destructive` | `link`
- **States**: default | hover | active | focus | disabled | loading - **Sizes**: `sm` | `md` | `lg`
- **Anatomy**: optional leading icon · label · optional trailing icon - **States**: default | hover | active | focus | focus-visible | disabled | loading
- **Motion**: <TBD> - **Anatomy**: optional leading icon · label · optional trailing icon · optional loading spinner
- **Tokens used**: `color.brand.primary`, `color.semantic.error` (destructive), `color.focus`, `padding.default`, `radius.md`, `motion.duration.fast`, `fontSize.sm/base/lg`, `fontWeight.medium`
- **A11y**:
- Hit target ≥ `hitTarget.mobile` for `md+` sizes
- `aria-busy` while loading; preserve label
- `aria-disabled` rather than `disabled` if the action is permission-gated rather than form-invalid
- `link` variant uses `<a>`, not `<button>`
### Input ### Input
- **Variants**: text | textarea | select | checkbox | radio | toggle
- **Sizes**: sm | md | lg - **Variants**: `text` | `textarea` | `select` | `checkbox` | `radio` | `toggle` | `password` | `number` | `date`
- **States**: default | focus | error | disabled | readonly - **Sizes**: `sm` | `md` | `lg`
- **Anatomy**: label · field · helper text · error message - **States**: default | focus | filled | disabled | readonly | error | success *(rare)*
- **Validation pattern**: <TBD — see interaction-design skill> - **Anatomy**: label · *(optional)* description · field · *(optional)* helper text · *(error or hint)* message
- **Tokens used**: `color.neutral.*`, `color.semantic.error`, `color.focus`, `padding.default`, `radius.sm`, `fontSize.base`
- **A11y**:
- Label is **always** present (visible or `aria-label`)
- Error message is `aria-describedby`-linked to the field
- `aria-invalid="true"` while error
- Required fields marked in two ways (asterisk + `required` attribute)
- `aria-describedby` for helper text
### Card / Surface ### Card / Surface
- **Variants**: <flat | bordered | elevated — pick or list>
- **Anatomy**: <header? · body · footer?> - **Variants**: `flat` | `bordered` | `elevated` *(per design-system shadow posture)*
- **States**: default | hover *(if interactive)* | focus *(if interactive)* | disabled
- **Anatomy**: optional header · body · optional footer · optional media region
- **Tokens used**: `color.neutral.*`, `radius.lg`, `shadow.sm` (elevated), `padding.loose`
- **A11y**:
- If clickable, the entire card is one `<button>` or `<a>` — not nested interactives
- If contains interactives, card itself is not clickable
### Badge / Tag
- **Variants**: `neutral` | `success` | `warning` | `error` | `info` | `brand`
- **Sizes**: `sm` | `md`
- **States**: default | *(removable)* with close button
- **Anatomy**: optional dot · label · optional close button
- **Tokens used**: `color.semantic.*`, `radius.pill`, `fontSize.xs`, `fontWeight.medium`
### Avatar
- **Variants**: `image` | `initials` | `placeholder`
- **Sizes**: `xs` | `sm` | `md` | `lg` | `xl`
- **States**: default | with status indicator
- **Anatomy**: image / initials · *(optional)* status dot
- **Tokens used**: `radius.full`, `color.neutral.*` (fallback bg), `fontSize.*` (initials sizing)
- **A11y**: `alt` text required for image variant; meaningful or empty `alt=""` for purely decorative
### Tooltip
- **Variants**: `default` | `error` | `info`
- **States**: hidden | shown
- **Anatomy**: trigger · floating panel · optional arrow
- **Tokens used**: `color.neutral.900` bg, `color.neutral.50` text, `radius.sm`, `shadow.md`, `motion.duration.fast`
- **A11y**:
- `role="tooltip"`, `aria-describedby` on trigger
- Show on hover AND focus (keyboard parity)
- Dismissible with `Esc`
- Never the only conveyance of critical information
### Modal / Drawer / Popover
These are **behavioral patterns** — see the interaction-design skill
for *when* to use which. The visual styling is here.
- **Modal**:
- Anatomy: backdrop · panel · optional header · body · optional footer · close button
- Tokens: `shadow.xl`, `radius.lg`, `motion.duration.default`, `zIndex.modal`
- Always traps focus; restores on close
- **Drawer**:
- Anatomy: backdrop · panel (side-anchored) · header · body · footer · close button
- Tokens: `shadow.xl`, `motion.duration.default`, `zIndex.drawer`
- **Popover**:
- Anatomy: trigger · floating panel · optional arrow
- Tokens: `shadow.md`, `radius.md`, `zIndex.popover`
### Tabs
- **Variants**: `underline` | `pills` | `enclosed`
- **States**: active | inactive | hover | focus | disabled
- **Anatomy**: tab list · tab · panel
- **A11y**:
- `role="tablist"`, `role="tab"`, `role="tabpanel"`
- Arrow-key navigation between tabs (per WAI-ARIA Authoring Practices)
- Tab `aria-selected` and `tabindex` managed appropriately
### Menu / Dropdown
- **Variants**: `default` | `with-icons` | `with-shortcuts` | `submenu`
- **States**: closed | open
- **Anatomy**: trigger · floating panel · menu items · separators
- **Tokens used**: `shadow.md`, `radius.md`, `padding.tight`, `zIndex.dropdown`
- **A11y**:
- `role="menu"`, `role="menuitem"`
- Arrow-key navigation; `Esc` closes
- Type-ahead is encouraged
- Focus returns to trigger on close
### Toast / Notification
- **Variants**: `default` | `success` | `warning` | `error` | `info`
- **Anatomy**: optional icon · title · optional description · optional action button · close button
- **Tokens used**: `color.semantic.*`, `radius.md`, `shadow.lg`, `motion.duration.default`, `zIndex.toast`
- **A11y**:
- `role="status"` for non-critical, `role="alert"` for critical
- Dismissible by user; don't auto-dismiss critical errors
- Stack region is `aria-live="polite"` (or `assertive` for errors)
## Project-specific components ## Project-specific components
<!-- <!--
Pulled from ia.md. One block each, with anatomy and any state-specific Pulled from ia.md. One block each, with anatomy and any state-specific
notes that should always apply when this component is rendered. notes that should always apply when this component is rendered. Match
the format of the primitives above.
--> -->
### <Component name> ### <Component name>
- **Purpose**: <one line> - **Variants**: <list>
- **Anatomy**: <slots / regions> - **Sizes**: <list>
- **States**: <list> - **States**: <list>
- **Notes**: <anything load-bearing> - **Anatomy**: <slots / regions>
- **Tokens used**: <list>
- **A11y**: <load-bearing rules>
- **Notes**: <anything else>
## Patterns to avoid ## Patterns to avoid
<!-- Things that come up often and should be vetoed by default. Saves the
Things that come up often and should be vetoed. Saves the agent from agent from re-inventing them.
re-inventing them.
-->
- <TBD: e.g., "no nested cards", "no inline icons in body text", etc.> - **Nested cards** — flatten or use sections; visual hierarchy comes from spacing and dividers, not stacking surfaces
- **Inline icons in body text** — icons in body text rarely scale or align well; use them in headers, buttons, badges
- **Modal-from-modal** — if a modal needs to launch another modal, the first modal is doing too much
- **Tooltip as the only conveyance** — tooltips are supplementary; the primary information must be available without hover
- **Color-only meaning** — every color-coded distinction needs a redundant signal (icon, label, position)
- **Custom focus rings that don't meet contrast** — use the system focus-ring token
- **Animation that violates `prefers-reduced-motion`** — always honor the user preference
- **Click targets below 44px on touch devices**
## Adding a new component
1. Propose it here first with the same shape as the others (variants, states, anatomy, tokens, a11y).
2. Add it to `tokens.ts` if it needs new tokens.
3. Add a mock that uses it before writing real-code implementation.
4. Council review (Maya for a11y, Marge for end-user) before locking in.

View File

@@ -1,13 +1,20 @@
// Design tokens for <PROJECT NAME>. // Design tokens for <PROJECT NAME>.
// Generated by Composer's ui-system phase. // Generated by Composer's ui-system phase.
// Single source of truth for colors, type, spacing, radii, shadows, motion. // Single source of truth colors, type, spacing, radii, shadows,
// Import this in mocks and (eventually) in real UI code; do not hardcode. // density, motion, accessibility. Import this in mocks AND in real
// UI code; do not hardcode visual values.
// =============================================================================
// COLOR
// =============================================================================
export const color = { export const color = {
brand: { brand: {
primary: "<TBD: #hex>", primary: "<TBD: #hex>",
// secondary: "<TBD>", // secondary: "<TBD>",
}, },
// Neutral scale, tone: <warm | cool | true>
neutral: { neutral: {
50: "<TBD>", 50: "<TBD>",
100: "<TBD>", 100: "<TBD>",
@@ -21,41 +28,80 @@ export const color = {
900: "<TBD>", 900: "<TBD>",
950: "<TBD>", 950: "<TBD>",
}, },
semantic: { semantic: {
success: "<TBD>", success: "<TBD>",
warning: "<TBD>", warning: "<TBD>",
error: "<TBD>", error: "<TBD>",
info: "<TBD>", info: "<TBD>",
}, },
// dark mode tokens (if applicable):
// dark: { ... } // Accessibility tokens
focus: "<TBD>", // focus-ring color
selection: "<TBD>", // text selection background
// Text on each neutral background, pre-verified for contrast
textOnLight: "<TBD>", // body text on neutral.50neutral.200 — must be ≥ 4.5:1
textOnDark: "<TBD>", // body text on neutral.800neutral.950 — must be ≥ 4.5:1
textMuted: "<TBD>", // secondary text — must be ≥ 4.5:1 for body, 3:1 for UI
} as const; } as const;
// Dark-mode overrides (only filled in if dark mode = yes/system).
// When dark mode is active, prefer these over `color.*`.
export const colorDark = {
brand: { primary: "<TBD>" },
neutral: { 50: "<TBD>", 100: "<TBD>", /* … */ 950: "<TBD>" },
semantic: { success: "<TBD>", warning: "<TBD>", error: "<TBD>", info: "<TBD>" },
} as const;
// =============================================================================
// TYPOGRAPHY
// =============================================================================
export const fontFamily = { export const fontFamily = {
display: "<TBD: name>, system-ui, sans-serif", display: '"<TBD: name>", system-ui, -apple-system, sans-serif',
body: "<TBD: name>, system-ui, sans-serif", body: '"<TBD: name>", system-ui, -apple-system, sans-serif',
mono: "ui-monospace, SFMono-Regular, monospace", mono: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
} as const; } as const;
// Modular scale — base × ratio
export const fontSize = { export const fontSize = {
xs: "12px", xs: "12px",
sm: "14px", sm: "14px",
base:"16px", base: "16px", // <TBD: confirm base>
lg: "18px", lg: "18px",
xl: "20px", xl: "20px",
"2xl": "24px", "2xl": "24px",
"3xl": "30px", "3xl": "30px",
"4xl": "36px", "4xl": "36px",
"5xl": "48px",
} as const;
export const fontWeight = {
regular: 400,
medium: 500,
semibold: 600,
bold: 700,
} as const; } as const;
export const lineHeight = { export const lineHeight = {
tight: 1.2, tight: 1.2, // display
normal: 1.5, normal: 1.5, // body
relaxed: 1.7, relaxed: 1.7, // long-form prose
} as const; } as const;
// Spacing scale in base units. Multiply by base (4 or 8) for px. export const letterSpacing = {
tight: "-0.02em",
normal: "0",
wide: "0.04em",
} as const;
// =============================================================================
// SPACING
// =============================================================================
export const spacingBase = 4; // <TBD: 4 or 8> export const spacingBase = 4; // <TBD: 4 or 8>
export const spacing = { export const spacing = {
0: 0, 0: 0,
0.5: 0.5 * spacingBase, 0.5: 0.5 * spacingBase,
@@ -71,15 +117,36 @@ export const spacing = {
24: 24 * spacingBase, 24: 24 * spacingBase,
} as const; } as const;
// Padding tokens by density posture — components reference these
// rather than picking spacing values directly. Switch the global
// density by re-pointing `density` (or per-component override).
export const padding = {
tight: { x: spacing[2], y: spacing[1] },
default: { x: spacing[3], y: spacing[2] },
loose: { x: spacing[4], y: spacing[3] },
} as const;
export const density = "default" as keyof typeof padding; // <TBD: "tight" | "default" | "loose">
// =============================================================================
// RADII
// =============================================================================
export const radius = { export const radius = {
sm: "<TBD: px>", none: "0",
md: "<TBD: px>", sm: "<TBD>",
lg: "<TBD: px>", md: "<TBD>",
lg: "<TBD>",
pill: "9999px", pill: "9999px",
full: "9999px", full: "9999px",
} as const; } as const;
// =============================================================================
// SHADOWS
// =============================================================================
export const shadow = { export const shadow = {
none: "none",
xs: "<TBD>", xs: "<TBD>",
sm: "<TBD>", sm: "<TBD>",
md: "<TBD>", md: "<TBD>",
@@ -87,15 +154,63 @@ export const shadow = {
xl: "<TBD>", xl: "<TBD>",
} as const; } as const;
// =============================================================================
// MOTION
// =============================================================================
export const motion = { export const motion = {
duration: { duration: {
instant: 0,
fast: 150, fast: 150,
default: 250, default: 250,
slow: 400, slow: 400,
slower: 600,
}, },
easing: { easing: {
standard: "cubic-bezier(0.2, 0, 0, 1)", standard: "cubic-bezier(0.2, 0, 0, 1)",
accel: "cubic-bezier(0.4, 0, 1, 1)", accel: "cubic-bezier(0.4, 0, 1, 1)",
decel: "cubic-bezier(0, 0, 0.2, 1)", decel: "cubic-bezier(0, 0, 0.2, 1)",
bounce: "cubic-bezier(0.34, 1.56, 0.64, 1)",
}, },
} as const; } as const;
// =============================================================================
// Z-INDEX
// =============================================================================
export const zIndex = {
base: 0,
raised: 10,
dropdown: 100,
sticky: 200,
drawer: 300,
modal: 400,
popover: 500,
toast: 600,
tooltip: 700,
max: 9999,
} as const;
// =============================================================================
// BREAKPOINTS
// =============================================================================
// Note: breakpoint values live here for visual-token coordination, but the
// authoritative *interpretation* (mobile-first, primary target, etc.) is in
// the interaction-design skill.
export const breakpoint = {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
"2xl": "1536px",
} as const;
// =============================================================================
// HIT TARGETS (a11y)
// =============================================================================
export const hitTarget = {
mobile: 44, // px — WCAG 2.2 minimum
desktop: 32, // px — pointer-input minimum
} as const;

View File

@@ -1,112 +1,230 @@
--- ---
name: <SLUG>-interaction-design name: <SLUG>-interaction-design
description: Interaction design for the <PROJECT NAME> project. State surfaces (modal vs inline vs drawer vs full-page), motion philosophy, input modes (touch/mouse/keyboard), breakpoints, i18n posture, empty/loading/error/offline state postures, time formatting, and keyboard policy. Load and apply whenever building UI behavior — modals, forms, navigation, transitions, gestures, error handling, async states, focus management. Pairs with <SLUG>-design-system. description: Interaction design for the <PROJECT NAME> project. State surfaces (modal vs inline vs drawer vs full-page), motion philosophy, input modes (touch/mouse/keyboard), breakpoints, i18n posture, empty/loading/error/offline state postures, time formatting, keyboard policy, optimistic-update strategy, gesture handling, and focus management. Load and apply whenever building UI behavior — modals, forms, navigation, transitions, gestures, error handling, async states, focus management, or any decision about how the interface responds. Pairs with <SLUG>-design-system.
--- ---
# <PROJECT NAME> interaction design # <PROJECT NAME> interaction design
> **Status: <draft | reviewed | locked>** > **Status: <draft | reviewed | locked>**
> Generated by Composer's `ui-system` phase. Re-running that phase > Generated by Composer's `ui-system` phase. Re-running that phase
> updates this skill (with confirmation). > updates this skill (with confirmation on overwrite).
## How to use this skill ## How to use this skill
When building UI *behavior* — anything beyond static styling — read When building UI *behavior* — anything beyond static styling — read
this skill first. Don't invent new state-surface patterns, motion this skill first. Don't invent new state-surface patterns, motion
durations, or empty-state postures. They're declared once here and durations, or empty-state postures. They're declared once here and
reused everywhere. reused everywhere. For visual tokens (color, type, spacing), see the
paired `<SLUG>-design-system` skill.
For visual tokens (color, type, spacing), see the paired If a screen needs a pattern that isn't declared here, propose it in
`<SLUG>-design-system` skill. this skill before writing the code — not the other way around.
## Input modes ## Input modes
| Mode | Status | | Mode | Status | Notes |
|---|---| |---|---|---|
| Touch | <TBD: primary | supported | not supported> | | Touch | <primary | supported | not supported> | <…> |
| Mouse | <TBD> | | Mouse | <…> | <…> |
| Keyboard | <TBD> | | Keyboard | supported (always — a11y baseline) | full keyboard navigation required |
| Voice | <TBD: usually "not supported" in v1> | | Voice | <typically not supported in v1> | — |
| Switch / assistive | <…> | — |
**Primary**: <which> — design defaults assume this mode.
## State surfaces ## State surfaces
One pattern per action class. Used everywhere. One pattern per action class. Used everywhere. If a screen needs a
deviation, document it inline as an exception with a reason.
| Action class | Pattern | Notes | | Action class | Pattern | Notes |
|---|---|---| |---|---|---|
| Quick edit (single field) | inline edit | <TBD> | | Quick edit (single field) | <inline edit | popover | drawer> | — |
| Multi-field create / edit | <drawer | modal — pick one> | <TBD> | | Multi-field create | <drawer | modal | full-page route> | — |
| Confirm destructive | modal | always | | Multi-field edit | <drawer | modal | full-page route> | usually same as create |
| Confirm destructive | modal | always — non-negotiable |
| Settings / configuration | full-page route | — | | Settings / configuration | full-page route | — |
| Top-level navigation | <sidebar | tab bar | header nav> | <TBD> | | Top-level navigation | <sidebar | tab bar | header nav> | — |
| Detail-from-list | <full route | side panel> | <TBD> | | Detail-from-list | <full route | side panel | drawer> | — |
| Filtering / sorting | <inline controls | popover | drawer> | — |
| Search | <header search | command palette | dedicated page> | — |
## Motion ## Motion
- **Posture**: <minimal | restrained | expressive> - **Posture**: <minimal | restrained | expressive>
- **Default duration / easing**: see design-system tokens - **Default duration / easing**: see design-system tokens (`motion.duration.default`, `motion.easing.standard`)
- **Reduced motion**: respected via `prefers-reduced-motion` everywhere; fallbacks declared per pattern below
- **When things animate**: ### Animation triggers
- Page/route transitions: <yes/no, how>
- List item enter/exit: <yes/no, how> | Trigger | Animated? | Duration / easing |
- Modal/drawer open/close: <yes/no, how> |---|---|---|
- State changes (toggle, expand): <yes/no, how> | Page / route transitions | <yes | no | conditional> | <token> |
| List item enter / exit | <…> | <token> |
| Modal / drawer open / close | <…> | <token> |
| State changes (toggle, expand) | <…> | <token> |
| Loading → loaded transitions | <…> | <token> |
| Hover / focus state changes | <…> | <token — usually `fast`> |
### Reduced motion
`prefers-reduced-motion: reduce` is **always honored**. Fallback strategy:
- Replace transitions with instant state changes.
- Replace fades with instant show/hide.
- Replace slides with crossfades or instant.
- Keep hover/focus state changes (these are essential signals, not motion).
## Breakpoints ## Breakpoints
| Breakpoint | Range | Primary? | | Breakpoint | Range | Purpose |
|---|---|---| |---|---|---|
| Phone | < <px> | <TBD> | | Phone | < <px> | <> |
| Tablet | <px> <px> | <TBD> | | Tablet | <px> <px> | <> |
| Desktop | ≥ <px> | <TBD> | | Desktop | ≥ <px> | <> |
| (Large desktop) | ≥ <px> | optional |
**Primary design target**: <which one — "mobile-first", "desktop-first", "fluid"> - **Primary design target**: <mobile-first | desktop-first | fluid>
- **Fluid scale ranges** *(if used)*: <…>
- See `tokens.ts` `breakpoint` for px values.
## i18n ## i18n
- **Locales at launch**: <list — often just English in v1> - **Locales at launch**: <list>
- **RTL support**: <yes | no | planned> - **RTL support**: <yes | no | planned for vN>
- **String length budget**: assume +30% over English for German/French translations - **Pluralization**: <ICU MessageFormat | library | manual>
- **Pluralization**: <TBD: ICU MessageFormat / library / manual> - **String length budget**: assume +30% over English for de/fr translations
- **Date / number formatting**: `Intl.DateTimeFormat` / `Intl.NumberFormat` (locale-aware)
- **Locale switching mechanism**: <…>
## State postures ## State postures
One declared posture per state, used everywhere. No inventing on a per-screen basis. One declared posture per state, used everywhere. Per-screen variation
is an exception that needs justification, not a default.
### Empty ### Empty
- **Visual**: <icon + heading + body + optional CTA> - **Visual**: <icon + heading + body + CTA | heading + body | heading only>
- **Tone**: <encouraging | neutral | matter-of-fact> - **Tone**: <encouraging | neutral | matter-of-fact>
- **CTA**: <when present, what kind of action> - **CTA**: <present always | present when actionable | never>
- **Copy length**: keep to 12 sentences; the state itself communicates
### Loading ### Loading
- **Default pattern**: <skeleton | spinner | blocking> - **Default pattern**: <skeleton | spinner | blocking>
- **When to use which**: <criteria — "skeleton for list/card containers, spinner for inline buttons, blocking only for explicit user-initiated actions"> - **By action class**:
- **Threshold for showing loading at all**: <ms — usually 200300ms; below that, just let it pop> - List / card containers: <usually skeleton>
- Inline buttons: <usually spinner-in-button>
- Page-level: <skeleton or blocking>
- **Threshold for showing loading at all**: <ms — usually 200300ms>
### Error ### Error
- **Surface**: <inline | toast | dialog — by error class> - **Surface by class**:
- **Recovery**: <retry button | clear path forward | "contact support" only as last resort> - Recoverable inline (validation): inline beneath the field
- **Copy tone**: <plain language, name what happened, name what to do next> - Recoverable async (network): toast with retry
- Unrecoverable (system): dialog
- **Recovery affordances**: <retry | clear path forward | report>
- **Copy tone**: plain language, name what happened, name what to do next
- **Copy length**: 12 sentences max
- **Never** show stack traces, error codes without explanation, or "An error occurred" without specifics
### Offline ### Offline
- **Behavior**: <feature subset that works | banner only | "go online to continue"> - **Behavior**: <full subset works | banner only | "go online to continue">
- **Sync semantics**: <when reconnected, what happens> - **Indicator**: <persistent banner | transient toast | none>
- **Sync semantics**: <queue + flush on reconnect | fail loudly | not in v1>
- **Conflict resolution on reconnect**: <see Optimistic updates below>
## Time formatting ## Time formatting
- **Relative time** for events within: <threshold — e.g., < 7 days> - **Relative time** for events within: <threshold — e.g., < 7 days>
- **Absolute time** otherwise, format: <pattern — e.g., "MMM d, yyyy h:mm a"> - Pattern: "<3 hours ago>", "<yesterday>", "<last Tuesday>"
- **Time zones**: <user-local | fixed (which?) | configurable> - **Absolute time** otherwise:
- **Date-only vs date+time**: <when each is shown> - Pattern: <`Intl.DateTimeFormat` locale-aware | explicit format>
- Example: <e.g., "Apr 28, 2026 4:30 PM">
- **Date-only vs date+time**: show time when it matters (events, logs); date-only for ages, anniversaries, milestones
- **Time zones**: <user-local | fixed | configurable>
- **Tooltip / hover**: relative-time displays should show absolute on hover
## Keyboard ## Keyboard
- **Global shortcuts**: <list, or "none in v1"> - **Global shortcuts**: <list, or "none in v1">
- **Within-modal**: `Esc` closes; focus trap while open; restore focus on close - **Within-modal**:
- **Tab order**: declared per-flow as needed; focus visible always (per design-system) - `Esc` closes (always)
- **Forms**: `Enter` submits; `Esc` cancels destructive - Focus trapped while open
- **Lists**: <arrow-key nav? | not in v1> - Focus restored to trigger element on close
- **Tab order**: declared per-flow as relevant; focus visible always (per design-system)
- **Forms**:
- `Enter` submits *(or Cmd+Enter for multi-line forms)*
- `Esc` cancels destructive actions only
- **Lists / menus**:
- Arrow-key navigation: <yes | no in v1>
- `Home` / `End`: <yes | no>
- Type-ahead select: <yes | no>
## Optimistic updates & error recovery
- **Default posture**: <optimistic with rollback | pessimistic | mixed>
- **Optimistic by default**: most actions feel fast; rollback on failure
- **Pessimistic by default**: destructive actions, money flows, anything irreversible
- **Mixed strategy** *(if applicable)*:
- Optimistic for: <action classes>
- Pessimistic for: <action classes>
### Rollback strategy
- **UI on failure**: <toast + revert | silent revert | dialog>
- **Retry affordance**: <inline retry button | toast button | none>
- **Stale data detection**: <ETag | timestamp | version>
### Conflict resolution
- **Strategy**: <last-write-wins | show conflict UI | append both | platform-specific>
- **User-visible signaling**: <…>
## Gestures (touch only)
<!-- Skip / remove this section if input model doesn't include touch. -->
- **Swipe actions on rows**: <yes (which actions, which directions) | no>
- **Long-press menu**: <yes | no>
- **Pull-to-refresh**: <yes (where) | no>
- **Pinch-to-zoom**: <yes (where — usually images, maps) | no>
- **Edge-swipe back navigation**: <platform default | overridden>
## Focus management
- **After modal close**: restore focus to trigger element (always)
- **After route change**: <heading focus | container focus | no change>
- **After form error**: focus first error field (always)
- **After successful action that closes UI**: restore to a sensible parent
- **On initial page load**: <do not auto-focus | focus main heading | focus primary CTA>
- **Focus visibility**: always visible per design-system tokens; no `outline: none` without a replacement
## Notification patterns
- **Toasts**: <use cases — confirmations, async results>
- Position: <top-right | bottom-center | platform-default>
- Duration: <ms — usually 40006000>
- Dismissible: yes
- Stacking: <max N | replace>
- **Inline notifications / banners**: <use cases — status, warnings, persistent state>
- **Dialogs**: <use cases — confirmation, error, blocking decisions>
- **Badge counts**: <where shown, how cleared>
## Selection & multi-select
- **Single select**: <pattern>
- **Multi-select**:
- **Trigger**: <long-press (touch), checkbox always visible, shift-click range, etc.>
- **Action bar**: <sticky bottom | top | inline>
- **Bulk action limits**: <none | N items max>
- **Select-all**: <yes (where) | no>
## Drag & drop
- **Use cases**: <reordering | moving between lists | upload | none>
- **Affordance**: <handle-only | row-anywhere | platform-default>
- **Touch support**: <yes (long-press to lift) | desktop-only | none>
- **Live preview**: <yes | no>
## TBD on return ## TBD on return
- <TBD: anything the placeholder pass couldn't answer> <!-- Anything skipped or deferred during the ui-system interview. -->
- <TBD: ...>