@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 +5 -5
- package/adapters/codex/prompts/component-builder.md +6 -5
- package/adapters/codex/prompts/token-sync-layer.md +4 -1
- package/adapters/cursor/.cursor/rules/component-builder.mdc +6 -5
- package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +4 -1
- package/adapters/generic/skills/component-builder/SKILL.md +6 -5
- package/adapters/generic/skills/token-sync-layer/SKILL.md +4 -1
- package/package.json +1 -1
- package/references/figma-component-standards.md +17 -19
- package/references/figma-scripting.md +2 -1
- package/references/state-baseline.md +25 -0
- package/scripts/README.md +2 -1
- package/scripts/build-state-baseline.mjs +69 -0
- package/scripts/lib/component-states.mjs +24 -6
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.
|
|
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.
|
|
118
|
-
npx @radicool/throughline@0.
|
|
119
|
-
npx @radicool/throughline@0.
|
|
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.
|
|
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
|
-
|
|
71
|
-
wherever they apply (loading,
|
|
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
|
|
404
|
-
state
|
|
405
|
-
loading/
|
|
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
|
|
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
|
-
|
|
75
|
-
wherever they apply (loading,
|
|
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
|
|
408
|
-
state
|
|
409
|
-
loading/
|
|
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
|
|
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
|
-
|
|
71
|
-
wherever they apply (loading,
|
|
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
|
|
404
|
-
state
|
|
405
|
-
loading/
|
|
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
|
|
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.
|
|
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
|
|
127
|
-
|
|
128
|
-
states whenever they apply to that component: **loading** (anything
|
|
129
|
-
async work — buttons, submit inputs), **selected**
|
|
130
|
-
list/menu items
|
|
131
|
-
async-result buttons). Decide *which* conditional
|
|
132
|
-
state that does apply to keep the matrix small.
|
|
133
|
-
- **
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 (
|
|
561
|
-
|
|
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
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
`state-incomplete`
|
|
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
|
|
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.
|
|
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).
|
|
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
|
|
8
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
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
|
}
|