@radicool/throughline 0.25.0 → 0.27.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.
Files changed (44) hide show
  1. package/README.md +8 -4
  2. package/adapters/codex/AGENTS.md +2 -0
  3. package/adapters/codex/prompts/component-builder.md +5 -3
  4. package/adapters/codex/prompts/compose-code.md +78 -0
  5. package/adapters/codex/prompts/compose-figma.md +59 -0
  6. package/adapters/codex/prompts/document-component.md +3 -2
  7. package/adapters/cursor/.cursor/commands/compose-code.md +78 -0
  8. package/adapters/cursor/.cursor/commands/compose-figma.md +59 -0
  9. package/adapters/cursor/.cursor/commands/document-component.md +3 -2
  10. package/adapters/cursor/.cursor/rules/component-builder.mdc +5 -3
  11. package/adapters/generic/AGENTS.md +2 -0
  12. package/adapters/generic/commands/compose-code.md +78 -0
  13. package/adapters/generic/commands/compose-figma.md +59 -0
  14. package/adapters/generic/commands/document-component.md +3 -2
  15. package/adapters/generic/skills/component-builder/SKILL.md +5 -3
  16. package/package.json +1 -1
  17. package/references/doc-card-builder.md +62 -15
  18. package/references/figma-adherence.md +21 -0
  19. package/references/figma-compose.md +896 -0
  20. package/scripts/README.md +8 -4
  21. package/scripts/build-doc-card-builder.mjs +16 -0
  22. package/scripts/build-figma-checks.mjs +89 -2
  23. package/scripts/compose-code.mjs +271 -0
  24. package/scripts/doc-card-payload.mjs +115 -0
  25. package/scripts/install.mjs +5 -3
  26. package/scripts/lib/doc-card-plan.mjs +37 -0
  27. package/scripts/lib/doc-card-render.figma.js +9 -15
  28. package/scripts/lib/figma-adherence-read.figma.js +21 -0
  29. package/scripts/lib/figma-compose.figma.js +326 -0
  30. package/scripts/lib/figma-compose.mjs +317 -0
  31. package/scripts/lib/fixtures/figma-compose/casing-and-suffix.json +8 -0
  32. package/scripts/lib/fixtures/figma-compose/fill-in-unlaid-slot.json +7 -0
  33. package/scripts/lib/fixtures/figma-compose/hug-on-fixed-component.json +34 -0
  34. package/scripts/lib/fixtures/figma-compose/inventory.json +295 -0
  35. package/scripts/lib/fixtures/figma-compose/many-errors.json +8 -0
  36. package/scripts/lib/fixtures/figma-compose/missing-component.json +7 -0
  37. package/scripts/lib/fixtures/figma-compose/name-taken.json +6 -0
  38. package/scripts/lib/fixtures/figma-compose/padding-sides.json +23 -0
  39. package/scripts/lib/fixtures/figma-compose/radius-kind.json +27 -0
  40. package/scripts/lib/fixtures/figma-compose/raw-number.json +5 -0
  41. package/scripts/lib/fixtures/figma-compose/sign-in-ok.json +16 -0
  42. package/scripts/lib/fixtures/figma-compose/slot-ok.json +9 -0
  43. package/scripts/lib/fixtures/figma-compose/unknown-prop-and-option.json +7 -0
  44. package/scripts/lib/fixtures/figma-compose/wrong-kind.json +5 -0
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.25.0 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.27.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. |
@@ -109,6 +109,10 @@ This is the reliable entry point — it runs environment setup first, ahead of a
109
109
 
110
110
  Already have screens or components in Figma? `/throughline:check-figma` checks them against the system without touching the file: it flags a component someone detached, and a colour, gap, padding or radius that's hardcoded when a token already has that value. It also runs the state, focus-ring and focus-contrast checks over the components already in your library.
111
111
 
112
+ Need a new screen? `/throughline:compose-figma` builds it in Figma from your own components and tokens, every value bound. If the screen needs something your system doesn't have, it says so instead of drawing a lookalike, and it runs the same check before handing the screen over.
113
+
114
+ Want it in code instead? `/throughline:compose-code` writes the screen as one React file in your app, using only your components and your tokens. It checks the plan before writing a line, reports any component you don't have rather than building one, and gates the file before handing it over.
115
+
112
116
  Update anytime with `/plugin marketplace update throughline-marketplace`.
113
117
 
114
118
  #### Cursor, Codex, or a generic AGENTS.md agent
@@ -116,9 +120,9 @@ Update anytime with `/plugin marketplace update throughline-marketplace`.
116
120
  Run the installer in your project — it stamps in the skills, the reference docs, the scripts, and the Figma MCP config for your tool:
117
121
 
118
122
  ```
119
- npx @radicool/throughline@0.25.0 init --target=cursor # → .cursor/rules + .cursor/mcp.json
120
- npx @radicool/throughline@0.25.0 init --target=codex # → prompts/ + AGENTS.md index + codex-mcp.toml
121
- npx @radicool/throughline@0.25.0 init --target=generic # → skills/ + AGENTS.md index
123
+ npx @radicool/throughline@0.27.0 init --target=cursor # → .cursor/rules + .cursor/mcp.json
124
+ npx @radicool/throughline@0.27.0 init --target=codex # → prompts/ + AGENTS.md index + codex-mcp.toml
125
+ npx @radicool/throughline@0.27.0 init --target=generic # → skills/ + AGENTS.md index
122
126
  ```
123
127
 
124
128
  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.
@@ -20,6 +20,8 @@ ThroughLine builds a design system end to end. Load the matching prompt for the
20
20
  ## ThroughLine commands
21
21
 
22
22
  - `check-figma` — Check what's already in Figma against the design system, read-only — screens for detached components and raw values a token has, and existing component sets for their states, focus ring and focus contrast. → load `prompts/check-figma.md`.
23
+ - `compose-code` — Compose a screen in code from the design system's own components and tokens — plan checked before writing, every value a token, gated before it's handed over. → load `prompts/compose-code.md`.
24
+ - `compose-figma` — Compose a screen in Figma from the design system's own components, variables and text styles — every value bound, nothing invented, gated before it's handed over. → load `prompts/compose-figma.md`.
23
25
  - `design-system-status` — Show a plain-language summary of the current design system state — what's set up, what's not, and sensible next steps — read from design-system.json. → load `prompts/design-system-status.md`.
24
26
  - `document-component` — Author, refresh, or reconcile the usage documentation for one existing component — draft its canonical doc record from four sources, project it to Figma, the doc card, and code, and resolve any drift via a reviewable per-item choice. → load `prompts/document-component.md`.
25
27
  - `new-component` — Build a single new component end to end — in Figma, then sync any new tokens, then build its code component and stories — with a confirmation between each stage. → load `prompts/new-component.md`.
@@ -327,9 +327,11 @@ reference register, not this skill's guide voice.
327
327
  always last.
328
328
  - **Doc card body.** Render the card's `Usage` band with the canonical builder
329
329
  snippet in `.throughline/references/doc-card-builder.md` (via
330
- `figma_execute` with an explicit `timeout`, one card per call): fill the
331
- RECORD/CANONICAL_FP slots, resolve the nine required semantic variables and
332
- the `Body/Default` text style per that file's call contract, run it, and
330
+ `figma_execute` with an explicit `timeout`, one card per call): assemble the
331
+ call with `node .throughline/scripts/doc-card-payload.mjs <record> <cardNodeId>`
332
+ (it fills the RECORD/CANONICAL_FP slots and resolves the nine semantic
333
+ variables and `Body/Default` per that file's call contract), run it, check
334
+ `summary.renderHash` against the `expectedRenderHash` it printed, and
333
335
  verify the returned summary (`rowsRendered`, `blocksCreated`, `cardWidth`) —
334
336
  not a screenshot. The builder creates the `Doc Fingerprint` node itself and
335
337
  is the only thing that may build the usage body — never hand-assemble it.
@@ -0,0 +1,78 @@
1
+ Compose the screen the user asked for as one React file in their app, from what
2
+ the design system already has. Use the settings already in `design-system.json`
3
+ rather than re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the framework.** Read `project.uiFramework` from `design-system.json`.
6
+ Composing in code covers `shadcn` and `tailwind`. For anything else, stop and
7
+ say composing in code doesn't cover that framework yet.
8
+ 2. **Find the gate settings.** Search the repo's `package.json` files (not under
9
+ `node_modules`) for an `adherence:check` script. If one exists, take its
10
+ `--system`, `--package` and every `--tokens`, each resolved against the
11
+ directory of the `package.json` that holds it. Otherwise ask the user for the
12
+ package specifier the app imports the system from and the token source files,
13
+ and use the repo root as `--system`. Pass every token file: a system whose
14
+ values span modes has one per mode.
15
+ 3. **Know the target.** Unless the user named it, ask for the screen's name and
16
+ the file path to write it to, inside the app. Write `.tsx`, or `.jsx` when the
17
+ app has no TypeScript.
18
+ 4. **Read what exists.** Run
19
+ `node .throughline/scripts/compose-code.mjs inventory --system <dir> --package <spec>`.
20
+ Its components, with their variant axes, values, states, `whenToUse` and
21
+ `whenNotToUse`, are the only components the screen may use. Read a listed
22
+ component's own source for its prop names and exported sub-parts
23
+ (`CardHeader`); the inventory records neither. Find it by resolving the
24
+ package specifier: a workspace package in a monorepo, or the app's
25
+ `node_modules` otherwise. Token variable names come from
26
+ the system's generated CSS (the file its styles import), and the utilities
27
+ from its Tailwind preset; read both.
28
+ 5. **Plan.** Write the plan as JSON to a scratch file:
29
+ `{ "screen", "file", "components": [{ "name", "variants": { "<axis>": "<value>" } }] }`,
30
+ one entry per component the screen uses, with the variant values it sets. A
31
+ sub-part (`CardHeader`) comes with its owner (`Card`) and needs no entry. When
32
+ the request needs a component the inventory doesn't have, say so before
33
+ writing and ask: leave it out, use a named existing component instead, or stop.
34
+ The `nearest` names are matched on spelling; suggest the alternative that fits
35
+ by `whenToUse` instead when one does. Never build a lookalike. An icon, or
36
+ anything else the system's package doesn't export, is a gap in the same way:
37
+ the screen imports only from the package and React.
38
+ 6. **Check the plan.** Run the script with `check --system <dir> --package <spec> --plan <plan file>`
39
+ from the repo root. If it returns errors, nothing has been written: fix the
40
+ plan (a `missing-component` error goes back to the user as in step 5) and check
41
+ again. A `name-taken` error means the file at that path wasn't composed for
42
+ this screen: ask for another path, or for the user to move that file. A
43
+ re-run replaces an earlier composed file only when the screen name matches
44
+ its marker exactly, so reuse the name as written. Write
45
+ the variant values in the casing the returned plan uses.
46
+ 7. **Write.** Put the marker `// Composed by throughline: <screen>.` on the first
47
+ line, or on the second after a `'use client'` directive. Import components only
48
+ from the package specifier. Plain elements carry layout only (flex, grid, gap,
49
+ padding, sizing); visible text uses semantic elements (`h1`, `p`, `label`);
50
+ everything the user operates is a system component from the plan (a link may
51
+ be a plain `<a>`). Write colour, spacing, sizing, type size and font weight
52
+ only as token variables
53
+ (`gap-[var(--space-gap-md)]`, `style={{ padding: 'var(--space-4)' }}`) or the
54
+ utilities the system's preset generates from tokens (`bg-primary`,
55
+ `rounded-md`). Never a raw value, and never Tailwind's default scale
56
+ (`gap-4`, `text-lg`, `font-semibold`). Percentages, viewport units,
57
+ fractions (`w-1/2`) and keywords (`w-full`) are layout and fine. The gate
58
+ reads class names anywhere in the file, so visible copy that spells one
59
+ (`text-sm`) fails too; reword the copy rather than the rule. The gate can't
60
+ see a lookalike built from plain elements (a `div` with `role="switch"`), so
61
+ that one is yours to refuse. A size no token holds (a 400px card) can't be bound:
62
+ use a fraction or a keyword and say so in the report. Handlers are stubs; data and state are the app's.
63
+ 8. **Gate.** Run the script with
64
+ `gate --system <dir> --package <spec> --plan <plan file> --file <screen file>`,
65
+ with `--tokens <file>` repeated once per token file.
66
+ On a failure, fix the file and run it again, up to three attempts in all. If
67
+ the third still fails, stop: the file stays where it is, marked, and the
68
+ report lists every failure and says a re-run replaces it. Never change the
69
+ plan or skip a rule to make it pass.
70
+ 9. **Report.** The file's path, whether it replaced an earlier composed one, the
71
+ gate's counts, every gap (each component asked for that doesn't exist, with the
72
+ nearest names the script offered, and what was left out because of it), and
73
+ how to mount the screen where the app picks its screens (its router or entry).
74
+ Don't edit it.
75
+
76
+ Never edit a component, a token or the app's routing. If the screen can't be
77
+ built from what the system has, the gap is the finding.
78
+
@@ -0,0 +1,59 @@
1
+ Compose the screen the user asked for in Figma, from what the file already has.
2
+ Use the settings already in `design-system.json` (`figma.fileKey`) rather than
3
+ re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
6
+ absent, stop and ask which file to compose in.
7
+ 2. **Confirm the active file.** Run the preflight in
8
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
9
+ file is your target") against that fileKey, and pin it with `figma_navigate`
10
+ (`lock: true`) for the run. Unpin when done.
11
+ 3. **Read what exists.** Unless the user named them, ask which page holds the
12
+ component sets and which page the screen goes on (it's created if missing). Run
13
+ the snippet in `.throughline/references/figma-compose.md` verbatim, called
14
+ as its "How to call it" section says (`MODE` and `SPEC` declared above it,
15
+ `return await runCompose(MODE, SPEC);` below it), with `MODE = 'inventory'` and
16
+ `SPEC = { componentsPage }`. If the plan will set an instance-swap property (an
17
+ icon, say), ask which page holds the swap targets and run `inventory` again with
18
+ `iconsPage`.
19
+ 4. **Plan.** Write the screen description (its shape is the `DESCRIPTION` comment
20
+ in the snippet, at the top of the resolver) using only names from the
21
+ inventory's summary: component sets and their properties and options, variables
22
+ by kind, text styles, modes. Leave `sizing` off an instance unless it should
23
+ fill; `hug` works only on a set whose summary says `autoLayout: true`. Frames
24
+ carry layout only (auto layout, gap, padding, fill, stroke, radius); everything
25
+ the user sees is a component instance or styled text. When the request needs a
26
+ component the inventory doesn't have, say so before building and ask: leave it
27
+ out, use a named existing component instead, or stop. Never draw a lookalike
28
+ from frames.
29
+ 5. **Build.** Run the snippet with `MODE = 'build'` and
30
+ `SPEC = { componentsPage, iconsPage, description, fileKey }`. If it returns
31
+ errors, nothing was written: fix the description (a `missing-component` error
32
+ goes back to the user as in step 4) and build again. A `name-taken` error on
33
+ `<screen>` means a screen a person made already has that name: ask for another
34
+ name. On `WIP: <screen>` it means an unmarked frame holds the working name: ask
35
+ the user to rename or remove it. If the call throws instead, the build rolled
36
+ itself back and nothing was written: stop, and report the error as a composer
37
+ bug rather than reworking the screen around it.
38
+ 6. **Gate.** Run the screen gate in
39
+ `.throughline/references/figma-adherence.md` verbatim with
40
+ `TARGET = { page, screens: ['WIP: <screen>'], fileKey }` and `SKIP = []`. If it
41
+ fails, stop: report its failures as a composer bug, with their links, and leave
42
+ the `WIP:` frame for inspection. Don't change the description to make it pass.
43
+ Remove that frame with `MODE = 'discard'` and `SPEC = { page, screen }` only
44
+ when the user asks.
45
+ 7. **Finalize.** Run the snippet with `MODE = 'finalize'` and
46
+ `SPEC = { page, screen, fileKey }`. It replaces an earlier screen of that name
47
+ only if throughline composed it.
48
+ 8. **Review.** Dispatch the `reviewer` agent in Figma-visual mode with the screen's
49
+ node id (from finalize), the fileKey, the project's working directory (where
50
+ `design-system.json` is) and the user's request as its spec, unless the host
51
+ can't dispatch agents, in which case screenshot the screen yourself and say so.
52
+ 9. **Report.** The screen's link, whether it replaced an earlier one, the gate's
53
+ counts, the reviewer's verdict, and every gap: each component that was asked
54
+ for and doesn't exist, with the nearest names the script offered, and anything
55
+ left out of the request because of it.
56
+
57
+ Never edit a component, a variable or a text style, and never write a raw value.
58
+ If the screen can't be built from what the system has, the gap is the finding.
59
+
@@ -24,8 +24,9 @@ Ask which component to document (e.g. "Button"), then:
24
24
  2. **Project it.** Write `design-system/docs/components/<Name>.doc.json`, set the
25
25
  Figma component `description`, rebuild the doc card's `Usage` band with the
26
26
  canonical builder (`.throughline/references/doc-card-builder.md` —
27
- verify its returned summary and stamp `surfaces.docCard.{src,render,renderer}`
28
- from it), and (if the repo/code side exists) render MDX/JSDoc and run
27
+ assemble the call with `node .throughline/scripts/doc-card-payload.mjs`,
28
+ check the returned `renderHash` against the one it printed, verify the
29
+ returned summary and stamp `surfaces.docCard.{src,render,renderer}` from it), and (if the repo/code side exists) render MDX/JSDoc and run
29
30
  `docs:digest` per the `storybook-chromatic-builder` render step.
30
31
  Before the Figma writes, run the active-file preflight in
31
32
  `.throughline/references/figma-scripting.md` — this command writes
@@ -0,0 +1,78 @@
1
+ Compose the screen the user asked for as one React file in their app, from what
2
+ the design system already has. Use the settings already in `design-system.json`
3
+ rather than re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the framework.** Read `project.uiFramework` from `design-system.json`.
6
+ Composing in code covers `shadcn` and `tailwind`. For anything else, stop and
7
+ say composing in code doesn't cover that framework yet.
8
+ 2. **Find the gate settings.** Search the repo's `package.json` files (not under
9
+ `node_modules`) for an `adherence:check` script. If one exists, take its
10
+ `--system`, `--package` and every `--tokens`, each resolved against the
11
+ directory of the `package.json` that holds it. Otherwise ask the user for the
12
+ package specifier the app imports the system from and the token source files,
13
+ and use the repo root as `--system`. Pass every token file: a system whose
14
+ values span modes has one per mode.
15
+ 3. **Know the target.** Unless the user named it, ask for the screen's name and
16
+ the file path to write it to, inside the app. Write `.tsx`, or `.jsx` when the
17
+ app has no TypeScript.
18
+ 4. **Read what exists.** Run
19
+ `node .throughline/scripts/compose-code.mjs inventory --system <dir> --package <spec>`.
20
+ Its components, with their variant axes, values, states, `whenToUse` and
21
+ `whenNotToUse`, are the only components the screen may use. Read a listed
22
+ component's own source for its prop names and exported sub-parts
23
+ (`CardHeader`); the inventory records neither. Find it by resolving the
24
+ package specifier: a workspace package in a monorepo, or the app's
25
+ `node_modules` otherwise. Token variable names come from
26
+ the system's generated CSS (the file its styles import), and the utilities
27
+ from its Tailwind preset; read both.
28
+ 5. **Plan.** Write the plan as JSON to a scratch file:
29
+ `{ "screen", "file", "components": [{ "name", "variants": { "<axis>": "<value>" } }] }`,
30
+ one entry per component the screen uses, with the variant values it sets. A
31
+ sub-part (`CardHeader`) comes with its owner (`Card`) and needs no entry. When
32
+ the request needs a component the inventory doesn't have, say so before
33
+ writing and ask: leave it out, use a named existing component instead, or stop.
34
+ The `nearest` names are matched on spelling; suggest the alternative that fits
35
+ by `whenToUse` instead when one does. Never build a lookalike. An icon, or
36
+ anything else the system's package doesn't export, is a gap in the same way:
37
+ the screen imports only from the package and React.
38
+ 6. **Check the plan.** Run the script with `check --system <dir> --package <spec> --plan <plan file>`
39
+ from the repo root. If it returns errors, nothing has been written: fix the
40
+ plan (a `missing-component` error goes back to the user as in step 5) and check
41
+ again. A `name-taken` error means the file at that path wasn't composed for
42
+ this screen: ask for another path, or for the user to move that file. A
43
+ re-run replaces an earlier composed file only when the screen name matches
44
+ its marker exactly, so reuse the name as written. Write
45
+ the variant values in the casing the returned plan uses.
46
+ 7. **Write.** Put the marker `// Composed by throughline: <screen>.` on the first
47
+ line, or on the second after a `'use client'` directive. Import components only
48
+ from the package specifier. Plain elements carry layout only (flex, grid, gap,
49
+ padding, sizing); visible text uses semantic elements (`h1`, `p`, `label`);
50
+ everything the user operates is a system component from the plan (a link may
51
+ be a plain `<a>`). Write colour, spacing, sizing, type size and font weight
52
+ only as token variables
53
+ (`gap-[var(--space-gap-md)]`, `style={{ padding: 'var(--space-4)' }}`) or the
54
+ utilities the system's preset generates from tokens (`bg-primary`,
55
+ `rounded-md`). Never a raw value, and never Tailwind's default scale
56
+ (`gap-4`, `text-lg`, `font-semibold`). Percentages, viewport units,
57
+ fractions (`w-1/2`) and keywords (`w-full`) are layout and fine. The gate
58
+ reads class names anywhere in the file, so visible copy that spells one
59
+ (`text-sm`) fails too; reword the copy rather than the rule. The gate can't
60
+ see a lookalike built from plain elements (a `div` with `role="switch"`), so
61
+ that one is yours to refuse. A size no token holds (a 400px card) can't be bound:
62
+ use a fraction or a keyword and say so in the report. Handlers are stubs; data and state are the app's.
63
+ 8. **Gate.** Run the script with
64
+ `gate --system <dir> --package <spec> --plan <plan file> --file <screen file>`,
65
+ with `--tokens <file>` repeated once per token file.
66
+ On a failure, fix the file and run it again, up to three attempts in all. If
67
+ the third still fails, stop: the file stays where it is, marked, and the
68
+ report lists every failure and says a re-run replaces it. Never change the
69
+ plan or skip a rule to make it pass.
70
+ 9. **Report.** The file's path, whether it replaced an earlier composed one, the
71
+ gate's counts, every gap (each component asked for that doesn't exist, with the
72
+ nearest names the script offered, and what was left out because of it), and
73
+ how to mount the screen where the app picks its screens (its router or entry).
74
+ Don't edit it.
75
+
76
+ Never edit a component, a token or the app's routing. If the screen can't be
77
+ built from what the system has, the gap is the finding.
78
+
@@ -0,0 +1,59 @@
1
+ Compose the screen the user asked for in Figma, from what the file already has.
2
+ Use the settings already in `design-system.json` (`figma.fileKey`) rather than
3
+ re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
6
+ absent, stop and ask which file to compose in.
7
+ 2. **Confirm the active file.** Run the preflight in
8
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
9
+ file is your target") against that fileKey, and pin it with `figma_navigate`
10
+ (`lock: true`) for the run. Unpin when done.
11
+ 3. **Read what exists.** Unless the user named them, ask which page holds the
12
+ component sets and which page the screen goes on (it's created if missing). Run
13
+ the snippet in `.throughline/references/figma-compose.md` verbatim, called
14
+ as its "How to call it" section says (`MODE` and `SPEC` declared above it,
15
+ `return await runCompose(MODE, SPEC);` below it), with `MODE = 'inventory'` and
16
+ `SPEC = { componentsPage }`. If the plan will set an instance-swap property (an
17
+ icon, say), ask which page holds the swap targets and run `inventory` again with
18
+ `iconsPage`.
19
+ 4. **Plan.** Write the screen description (its shape is the `DESCRIPTION` comment
20
+ in the snippet, at the top of the resolver) using only names from the
21
+ inventory's summary: component sets and their properties and options, variables
22
+ by kind, text styles, modes. Leave `sizing` off an instance unless it should
23
+ fill; `hug` works only on a set whose summary says `autoLayout: true`. Frames
24
+ carry layout only (auto layout, gap, padding, fill, stroke, radius); everything
25
+ the user sees is a component instance or styled text. When the request needs a
26
+ component the inventory doesn't have, say so before building and ask: leave it
27
+ out, use a named existing component instead, or stop. Never draw a lookalike
28
+ from frames.
29
+ 5. **Build.** Run the snippet with `MODE = 'build'` and
30
+ `SPEC = { componentsPage, iconsPage, description, fileKey }`. If it returns
31
+ errors, nothing was written: fix the description (a `missing-component` error
32
+ goes back to the user as in step 4) and build again. A `name-taken` error on
33
+ `<screen>` means a screen a person made already has that name: ask for another
34
+ name. On `WIP: <screen>` it means an unmarked frame holds the working name: ask
35
+ the user to rename or remove it. If the call throws instead, the build rolled
36
+ itself back and nothing was written: stop, and report the error as a composer
37
+ bug rather than reworking the screen around it.
38
+ 6. **Gate.** Run the screen gate in
39
+ `.throughline/references/figma-adherence.md` verbatim with
40
+ `TARGET = { page, screens: ['WIP: <screen>'], fileKey }` and `SKIP = []`. If it
41
+ fails, stop: report its failures as a composer bug, with their links, and leave
42
+ the `WIP:` frame for inspection. Don't change the description to make it pass.
43
+ Remove that frame with `MODE = 'discard'` and `SPEC = { page, screen }` only
44
+ when the user asks.
45
+ 7. **Finalize.** Run the snippet with `MODE = 'finalize'` and
46
+ `SPEC = { page, screen, fileKey }`. It replaces an earlier screen of that name
47
+ only if throughline composed it.
48
+ 8. **Review.** Dispatch the `reviewer` agent in Figma-visual mode with the screen's
49
+ node id (from finalize), the fileKey, the project's working directory (where
50
+ `design-system.json` is) and the user's request as its spec, unless the host
51
+ can't dispatch agents, in which case screenshot the screen yourself and say so.
52
+ 9. **Report.** The screen's link, whether it replaced an earlier one, the gate's
53
+ counts, the reviewer's verdict, and every gap: each component that was asked
54
+ for and doesn't exist, with the nearest names the script offered, and anything
55
+ left out of the request because of it.
56
+
57
+ Never edit a component, a variable or a text style, and never write a raw value.
58
+ If the screen can't be built from what the system has, the gap is the finding.
59
+
@@ -24,8 +24,9 @@ Ask which component to document (e.g. "Button"), then:
24
24
  2. **Project it.** Write `design-system/docs/components/<Name>.doc.json`, set the
25
25
  Figma component `description`, rebuild the doc card's `Usage` band with the
26
26
  canonical builder (`.throughline/references/doc-card-builder.md` —
27
- verify its returned summary and stamp `surfaces.docCard.{src,render,renderer}`
28
- from it), and (if the repo/code side exists) render MDX/JSDoc and run
27
+ assemble the call with `node .throughline/scripts/doc-card-payload.mjs`,
28
+ check the returned `renderHash` against the one it printed, verify the
29
+ returned summary and stamp `surfaces.docCard.{src,render,renderer}` from it), and (if the repo/code side exists) render MDX/JSDoc and run
29
30
  `docs:digest` per the `storybook-chromatic-builder` render step.
30
31
  Before the Figma writes, run the active-file preflight in
31
32
  `.throughline/references/figma-scripting.md` — this command writes
@@ -331,9 +331,11 @@ reference register, not this skill's guide voice.
331
331
  always last.
332
332
  - **Doc card body.** Render the card's `Usage` band with the canonical builder
333
333
  snippet in `.throughline/references/doc-card-builder.md` (via
334
- `figma_execute` with an explicit `timeout`, one card per call): fill the
335
- RECORD/CANONICAL_FP slots, resolve the nine required semantic variables and
336
- the `Body/Default` text style per that file's call contract, run it, and
334
+ `figma_execute` with an explicit `timeout`, one card per call): assemble the
335
+ call with `node .throughline/scripts/doc-card-payload.mjs <record> <cardNodeId>`
336
+ (it fills the RECORD/CANONICAL_FP slots and resolves the nine semantic
337
+ variables and `Body/Default` per that file's call contract), run it, check
338
+ `summary.renderHash` against the `expectedRenderHash` it printed, and
337
339
  verify the returned summary (`rowsRendered`, `blocksCreated`, `cardWidth`) —
338
340
  not a screenshot. The builder creates the `Doc Fingerprint` node itself and
339
341
  is the only thing that may build the usage body — never hand-assemble it.
@@ -20,6 +20,8 @@ ThroughLine builds a design system end to end. Read the matching skill file for
20
20
  ## ThroughLine commands
21
21
 
22
22
  - `check-figma` — Check what's already in Figma against the design system, read-only — screens for detached components and raw values a token has, and existing component sets for their states, focus ring and focus contrast. → read `commands/check-figma.md`.
23
+ - `compose-code` — Compose a screen in code from the design system's own components and tokens — plan checked before writing, every value a token, gated before it's handed over. → read `commands/compose-code.md`.
24
+ - `compose-figma` — Compose a screen in Figma from the design system's own components, variables and text styles — every value bound, nothing invented, gated before it's handed over. → read `commands/compose-figma.md`.
23
25
  - `design-system-status` — Show a plain-language summary of the current design system state — what's set up, what's not, and sensible next steps — read from design-system.json. → read `commands/design-system-status.md`.
24
26
  - `document-component` — Author, refresh, or reconcile the usage documentation for one existing component — draft its canonical doc record from four sources, project it to Figma, the doc card, and code, and resolve any drift via a reviewable per-item choice. → read `commands/document-component.md`.
25
27
  - `new-component` — Build a single new component end to end — in Figma, then sync any new tokens, then build its code component and stories — with a confirmation between each stage. → read `commands/new-component.md`.
@@ -0,0 +1,78 @@
1
+ Compose the screen the user asked for as one React file in their app, from what
2
+ the design system already has. Use the settings already in `design-system.json`
3
+ rather than re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the framework.** Read `project.uiFramework` from `design-system.json`.
6
+ Composing in code covers `shadcn` and `tailwind`. For anything else, stop and
7
+ say composing in code doesn't cover that framework yet.
8
+ 2. **Find the gate settings.** Search the repo's `package.json` files (not under
9
+ `node_modules`) for an `adherence:check` script. If one exists, take its
10
+ `--system`, `--package` and every `--tokens`, each resolved against the
11
+ directory of the `package.json` that holds it. Otherwise ask the user for the
12
+ package specifier the app imports the system from and the token source files,
13
+ and use the repo root as `--system`. Pass every token file: a system whose
14
+ values span modes has one per mode.
15
+ 3. **Know the target.** Unless the user named it, ask for the screen's name and
16
+ the file path to write it to, inside the app. Write `.tsx`, or `.jsx` when the
17
+ app has no TypeScript.
18
+ 4. **Read what exists.** Run
19
+ `node .throughline/scripts/compose-code.mjs inventory --system <dir> --package <spec>`.
20
+ Its components, with their variant axes, values, states, `whenToUse` and
21
+ `whenNotToUse`, are the only components the screen may use. Read a listed
22
+ component's own source for its prop names and exported sub-parts
23
+ (`CardHeader`); the inventory records neither. Find it by resolving the
24
+ package specifier: a workspace package in a monorepo, or the app's
25
+ `node_modules` otherwise. Token variable names come from
26
+ the system's generated CSS (the file its styles import), and the utilities
27
+ from its Tailwind preset; read both.
28
+ 5. **Plan.** Write the plan as JSON to a scratch file:
29
+ `{ "screen", "file", "components": [{ "name", "variants": { "<axis>": "<value>" } }] }`,
30
+ one entry per component the screen uses, with the variant values it sets. A
31
+ sub-part (`CardHeader`) comes with its owner (`Card`) and needs no entry. When
32
+ the request needs a component the inventory doesn't have, say so before
33
+ writing and ask: leave it out, use a named existing component instead, or stop.
34
+ The `nearest` names are matched on spelling; suggest the alternative that fits
35
+ by `whenToUse` instead when one does. Never build a lookalike. An icon, or
36
+ anything else the system's package doesn't export, is a gap in the same way:
37
+ the screen imports only from the package and React.
38
+ 6. **Check the plan.** Run the script with `check --system <dir> --package <spec> --plan <plan file>`
39
+ from the repo root. If it returns errors, nothing has been written: fix the
40
+ plan (a `missing-component` error goes back to the user as in step 5) and check
41
+ again. A `name-taken` error means the file at that path wasn't composed for
42
+ this screen: ask for another path, or for the user to move that file. A
43
+ re-run replaces an earlier composed file only when the screen name matches
44
+ its marker exactly, so reuse the name as written. Write
45
+ the variant values in the casing the returned plan uses.
46
+ 7. **Write.** Put the marker `// Composed by throughline: <screen>.` on the first
47
+ line, or on the second after a `'use client'` directive. Import components only
48
+ from the package specifier. Plain elements carry layout only (flex, grid, gap,
49
+ padding, sizing); visible text uses semantic elements (`h1`, `p`, `label`);
50
+ everything the user operates is a system component from the plan (a link may
51
+ be a plain `<a>`). Write colour, spacing, sizing, type size and font weight
52
+ only as token variables
53
+ (`gap-[var(--space-gap-md)]`, `style={{ padding: 'var(--space-4)' }}`) or the
54
+ utilities the system's preset generates from tokens (`bg-primary`,
55
+ `rounded-md`). Never a raw value, and never Tailwind's default scale
56
+ (`gap-4`, `text-lg`, `font-semibold`). Percentages, viewport units,
57
+ fractions (`w-1/2`) and keywords (`w-full`) are layout and fine. The gate
58
+ reads class names anywhere in the file, so visible copy that spells one
59
+ (`text-sm`) fails too; reword the copy rather than the rule. The gate can't
60
+ see a lookalike built from plain elements (a `div` with `role="switch"`), so
61
+ that one is yours to refuse. A size no token holds (a 400px card) can't be bound:
62
+ use a fraction or a keyword and say so in the report. Handlers are stubs; data and state are the app's.
63
+ 8. **Gate.** Run the script with
64
+ `gate --system <dir> --package <spec> --plan <plan file> --file <screen file>`,
65
+ with `--tokens <file>` repeated once per token file.
66
+ On a failure, fix the file and run it again, up to three attempts in all. If
67
+ the third still fails, stop: the file stays where it is, marked, and the
68
+ report lists every failure and says a re-run replaces it. Never change the
69
+ plan or skip a rule to make it pass.
70
+ 9. **Report.** The file's path, whether it replaced an earlier composed one, the
71
+ gate's counts, every gap (each component asked for that doesn't exist, with the
72
+ nearest names the script offered, and what was left out because of it), and
73
+ how to mount the screen where the app picks its screens (its router or entry).
74
+ Don't edit it.
75
+
76
+ Never edit a component, a token or the app's routing. If the screen can't be
77
+ built from what the system has, the gap is the finding.
78
+
@@ -0,0 +1,59 @@
1
+ Compose the screen the user asked for in Figma, from what the file already has.
2
+ Use the settings already in `design-system.json` (`figma.fileKey`) rather than
3
+ re-asking them. Scale explanation to `user.codingLevel`.
4
+
5
+ 1. **Know the file.** Read `figma.fileKey` from `design-system.json`. If it's
6
+ absent, stop and ask which file to compose in.
7
+ 2. **Confirm the active file.** Run the preflight in
8
+ `.throughline/references/figma-scripting.md` ("confirm the *active*
9
+ file is your target") against that fileKey, and pin it with `figma_navigate`
10
+ (`lock: true`) for the run. Unpin when done.
11
+ 3. **Read what exists.** Unless the user named them, ask which page holds the
12
+ component sets and which page the screen goes on (it's created if missing). Run
13
+ the snippet in `.throughline/references/figma-compose.md` verbatim, called
14
+ as its "How to call it" section says (`MODE` and `SPEC` declared above it,
15
+ `return await runCompose(MODE, SPEC);` below it), with `MODE = 'inventory'` and
16
+ `SPEC = { componentsPage }`. If the plan will set an instance-swap property (an
17
+ icon, say), ask which page holds the swap targets and run `inventory` again with
18
+ `iconsPage`.
19
+ 4. **Plan.** Write the screen description (its shape is the `DESCRIPTION` comment
20
+ in the snippet, at the top of the resolver) using only names from the
21
+ inventory's summary: component sets and their properties and options, variables
22
+ by kind, text styles, modes. Leave `sizing` off an instance unless it should
23
+ fill; `hug` works only on a set whose summary says `autoLayout: true`. Frames
24
+ carry layout only (auto layout, gap, padding, fill, stroke, radius); everything
25
+ the user sees is a component instance or styled text. When the request needs a
26
+ component the inventory doesn't have, say so before building and ask: leave it
27
+ out, use a named existing component instead, or stop. Never draw a lookalike
28
+ from frames.
29
+ 5. **Build.** Run the snippet with `MODE = 'build'` and
30
+ `SPEC = { componentsPage, iconsPage, description, fileKey }`. If it returns
31
+ errors, nothing was written: fix the description (a `missing-component` error
32
+ goes back to the user as in step 4) and build again. A `name-taken` error on
33
+ `<screen>` means a screen a person made already has that name: ask for another
34
+ name. On `WIP: <screen>` it means an unmarked frame holds the working name: ask
35
+ the user to rename or remove it. If the call throws instead, the build rolled
36
+ itself back and nothing was written: stop, and report the error as a composer
37
+ bug rather than reworking the screen around it.
38
+ 6. **Gate.** Run the screen gate in
39
+ `.throughline/references/figma-adherence.md` verbatim with
40
+ `TARGET = { page, screens: ['WIP: <screen>'], fileKey }` and `SKIP = []`. If it
41
+ fails, stop: report its failures as a composer bug, with their links, and leave
42
+ the `WIP:` frame for inspection. Don't change the description to make it pass.
43
+ Remove that frame with `MODE = 'discard'` and `SPEC = { page, screen }` only
44
+ when the user asks.
45
+ 7. **Finalize.** Run the snippet with `MODE = 'finalize'` and
46
+ `SPEC = { page, screen, fileKey }`. It replaces an earlier screen of that name
47
+ only if throughline composed it.
48
+ 8. **Review.** Dispatch the `reviewer` agent in Figma-visual mode with the screen's
49
+ node id (from finalize), the fileKey, the project's working directory (where
50
+ `design-system.json` is) and the user's request as its spec, unless the host
51
+ can't dispatch agents, in which case screenshot the screen yourself and say so.
52
+ 9. **Report.** The screen's link, whether it replaced an earlier one, the gate's
53
+ counts, the reviewer's verdict, and every gap: each component that was asked
54
+ for and doesn't exist, with the nearest names the script offered, and anything
55
+ left out of the request because of it.
56
+
57
+ Never edit a component, a variable or a text style, and never write a raw value.
58
+ If the screen can't be built from what the system has, the gap is the finding.
59
+
@@ -24,8 +24,9 @@ Ask which component to document (e.g. "Button"), then:
24
24
  2. **Project it.** Write `design-system/docs/components/<Name>.doc.json`, set the
25
25
  Figma component `description`, rebuild the doc card's `Usage` band with the
26
26
  canonical builder (`.throughline/references/doc-card-builder.md` —
27
- verify its returned summary and stamp `surfaces.docCard.{src,render,renderer}`
28
- from it), and (if the repo/code side exists) render MDX/JSDoc and run
27
+ assemble the call with `node .throughline/scripts/doc-card-payload.mjs`,
28
+ check the returned `renderHash` against the one it printed, verify the
29
+ returned summary and stamp `surfaces.docCard.{src,render,renderer}` from it), and (if the repo/code side exists) render MDX/JSDoc and run
29
30
  `docs:digest` per the `storybook-chromatic-builder` render step.
30
31
  Before the Figma writes, run the active-file preflight in
31
32
  `.throughline/references/figma-scripting.md` — this command writes
@@ -327,9 +327,11 @@ reference register, not this skill's guide voice.
327
327
  always last.
328
328
  - **Doc card body.** Render the card's `Usage` band with the canonical builder
329
329
  snippet in `.throughline/references/doc-card-builder.md` (via
330
- `figma_execute` with an explicit `timeout`, one card per call): fill the
331
- RECORD/CANONICAL_FP slots, resolve the nine required semantic variables and
332
- the `Body/Default` text style per that file's call contract, run it, and
330
+ `figma_execute` with an explicit `timeout`, one card per call): assemble the
331
+ call with `node .throughline/scripts/doc-card-payload.mjs <record> <cardNodeId>`
332
+ (it fills the RECORD/CANONICAL_FP slots and resolves the nine semantic
333
+ variables and `Body/Default` per that file's call contract), run it, check
334
+ `summary.renderHash` against the `expectedRenderHash` it printed, and
333
335
  verify the returned summary (`rowsRendered`, `blocksCreated`, `cardWidth`) —
334
336
  not a screenshot. The builder creates the `Doc Fingerprint` node itself and
335
337
  is the only thing that may build the usage body — never hand-assemble it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radicool/throughline",
3
- "version": "0.25.0",
3
+ "version": "0.27.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": {