@radicool/throughline 0.25.0 → 0.26.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 +6 -4
- package/adapters/codex/AGENTS.md +1 -0
- package/adapters/codex/prompts/component-builder.md +5 -3
- package/adapters/codex/prompts/compose-figma.md +59 -0
- package/adapters/codex/prompts/document-component.md +3 -2
- package/adapters/cursor/.cursor/commands/compose-figma.md +59 -0
- package/adapters/cursor/.cursor/commands/document-component.md +3 -2
- package/adapters/cursor/.cursor/rules/component-builder.mdc +5 -3
- package/adapters/generic/AGENTS.md +1 -0
- package/adapters/generic/commands/compose-figma.md +59 -0
- package/adapters/generic/commands/document-component.md +3 -2
- package/adapters/generic/skills/component-builder/SKILL.md +5 -3
- package/package.json +1 -1
- package/references/doc-card-builder.md +62 -15
- package/references/figma-adherence.md +21 -0
- package/references/figma-compose.md +896 -0
- package/scripts/README.md +7 -4
- package/scripts/build-doc-card-builder.mjs +16 -0
- package/scripts/build-figma-checks.mjs +89 -2
- package/scripts/doc-card-payload.mjs +115 -0
- package/scripts/install.mjs +4 -2
- package/scripts/lib/doc-card-plan.mjs +37 -0
- package/scripts/lib/doc-card-render.figma.js +9 -15
- package/scripts/lib/figma-adherence-read.figma.js +21 -0
- package/scripts/lib/figma-compose.figma.js +326 -0
- package/scripts/lib/figma-compose.mjs +317 -0
- package/scripts/lib/fixtures/figma-compose/casing-and-suffix.json +8 -0
- package/scripts/lib/fixtures/figma-compose/fill-in-unlaid-slot.json +7 -0
- package/scripts/lib/fixtures/figma-compose/hug-on-fixed-component.json +34 -0
- package/scripts/lib/fixtures/figma-compose/inventory.json +295 -0
- package/scripts/lib/fixtures/figma-compose/many-errors.json +8 -0
- package/scripts/lib/fixtures/figma-compose/missing-component.json +7 -0
- package/scripts/lib/fixtures/figma-compose/name-taken.json +6 -0
- package/scripts/lib/fixtures/figma-compose/padding-sides.json +23 -0
- package/scripts/lib/fixtures/figma-compose/radius-kind.json +27 -0
- package/scripts/lib/fixtures/figma-compose/raw-number.json +5 -0
- package/scripts/lib/fixtures/figma-compose/sign-in-ok.json +16 -0
- package/scripts/lib/fixtures/figma-compose/slot-ok.json +9 -0
- package/scripts/lib/fixtures/figma-compose/unknown-prop-and-option.json +7 -0
- 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.
|
|
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.26.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,8 @@ 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
|
+
|
|
112
114
|
Update anytime with `/plugin marketplace update throughline-marketplace`.
|
|
113
115
|
|
|
114
116
|
#### Cursor, Codex, or a generic AGENTS.md agent
|
|
@@ -116,9 +118,9 @@ Update anytime with `/plugin marketplace update throughline-marketplace`.
|
|
|
116
118
|
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
119
|
|
|
118
120
|
```
|
|
119
|
-
npx @radicool/throughline@0.
|
|
120
|
-
npx @radicool/throughline@0.
|
|
121
|
-
npx @radicool/throughline@0.
|
|
121
|
+
npx @radicool/throughline@0.26.0 init --target=cursor # → .cursor/rules + .cursor/mcp.json
|
|
122
|
+
npx @radicool/throughline@0.26.0 init --target=codex # → prompts/ + AGENTS.md index + codex-mcp.toml
|
|
123
|
+
npx @radicool/throughline@0.26.0 init --target=generic # → skills/ + AGENTS.md index
|
|
122
124
|
```
|
|
123
125
|
|
|
124
126
|
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.
|
package/adapters/codex/AGENTS.md
CHANGED
|
@@ -20,6 +20,7 @@ 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-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
24
|
- `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
25
|
- `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
26
|
- `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):
|
|
331
|
-
|
|
332
|
-
the
|
|
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,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
|
-
|
|
28
|
-
|
|
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,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
|
-
|
|
28
|
-
|
|
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):
|
|
335
|
-
|
|
336
|
-
the
|
|
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,7 @@ 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-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
24
|
- `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
25
|
- `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
26
|
- `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,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
|
-
|
|
28
|
-
|
|
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):
|
|
331
|
-
|
|
332
|
-
the
|
|
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.
|
|
3
|
+
"version": "0.26.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": {
|
|
@@ -14,6 +14,18 @@ it reads the specimen and never writes it. The status chip keeps its own owner
|
|
|
14
14
|
|
|
15
15
|
## How to call it
|
|
16
16
|
|
|
17
|
+
Steps 1–4 are one command once `figma.docCardVariables` is recorded:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
node scripts/doc-card-payload.mjs design-system/docs/components/<Name>.doc.json <cardNodeId> --out <scratch>/payload.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
It writes the complete `figma_execute` body — record, fingerprint, the nine
|
|
24
|
+
role lookups by recorded name, `Body/Default`, the snippet and the call — and
|
|
25
|
+
prints the `expectedRenderHash` to stderr. Pass the file's contents as the
|
|
26
|
+
`code` unchanged, then do steps 5–6. The steps below are what it does, and
|
|
27
|
+
what to do by hand when the mapping isn't recorded yet.
|
|
28
|
+
|
|
17
29
|
1. Load the record and compute its canonical fingerprint in Node
|
|
18
30
|
(`canonicalFingerprint` in `scripts/lib/doc-record.mjs`).
|
|
19
31
|
2. Read `figma.docCardVariables` from `design-system.json`.
|
|
@@ -82,6 +94,10 @@ const summary = await renderDocCard({ card, record: RECORD, vars, bodyTextStyle
|
|
|
82
94
|
`cardWidth` — not from a screenshot, then stamp the manifest from it:
|
|
83
95
|
`surfaces.docCard = { src: summary.fingerprint, render: summary.renderHash,
|
|
84
96
|
renderer: summary.rendererVersion }`. Never re-read the card to stamp.
|
|
97
|
+
With the payload script, first check `summary.renderHash` equals the
|
|
98
|
+
`expectedRenderHash` it printed. A mismatch means the payload changed on
|
|
99
|
+
the way in, or `Body/Default` isn't the size the script assumed — the
|
|
100
|
+
summary's `bodyFontSize` says which; re-run with `--font-size`.
|
|
85
101
|
|
|
86
102
|
## The snippet
|
|
87
103
|
|
|
@@ -188,20 +204,11 @@ function planDocCard(record, bodyTextStyle) {
|
|
|
188
204
|
};
|
|
189
205
|
}
|
|
190
206
|
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// BEFORE .characters, fonts loaded up front, resize() sizing modes re-asserted,
|
|
197
|
-
// bound paints seeded light-gray (never pure black).
|
|
198
|
-
//
|
|
199
|
-
// In-scope globals when assembled: planDocCard, DOC_CARD_RENDERER_VERSION
|
|
200
|
-
// (inlined planner) and the caller-filled slots RECORD, CANONICAL_FP.
|
|
201
|
-
|
|
202
|
-
// 32-bit FNV-1a — the render hash. Only ever compared against itself (the
|
|
203
|
-
// manifest's surfaces.docCard.render), so it does not need to match the
|
|
204
|
-
// sha256-based canonical fingerprint, which cannot run in the Figma sandbox.
|
|
207
|
+
// 32-bit FNV-1a — the render hash: fnv1a(JSON.stringify(plan)). Only ever
|
|
208
|
+
// compared against itself (the manifest's surfaces.docCard.render), so it does
|
|
209
|
+
// not need to match the sha256-based canonical fingerprint, which cannot run in
|
|
210
|
+
// the Figma sandbox. Lives here, not in the renderer, so Node can compute the
|
|
211
|
+
// hash a render should return (doc-card-payload.mjs) with the same code.
|
|
205
212
|
function fnv1a(str) {
|
|
206
213
|
let h = 0x811c9dc5;
|
|
207
214
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -211,6 +218,40 @@ function fnv1a(str) {
|
|
|
211
218
|
return h.toString(16).padStart(8, '0');
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
// When a header's title row has no node named `Status Pill`/`Status`, name the
|
|
222
|
+
// node that most likely IS the chip so the error can say what to rename. Never
|
|
223
|
+
// used to pick a node to write — the renderer still refuses to guess. A
|
|
224
|
+
// candidate is a FRAME below the title row whose only child is a TEXT node
|
|
225
|
+
// (the chip shape: a pill frame around its label). Exactly one candidate or
|
|
226
|
+
// no hint. Reads only type/name/id/characters/children, so it runs on Figma
|
|
227
|
+
// nodes and on plain test objects alike.
|
|
228
|
+
function statusChipHint(titleRow) {
|
|
229
|
+
if (!titleRow || titleRow.type !== 'FRAME') return null;
|
|
230
|
+
const found = [];
|
|
231
|
+
const walk = (node) => {
|
|
232
|
+
for (const child of node.children || []) {
|
|
233
|
+
if (child.type === 'FRAME' && child.children && child.children.length === 1
|
|
234
|
+
&& child.children[0].type === 'TEXT') {
|
|
235
|
+
found.push({ id: child.id, name: child.name, label: child.children[0].characters });
|
|
236
|
+
}
|
|
237
|
+
walk(child);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
walk(titleRow);
|
|
241
|
+
return found.length === 1 ? found[0] : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Figma plugin-API renderer for the doc-card Usage band. This file is NOT a
|
|
245
|
+
// Node module — build-doc-card-builder.mjs concatenates it after the inlined
|
|
246
|
+
// planner (doc-card-plan.mjs) into references/doc-card-builder.md, and the
|
|
247
|
+
// result runs inside figma_execute (dynamic-page mode). Constraints honored
|
|
248
|
+
// here (see references/figma-scripting.md): async APIs only, style/font set
|
|
249
|
+
// BEFORE .characters, fonts loaded up front, resize() sizing modes re-asserted,
|
|
250
|
+
// bound paints seeded light-gray (never pure black).
|
|
251
|
+
//
|
|
252
|
+
// In-scope globals when assembled: planDocCard, DOC_CARD_RENDERER_VERSION,
|
|
253
|
+
// fnv1a, statusChipHint (inlined planner) and the caller-filled slots RECORD, CANONICAL_FP.
|
|
254
|
+
|
|
214
255
|
const REQUIRED_VARS = [
|
|
215
256
|
'textDefault', 'textMuted', 'tonePositive', 'toneNegative', 'border',
|
|
216
257
|
'spacePadding', 'spaceRowGap', 'spaceBlockGap', 'spaceItemGap',
|
|
@@ -341,7 +382,13 @@ async function renderDocCard({ card, record, vars, bodyTextStyle }) {
|
|
|
341
382
|
const descAmbiguous = !named && descCandidates.length > 1;
|
|
342
383
|
|
|
343
384
|
const missingAnchors = [];
|
|
344
|
-
if (!hasStatusAnchor)
|
|
385
|
+
if (!hasStatusAnchor) {
|
|
386
|
+
const hint = statusChipHint(titleRow);
|
|
387
|
+
missingAnchors.push('status (title row must contain a descendant named "Status Pill" or "Status"'
|
|
388
|
+
+ (hint ? ' — frame ' + hint.id + ' "' + hint.name + '" holding "' + hint.label
|
|
389
|
+
+ '" looks like the status chip; rename it "Status Pill" and re-run' : '')
|
|
390
|
+
+ ')');
|
|
391
|
+
}
|
|
345
392
|
if (!headerDescCandidate) {
|
|
346
393
|
missingAnchors.push(descAmbiguous
|
|
347
394
|
? 'description (found ' + descCandidates.length + ' candidate TEXT children, cannot tell which is the description — name the right one "Header Description" by hand and re-run)'
|
|
@@ -345,6 +345,19 @@ function overriddenRecords(instance, path, underDetached) {
|
|
|
345
345
|
return records;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
// The SLOT nodes on this instance's own surface: its slots, and the slots of
|
|
349
|
+
// instances nested in its component, reached only through component sublayers
|
|
350
|
+
// (ids 'I…'). A slot inside content added to a slot is left out here; the walk
|
|
351
|
+
// reaches it through that content.
|
|
352
|
+
function slotsOf(instance) {
|
|
353
|
+
return instance.findAll((n) => n.type === 'SLOT').filter((slot) => {
|
|
354
|
+
for (let node = slot.parent; node && node !== instance; node = node.parent) {
|
|
355
|
+
if (!String(node.id).startsWith('I')) return false;
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
348
361
|
function screensOn(page, names) {
|
|
349
362
|
const all = [];
|
|
350
363
|
for (const top of page.children) {
|
|
@@ -405,6 +418,14 @@ async function readScreens(target) {
|
|
|
405
418
|
if (node.visible === false) return;
|
|
406
419
|
if (node.type === 'INSTANCE') {
|
|
407
420
|
nodes.push(...overriddenRecords(node, path, insideDetached));
|
|
421
|
+
// Content added to a slot is the screen's own, and isn't listed in
|
|
422
|
+
// instance.overrides. A slot's placeholder children keep instance-sublayer
|
|
423
|
+
// ids ('I…;…') and stay part of the component.
|
|
424
|
+
for (const slot of slotsOf(node)) {
|
|
425
|
+
for (const child of slot.children) {
|
|
426
|
+
if (!String(child.id).startsWith('I')) walk(child, `${path} / ${slot.name} / ${child.name}`, false, insideDetached);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
408
429
|
return;
|
|
409
430
|
}
|
|
410
431
|
const mixedPaints = node.fills === figma.mixed;
|