@radicool/throughline 0.21.4 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,7 +73,7 @@ Everything above was created during a single working session and synced directly
73
73
 
74
74
  | | |
75
75
  |---|---|
76
- | **A supported agent** | Required — [Claude Code](https://docs.claude.com/en/docs/claude-code) (native plugin), or **Cursor**, **Codex**, or any **AGENTS.md**-aware agent via `npx @radicool/throughline@0.21.4 init`. |
76
+ | **A supported agent** | Required — [Claude Code](https://docs.claude.com/en/docs/claude-code) (native plugin), or **Cursor**, **Codex**, or any **AGENTS.md**-aware agent via `npx @radicool/throughline@0.22.0 init`. |
77
77
  | **Figma** | Required, **desktop app** (the browser version causes connection errors). **Professional plan or higher recommended** — multi-mode variables (Light/Dark, brand themes) need it. |
78
78
  | **Figma access token** | Optional — only for the few Figma calls the Desktop Bridge can't make, like reading a published library. Without it, you work through the bridge alone. The setup skill walks you through it; your token stays yours and is never shared in chat. |
79
79
  | **GitHub** (or similar) | Optional — only when you're ready to graduate to a real remote repo with PRs and CI. |
@@ -114,9 +114,9 @@ Update anytime with `/plugin marketplace update throughline-marketplace`.
114
114
  Run the installer in your project — it stamps in the skills, the reference docs, the scripts, and the Figma MCP config for your tool:
115
115
 
116
116
  ```
117
- npx @radicool/throughline@0.21.4 init --target=cursor # → .cursor/rules + .cursor/mcp.json
118
- npx @radicool/throughline@0.21.4 init --target=codex # → prompts/ + AGENTS.md index + codex-mcp.toml
119
- npx @radicool/throughline@0.21.4 init --target=generic # → skills/ + AGENTS.md index
117
+ npx @radicool/throughline@0.22.0 init --target=cursor # → .cursor/rules + .cursor/mcp.json
118
+ npx @radicool/throughline@0.22.0 init --target=codex # → prompts/ + AGENTS.md index + codex-mcp.toml
119
+ npx @radicool/throughline@0.22.0 init --target=generic # → skills/ + AGENTS.md index
120
120
  ```
121
121
 
122
122
  It's safe to re-run (it merges `AGENTS.md` and `.cursor/mcp.json` non-destructively) and stages everything the skills read into `.throughline/`. Then open the `figma-environment-setup` skill/rule/prompt for your tool to begin. For Codex, add the printed `codex-mcp.toml` block to your Codex config to enable Figma access.
@@ -162,7 +162,7 @@ For the technically curious — how the machine actually runs.
162
162
 
163
163
  **Modes, within Figma's limits.** Light/Dark lives on the semantic collections; brand variants live on the primitive palette. Splitting the two axes across two collections keeps each one under Figma's 4-modes-per-collection cap on the Professional plan while still resolving correctly (`bg/default` → `{gray/50}` → the active brand's gray).
164
164
 
165
- **Model routing.** ThroughLine routes work by *cognition, not guesswork* — the expensive thinking on the best model, the mechanical doing on a cheap one. Setup pins **Haiku** to keep first-run costs low. Inside a skill, the deciding — a token architecture, a variant matrix, a slot contract — is planned once on the strongest tier, while the long mechanical part — placing Figma nodes, transcribing component code, running SVGR — runs on a cheap tier and self-verifies. You get the best model where it matters and a cheap one where it doesn't, without micromanaging it. The recommended mapping is **Haiku → Sonnet → Opus** for the `fast < balanced < deep` tiers, but apart from setup's Haiku pin, nothing is hardcoded — the ladder resolves against whatever models your install actually has, and on hosts without subagents skills degrade cleanly to a single model. You can still steer the session yourself (`/model opus` for the heaviest authoring, back to `/model sonnet` after); nothing forces an expensive model on you.
165
+ **Model routing.** ThroughLine routes work by *cognition, not guesswork* — the expensive thinking on the best model, the mechanical doing on a cheap one. Inside a skill, the deciding — a token architecture, a variant matrix, a slot contract — is planned once on the strongest tier, while the long mechanical part — placing Figma nodes, transcribing component code, running SVGR — runs on a cheap tier and self-verifies. You get the best model where it matters and a cheap one where it doesn't, without micromanaging it. The recommended mapping is **Haiku → Sonnet → Opus** for the `fast < balanced < deep` tiers, but nothing is hardcoded — the ladder resolves against whatever models your install actually has, and on hosts without subagents skills degrade cleanly to a single model. You can still steer the session yourself (`/model opus` for the heaviest authoring, back to `/model sonnet` after); nothing forces an expensive model on you.
166
166
 
167
167
  ## Who it's for
168
168
 
@@ -67,8 +67,9 @@ inconsistent output:
67
67
  - **Sizes** (sm, md, lg). Each size is its **own variant row**, not a state — see
68
68
  the layout law below.
69
69
  - **States** — **include the full relevant set, don't trim it.** The baseline is
70
- default, hover, focus, active (pressed), disabled; add the conditional states
71
- wherever they apply (loading, selected, success/error). Decide which conditional
70
+ the set `.throughline/references/state-baseline.md` owes that component
71
+ (default included); add the conditional states wherever they apply (loading,
72
+ success/error). Decide which conditional
72
73
  states a given component can reach, but never drop a state it genuinely has. See
73
74
  "State handling" in `.throughline/references/figma-component-standards.md`
74
75
  for the per-component checklist.
@@ -400,9 +401,9 @@ Offer next steps: build the code counterparts and stories
400
401
  auto-layout grid where **variants are rows and states are columns**: one row per
401
402
  variant (each `type`, and each `size`, since size is a variant) stepping through
402
403
  states across the columns, size groups stacked vertically (see the standards doc).
403
- - Never ship a component with only its `default` state — include the full relevant
404
- state set (default/hover/focus/active/disabled plus applicable
405
- loading/selected/success/error).
404
+ - Never ship a component with only its `default` state — include every state
405
+ `.throughline/references/state-baseline.md` owes that component, plus
406
+ `default`, plus any applicable loading/success/error.
406
407
  - Never run the component header and the component area together with no division —
407
408
  every doc card segments the header from the component area with a divider line or
408
409
  a distinct header surface.
@@ -140,7 +140,10 @@ fit in the adapter output.
140
140
 
141
141
  ## Step 3 — Set up Style Dictionary + adapters
142
142
 
143
- Install and configure Style Dictionary v4 in `packages/tokens/`. For each chosen
143
+ Install Style Dictionary pinned to an exact version with
144
+ `pnpm add -D style-dictionary@4.4.0` (or the project's package manager) in
145
+ `packages/tokens/`; the generated config is verified against 4.4.0, and a newer
146
+ release can rename or add stock transforms. Then configure it. For each chosen
144
147
  platform, apply its adapter preset (per `.throughline/references/sync-adapters.md`):
145
148
  register the platform, transform group, format, and `outputReferences`
146
149
  (true for web → preserves the semantic→primitive cascade; false for native →
@@ -71,8 +71,9 @@ inconsistent output:
71
71
  - **Sizes** (sm, md, lg). Each size is its **own variant row**, not a state — see
72
72
  the layout law below.
73
73
  - **States** — **include the full relevant set, don't trim it.** The baseline is
74
- default, hover, focus, active (pressed), disabled; add the conditional states
75
- wherever they apply (loading, selected, success/error). Decide which conditional
74
+ the set `.throughline/references/state-baseline.md` owes that component
75
+ (default included); add the conditional states wherever they apply (loading,
76
+ success/error). Decide which conditional
76
77
  states a given component can reach, but never drop a state it genuinely has. See
77
78
  "State handling" in `.throughline/references/figma-component-standards.md`
78
79
  for the per-component checklist.
@@ -404,9 +405,9 @@ Offer next steps: build the code counterparts and stories
404
405
  auto-layout grid where **variants are rows and states are columns**: one row per
405
406
  variant (each `type`, and each `size`, since size is a variant) stepping through
406
407
  states across the columns, size groups stacked vertically (see the standards doc).
407
- - Never ship a component with only its `default` state — include the full relevant
408
- state set (default/hover/focus/active/disabled plus applicable
409
- loading/selected/success/error).
408
+ - Never ship a component with only its `default` state — include every state
409
+ `.throughline/references/state-baseline.md` owes that component, plus
410
+ `default`, plus any applicable loading/success/error.
410
411
  - Never run the component header and the component area together with no division —
411
412
  every doc card segments the header from the component area with a divider line or
412
413
  a distinct header surface.
@@ -144,7 +144,10 @@ fit in the adapter output.
144
144
 
145
145
  ## Step 3 — Set up Style Dictionary + adapters
146
146
 
147
- Install and configure Style Dictionary v4 in `packages/tokens/`. For each chosen
147
+ Install Style Dictionary pinned to an exact version with
148
+ `pnpm add -D style-dictionary@4.4.0` (or the project's package manager) in
149
+ `packages/tokens/`; the generated config is verified against 4.4.0, and a newer
150
+ release can rename or add stock transforms. Then configure it. For each chosen
148
151
  platform, apply its adapter preset (per `.throughline/references/sync-adapters.md`):
149
152
  register the platform, transform group, format, and `outputReferences`
150
153
  (true for web → preserves the semantic→primitive cascade; false for native →
@@ -67,8 +67,9 @@ inconsistent output:
67
67
  - **Sizes** (sm, md, lg). Each size is its **own variant row**, not a state — see
68
68
  the layout law below.
69
69
  - **States** — **include the full relevant set, don't trim it.** The baseline is
70
- default, hover, focus, active (pressed), disabled; add the conditional states
71
- wherever they apply (loading, selected, success/error). Decide which conditional
70
+ the set `.throughline/references/state-baseline.md` owes that component
71
+ (default included); add the conditional states wherever they apply (loading,
72
+ success/error). Decide which conditional
72
73
  states a given component can reach, but never drop a state it genuinely has. See
73
74
  "State handling" in `.throughline/references/figma-component-standards.md`
74
75
  for the per-component checklist.
@@ -400,9 +401,9 @@ Offer next steps: build the code counterparts and stories
400
401
  auto-layout grid where **variants are rows and states are columns**: one row per
401
402
  variant (each `type`, and each `size`, since size is a variant) stepping through
402
403
  states across the columns, size groups stacked vertically (see the standards doc).
403
- - Never ship a component with only its `default` state — include the full relevant
404
- state set (default/hover/focus/active/disabled plus applicable
405
- loading/selected/success/error).
404
+ - Never ship a component with only its `default` state — include every state
405
+ `.throughline/references/state-baseline.md` owes that component, plus
406
+ `default`, plus any applicable loading/success/error.
406
407
  - Never run the component header and the component area together with no division —
407
408
  every doc card segments the header from the component area with a divider line or
408
409
  a distinct header surface.
@@ -140,7 +140,10 @@ fit in the adapter output.
140
140
 
141
141
  ## Step 3 — Set up Style Dictionary + adapters
142
142
 
143
- Install and configure Style Dictionary v4 in `packages/tokens/`. For each chosen
143
+ Install Style Dictionary pinned to an exact version with
144
+ `pnpm add -D style-dictionary@4.4.0` (or the project's package manager) in
145
+ `packages/tokens/`; the generated config is verified against 4.4.0, and a newer
146
+ release can rename or add stock transforms. Then configure it. For each chosen
144
147
  platform, apply its adapter preset (per `.throughline/references/sync-adapters.md`):
145
148
  register the platform, transform group, format, and `outputReferences`
146
149
  (true for web → preserves the semantic→primitive cascade; false for native →
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radicool/throughline",
3
- "version": "0.21.4",
3
+ "version": "0.22.0",
4
4
  "description": "Build a complete design system end to end — author in Figma, sync tokens to code, generate Storybook. Usable from Claude Code, Cursor, Codex, or any AGENTS.md agent.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -123,19 +123,17 @@ run, and it mirrors how the states/types map to code props.
123
123
 
124
124
  - **Always include every relevant state for the component — completeness is the
125
125
  default, not a judgment call.** A component's `state` axis must enumerate its full
126
- applicable interaction surface, not just `default`. The baseline interactive set
127
- is **default, hover, focus, active (pressed), disabled**; add the **conditional**
128
- states whenever they apply to that component: **loading** (anything that triggers
129
- async work — buttons, submit inputs), **selected** (toggles, segmented controls,
130
- list/menu items, chips), and **success / error** (validated inputs, form fields,
131
- async-result buttons). Decide *which* conditional states apply, but never drop a
132
- state that does apply to keep the matrix small.
133
- - **Button:** default, hover, focus, active (pressed), disabled — plus loading
134
- (and selected / success / error where the button supports them).
135
- - **Input / text field:** default, hover, focus, disabled — plus error, success,
136
- and (where async) loading.
137
- - **Checkbox / radio / toggle / chip:** default, hover, focus, active, disabled —
138
- plus selected (and indeterminate where it applies).
126
+ applicable interaction surface, not just `default`. The states a component owes
127
+ come from the table in `references/state-baseline.md` (see below); add the
128
+ **conditional** states whenever they apply to that component: **loading** (anything
129
+ that triggers async work — buttons, submit inputs), **selected** for components the
130
+ table does not cover (segmented controls, list/menu items), and **success / error**
131
+ (validated inputs, form fields, async-result buttons). Decide *which* conditional
132
+ states apply, but never drop a state that does apply to keep the matrix small.
133
+ - **Which components owe which states is one table**, shared with the CLI's
134
+ `state-incomplete` check: `references/state-baseline.md`, generated from
135
+ `scripts/lib/component-states.mjs`. Add loading, success / error, and
136
+ indeterminate where they apply; those are not in the table.
139
137
  - Keep these on a `state` variant axis where they change appearance meaningfully;
140
138
  each becomes a column in the set per "Component set arrangement".
141
139
  - Distinguish *component states* (part of the component's definition) from
@@ -557,8 +555,8 @@ For each generated artboard / doc card / icon grid, read the nodes back (via
557
555
  a header container whose surface fill differs from the component area (both
558
556
  token-bound). A card with no header/component segmentation is a fail.
559
557
  8. **States complete** — each component set's `state` axis includes every relevant
560
- state for that component (default/hover/focus/active/disabled plus the applicable
561
- conditional states — loading/selected/success/error), with variants (incl. each
558
+ state for that component (every state the table in `references/state-baseline.md`
559
+ owes it, plus default, plus the applicable loading/success/error), with variants (incl. each
562
560
  size) as rows and states as columns. A set shipping only `default` is a fail.
563
561
  9. **Focus state matches the library idiom** — the focus state is built as
564
562
  `project.uiFramework`'s real pattern (see "State handling"), not a house-style
@@ -581,10 +579,10 @@ For each generated artboard / doc card / icon grid, read the nodes back (via
581
579
  error: fix and re-audit like any other item. If the ring is still missing
582
580
  when the pass budget below runs out, **stop the build** rather than hand off.
583
581
  - **Scope.** It runs only for a component whose name folds (lowercase,
584
- punctuation stripped) to a Button, an Input / text field, or a Checkbox /
585
- radio / toggle / chip, the same exact-name match as the state check. A Switch
586
- or a TextInput isn't in that list, so neither Figma check reads it (the CLI's
587
- `state-incomplete` does). It also
582
+ punctuation stripped) to a name in the table in `references/state-baseline.md`
583
+ (not its exempt list), the same
584
+ exact-name match as the state check and the same list the CLI's
585
+ `state-incomplete` reads. It also
588
586
  needs `project.uiFramework` to be exactly `shadcn`, `tailwind`, `mui`,
589
587
  `vanilla-css` or `vanilla`, or absent or `null`. Anything else (`ios-swift`,
590
588
  a tier-2 library, any other value) is skipped, and the skip is said.
@@ -68,7 +68,8 @@ wrong document.
68
68
  Run this before the first write, and again right before any replace — the
69
69
  build-verify-then-replace finalize deletes the existing node, so it is the write
70
70
  that destroys work in the wrong file. The `figma-executor` agent runs it as step 1
71
- of its contract. **When a skill builds inline instead** (a host with no subagent
71
+ of its contract; the `architect` and `reviewer` agents, which read Figma but cannot
72
+ navigate, run the compare and return `BLOCKED` on a mismatch. **When a skill builds inline instead** (a host with no subagent
72
73
  dispatch, or a write-back the skill does itself), nothing runs it for you: the
73
74
  skill has to.
74
75
 
@@ -0,0 +1,25 @@
1
+ <!-- GENERATED by scripts/build-state-baseline.mjs from scripts/lib/component-states.mjs. Do not edit; run: node scripts/build-state-baseline.mjs -->
2
+ # State baseline
3
+
4
+ Which interaction states a component owes. This is the one list both checks read:
5
+ `verify:check` (`state-incomplete`) reads it from `scripts/lib/component-states.mjs`,
6
+ and `figma-executor` (its state check and the `focus-indicator` check) reads
7
+ this table. CI fails when the table is stale.
8
+
9
+ A component name matches a row when it folds (lowercase, punctuation and spaces
10
+ stripped) to exactly one of that row's names. `Text Field` folds to `textfield`.
11
+ Matching is exact on purpose: `IconButton` matches nothing, and a component that
12
+ matches nothing owes no states.
13
+
14
+ | Archetype | Names | States owed |
15
+ |---|---|---|
16
+ | button | `button` | `hover`, `focus`, `active`, `disabled` |
17
+ | input | `input`, `textfield`, `textinput` | `hover`, `focus`, `disabled` |
18
+ | choice | `checkbox`, `radio`, `toggle`, `switch`, `chip` | `hover`, `focus`, `active`, `selected`, `disabled` |
19
+
20
+ Every state in the table must be a value on the component's `state` axis. On the
21
+ Figma side the matrix also carries `default` as the resting state; the CLI does not
22
+ count it. `checked` counts as `selected`, and `on` counts as `selected`, since that is what shadcn and Radix call it. Conditional states (`loading`, `error`, `success`) are not in the table:
23
+ whether one applies is a design judgment.
24
+
25
+ Names that fold to `card`, `modal`, `dialog`, `badge`, `tag` owe none, so neither check asserts states on them.
package/scripts/README.md CHANGED
@@ -29,6 +29,7 @@ tested here; copied verbatim by `token-crosswalk-builder` into the user's
29
29
  | `lib/component-states.mjs` | The archetype table and resolver behind `state-incomplete` — which baseline interaction states each kind of component owes, and when to abstain. `validate-adherence.mjs` also uses it to find Badges. | copied alongside `verify-check.mjs` and `validate-adherence.mjs` |
30
30
  | `lib/contrast.mjs` | The WCAG contrast maths and the semantic colour pairs behind `color-contrast` — which text role must clear which surface, in every mode, and where the rule deliberately abstains. | copied alongside `verify-check.mjs` |
31
31
  | `build-doc-card-builder.mjs` | Generate `references/doc-card-builder.md` from the planner + the Figma renderer template (`lib/doc-card-render.figma.js`). `--check` gates CI. | plugin-internal (not installed) |
32
+ | `build-state-baseline.mjs` | Generate `references/state-baseline.md`, the table of which components owe which interaction states, from `lib/component-states.mjs`. The Figma executor reads the table; `verify:check` reads the module. `--check` gates CI. | plugin-internal (not installed) |
32
33
  | `build-native-adapter-config.mjs` | Generate `references/native-adapter-config.md` by slicing `lib/sd-native.mjs` on its `@doc-section` markers and interleaving each fragment under its prose. Fails when module code falls outside every section, so the doc cannot silently ship incomplete. `--check` gates CI. | plugin-internal (not installed) |
33
34
 
34
35
  **Documentation scripts — install as a set.** Copying these files without
@@ -245,7 +246,7 @@ the committed `adapters/<target>/` tree plus the runtime payload
245
246
  (`references/` + `scripts/`, minus `scripts/adapters/`) into `.throughline/`,
246
247
  rewriting `${CLAUDE_PLUGIN_ROOT}` → `.throughline`:
247
248
 
248
- npx @radicool/throughline@0.21.4 init --target=cursor|codex|generic
249
+ npx @radicool/throughline@0.22.0 init --target=cursor|codex|generic
249
250
 
250
251
  See `scripts/install.mjs` (pure core + CLI + `install.test.mjs`).
251
252
 
@@ -0,0 +1,69 @@
1
+ // Generates references/state-baseline.md from scripts/lib/component-states.mjs.
2
+ // The table is the one list of which components owe which interaction states:
3
+ // `verify:check` reads the module, and `figma-executor` (which cannot import
4
+ // JavaScript) reads this rendering. Mirrors build-native-adapter-config.mjs:
5
+ // run bare to write, --check to gate CI. Zero dependencies.
6
+ import { readFileSync, writeFileSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
+ import { ARCHETYPES, BASELINE_STATES, NAME_SYNONYMS, STATE_SYNONYMS } from './lib/component-states.mjs';
10
+
11
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
12
+ export const OUT = join(REPO_ROOT, 'references', 'state-baseline.md');
13
+
14
+ export function render(states = BASELINE_STATES, synonyms = NAME_SYNONYMS, archetypes = ARCHETYPES, stateSynonyms = STATE_SYNONYMS) {
15
+ const aliases = Object.keys(stateSynonyms).map((s) => `\`${s}\` counts as \`${stateSynonyms[s]}\``).join(', and ');
16
+ const owing = archetypes.filter((a) => (states[a] ?? []).length > 0);
17
+ const rows = owing.map((a) => {
18
+ const names = Object.keys(synonyms).filter((n) => synonyms[n] === a);
19
+ return `| ${a} | ${names.map((n) => `\`${n}\``).join(', ')} | ${states[a].map((s) => `\`${s}\``).join(', ')} |`;
20
+ });
21
+ const exempt = archetypes.filter((a) => !owing.includes(a) && a !== 'other');
22
+ const exemptNames = exempt.flatMap((a) => Object.keys(synonyms).filter((n) => synonyms[n] === a));
23
+ return `<!-- GENERATED by scripts/build-state-baseline.mjs from scripts/lib/component-states.mjs. Do not edit; run: node scripts/build-state-baseline.mjs -->
24
+ # State baseline
25
+
26
+ Which interaction states a component owes. This is the one list both checks read:
27
+ \`verify:check\` (\`state-incomplete\`) reads it from \`scripts/lib/component-states.mjs\`,
28
+ and \`figma-executor\` (its state check and the \`focus-indicator\` check) reads
29
+ this table. CI fails when the table is stale.
30
+
31
+ A component name matches a row when it folds (lowercase, punctuation and spaces
32
+ stripped) to exactly one of that row's names. \`Text Field\` folds to \`textfield\`.
33
+ Matching is exact on purpose: \`IconButton\` matches nothing, and a component that
34
+ matches nothing owes no states.
35
+
36
+ | Archetype | Names | States owed |
37
+ |---|---|---|
38
+ ${rows.join('\n')}
39
+
40
+ Every state in the table must be a value on the component's \`state\` axis. On the
41
+ Figma side the matrix also carries \`default\` as the resting state; the CLI does not
42
+ count it. ${aliases}, since that is what shadcn and Radix call it. Conditional states (\`loading\`, \`error\`, \`success\`) are not in the table:
43
+ whether one applies is a design judgment.
44
+
45
+ Names that fold to ${exemptNames.map((n) => `\`${n}\``).join(', ')} owe none, so neither check asserts states on them.
46
+ `;
47
+ }
48
+
49
+ function main() {
50
+ const check = process.argv.includes('--check');
51
+ const rendered = render();
52
+ if (!check) {
53
+ writeFileSync(OUT, rendered);
54
+ console.log(`wrote ${OUT}`);
55
+ return;
56
+ }
57
+ const current = readFileSync(OUT, 'utf8');
58
+ if (current === rendered) {
59
+ console.log('references/state-baseline.md is up to date');
60
+ return;
61
+ }
62
+ console.error(
63
+ 'references/state-baseline.md is stale.\n' +
64
+ 'Run: node scripts/build-state-baseline.mjs',
65
+ );
66
+ process.exit(1);
67
+ }
68
+
69
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
@@ -1,13 +1,18 @@
1
1
  // The archetype baseline for `state-incomplete` (`docs/specs/2026-09-12-verification-proof-bundle.md`,
2
- // step 3). Source of truth for the prose is `references/figma-component-standards.md:126-138`.
2
+ // step 3). The prose in `references/figma-component-standards.md` (State handling) points at
3
+ // `references/state-baseline.md`, which is generated from this file.
3
4
  //
4
5
  // `default` is deliberately excluded from every baseline below: documenting the
5
6
  // resting state is not documentation, and the Figma matrix requirement for it is
6
7
  // the executor's, not this table's. Conditional states (`loading`, `error`,
7
- // `success`, `selected`) are deliberately excluded too — whether one applies to a
8
- // given component is a design judgment, not something a baseline can assert.
8
+ // `success`) are deliberately excluded too — whether one applies to a given
9
+ // component is a design judgment, not something a baseline can assert. `selected`
10
+ // is not conditional for a choice control: a checkbox, radio, toggle, switch or
11
+ // chip that cannot be selected is not one.
9
12
  //
10
- // MAINTENANCE: a change to the prose baseline must change this table.
13
+ // This table is the single source for both checks. `scripts/build-state-baseline.mjs`
14
+ // renders it into `references/state-baseline.md`, which the Figma executor reads;
15
+ // CI fails when that file is stale. MAINTENANCE: change this table, then run it.
11
16
 
12
17
  import { normalizeName } from './source-scan.mjs';
13
18
 
@@ -16,7 +21,7 @@ export const ARCHETYPES = ['button', 'input', 'choice', 'card', 'modal', 'badge'
16
21
  export const BASELINE_STATES = {
17
22
  button: ['hover', 'focus', 'active', 'disabled'],
18
23
  input: ['hover', 'focus', 'disabled'],
19
- choice: ['hover', 'focus', 'active', 'disabled'],
24
+ choice: ['hover', 'focus', 'active', 'selected', 'disabled'],
20
25
  card: [],
21
26
  modal: [],
22
27
  badge: [],
@@ -40,6 +45,14 @@ export const NAME_SYNONYMS = {
40
45
  tag: 'badge',
41
46
  };
42
47
 
48
+ // Other names a recorded state goes by. shadcn and Radix call a checked checkbox
49
+ // `checked` and a pressed-in toggle `on`; both are `selected` here. `pressed` is
50
+ // not one: the standards use it for `active`.
51
+ export const STATE_SYNONYMS = {
52
+ checked: 'selected',
53
+ on: 'selected',
54
+ };
55
+
43
56
  export function resolveArchetype(record) {
44
57
  if (ARCHETYPES.includes(record.archetype)) return record.archetype;
45
58
  const bySynonym = NAME_SYNONYMS[normalizeName(record.name)];
@@ -51,6 +64,11 @@ export function missingStates(record) {
51
64
  if (archetype === null) return [];
52
65
  const baseline = BASELINE_STATES[archetype];
53
66
  if (baseline.length === 0) return [];
54
- const recordedKeys = new Set(Object.keys(record.states ?? {}).map(normalizeName));
67
+ const recordedKeys = new Set(
68
+ Object.keys(record.states ?? {}).map((key) => {
69
+ const name = normalizeName(key);
70
+ return STATE_SYNONYMS[name] ?? name;
71
+ }),
72
+ );
55
73
  return baseline.filter((state) => !recordedKeys.has(normalizeName(state)));
56
74
  }