@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.
- package/README.md +8 -4
- package/adapters/codex/AGENTS.md +2 -0
- package/adapters/codex/prompts/component-builder.md +5 -3
- package/adapters/codex/prompts/compose-code.md +78 -0
- package/adapters/codex/prompts/compose-figma.md +59 -0
- package/adapters/codex/prompts/document-component.md +3 -2
- package/adapters/cursor/.cursor/commands/compose-code.md +78 -0
- 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 +2 -0
- package/adapters/generic/commands/compose-code.md +78 -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 +8 -4
- package/scripts/build-doc-card-builder.mjs +16 -0
- package/scripts/build-figma-checks.mjs +89 -2
- package/scripts/compose-code.mjs +271 -0
- package/scripts/doc-card-payload.mjs +115 -0
- package/scripts/install.mjs +5 -3
- 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
|
@@ -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;
|