@radicool/throughline 0.14.0 → 0.16.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 +2 -1
- package/adapters/codex/AGENTS.md +10 -10
- package/adapters/codex/prompts/component-builder.md +59 -15
- package/adapters/codex/prompts/document-component.md +42 -10
- package/adapters/codex/prompts/storybook-chromatic-builder.md +52 -9
- package/adapters/codex/prompts/token-crosswalk-builder.md +2 -0
- package/adapters/codex/prompts/token-sync-layer.md +64 -4
- package/adapters/cursor/.cursor/commands/document-component.md +42 -10
- package/adapters/cursor/.cursor/rules/component-builder.mdc +60 -16
- package/adapters/cursor/.cursor/rules/component-pipeline.mdc +1 -1
- package/adapters/cursor/.cursor/rules/design-system-audit.mdc +1 -1
- package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +1 -1
- package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/repository-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +1 -1
- package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +53 -10
- package/adapters/cursor/.cursor/rules/token-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +3 -1
- package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +65 -5
- package/adapters/generic/AGENTS.md +10 -10
- package/adapters/generic/commands/document-component.md +42 -10
- package/adapters/generic/skills/component-builder/SKILL.md +59 -15
- package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +52 -9
- package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +2 -0
- package/adapters/generic/skills/token-sync-layer/SKILL.md +64 -4
- package/package.json +1 -1
- package/references/component-doc-archetypes.md +15 -11
- package/references/component-doc-schema.md +23 -5
- package/references/doc-card-builder.md +565 -0
- package/references/doc-writing-standard.md +144 -0
- package/references/figma-component-standards.md +63 -16
- package/references/guide-voice.md +96 -0
- package/references/manifest-schema.md +24 -6
- package/references/native-adapter-config.md +930 -0
- package/references/sync-adapters.md +94 -12
- package/scripts/README.md +37 -3
- package/scripts/build-doc-card-builder.mjs +143 -0
- package/scripts/build-native-adapter-config.mjs +280 -0
- package/scripts/docs-check.mjs +18 -4
- package/scripts/docs-lint.mjs +163 -0
- package/scripts/install.mjs +14 -1
- package/scripts/lib/doc-card-plan.mjs +101 -0
- package/scripts/lib/doc-card-render.figma.js +371 -0
- package/scripts/lib/dtcg.mjs +87 -0
- package/scripts/lib/native-literal.mjs +205 -0
- package/scripts/lib/sd-native.mjs +770 -0
- package/scripts/validate-crosswalk.mjs +3 -29
- package/scripts/validate-token-output.mjs +338 -0
package/scripts/docs-check.mjs
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
// design-system.json, and reports drift. Zero dependencies.
|
|
4
4
|
//
|
|
5
5
|
// Drift classes: canonical-changed | stale | edited | missing-surface | edit-unverified
|
|
6
|
+
// | layout-upgrade-available
|
|
6
7
|
// (edit-unverified = a surface the CLI cannot read, e.g. Figma — informational;
|
|
7
8
|
// it is checked live by the Figma-connected skill instead.
|
|
8
9
|
// missing-surface = a repo surface that declares a file which is now gone — failing;
|
|
9
|
-
// distinct from edit-unverified, which has no file to read in the first place.
|
|
10
|
+
// distinct from edit-unverified, which has no file to read in the first place.
|
|
11
|
+
// layout-upgrade-available = informational, docCard only: the card's layout
|
|
12
|
+
// predates DOC_CARD_RENDERER_VERSION — re-render on next touch, never a failure.)
|
|
10
13
|
//
|
|
11
14
|
// Usage: node docs-check.mjs [--root <dir>] (default root: cwd)
|
|
12
15
|
import { readFileSync, existsSync } from 'node:fs';
|
|
@@ -14,11 +17,12 @@ import { join } from 'node:path';
|
|
|
14
17
|
import { parseArgs } from 'node:util';
|
|
15
18
|
import { pathToFileURL } from 'node:url';
|
|
16
19
|
import { loadRecord, canonicalFingerprint, fingerprint } from './lib/doc-record.mjs';
|
|
20
|
+
import { DOC_CARD_RENDERER_VERSION } from './lib/doc-card-plan.mjs';
|
|
17
21
|
|
|
18
22
|
// Surfaces whose rendered content the CLI can re-read from the repo.
|
|
19
23
|
const REPO_SURFACES = new Set(['storybookMdx']);
|
|
20
24
|
|
|
21
|
-
export function classifySurface({ currentCanonical, surface, currentRenderHash, fileMissing = false }) {
|
|
25
|
+
export function classifySurface({ currentCanonical, surface, currentRenderHash, fileMissing = false, expectedRenderer = null }) {
|
|
22
26
|
const flags = [];
|
|
23
27
|
if (surface.src !== currentCanonical) flags.push('stale');
|
|
24
28
|
if (fileMissing) {
|
|
@@ -28,6 +32,10 @@ export function classifySurface({ currentCanonical, surface, currentRenderHash,
|
|
|
28
32
|
} else if (surface.render !== currentRenderHash) {
|
|
29
33
|
flags.push('edited');
|
|
30
34
|
}
|
|
35
|
+
if (expectedRenderer !== null
|
|
36
|
+
&& (!surface.renderer || Number(surface.renderer) < Number(expectedRenderer))) {
|
|
37
|
+
flags.push('layout-upgrade-available');
|
|
38
|
+
}
|
|
31
39
|
return flags;
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -57,7 +65,10 @@ export function checkComponent({ name, meta, root }) {
|
|
|
57
65
|
fileMissing = true;
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
|
-
const flags = classifySurface({
|
|
68
|
+
const flags = classifySurface({
|
|
69
|
+
currentCanonical, surface, currentRenderHash, fileMissing,
|
|
70
|
+
expectedRenderer: surfaceName === 'docCard' ? DOC_CARD_RENDERER_VERSION : null,
|
|
71
|
+
});
|
|
61
72
|
if (flags.length) out.push({ name, surface: surfaceName, flags });
|
|
62
73
|
}
|
|
63
74
|
return out;
|
|
@@ -89,7 +100,10 @@ function main() {
|
|
|
89
100
|
const info = results.filter((r) => !r.flags.some((f) => FAILING.has(f)));
|
|
90
101
|
|
|
91
102
|
for (const r of drift) console.error(` ✗ ${r.name} · ${r.surface}: ${r.flags.join(', ')}`);
|
|
92
|
-
for (const r of info)
|
|
103
|
+
for (const r of info) {
|
|
104
|
+
const note = r.flags.includes('edit-unverified') ? ' (check in a Figma session)' : '';
|
|
105
|
+
console.log(` ~ ${r.name} · ${r.surface}: ${r.flags.join(', ')}${note}`);
|
|
106
|
+
}
|
|
93
107
|
|
|
94
108
|
if (drift.length) {
|
|
95
109
|
console.error(`✗ docs:check — ${drift.length} drifted surface(s); reconcile with /document-component`);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copy lint for component doc records (.doc.json). Warnings only — findings
|
|
3
|
+
// never affect the exit code; the lint shapes a draft before user approval
|
|
4
|
+
// rather than gating CI. Rules: references/doc-writing-standard.md (pinned in
|
|
5
|
+
// the design spec's lint table).
|
|
6
|
+
//
|
|
7
|
+
// Usage: node docs-lint.mjs <path/to/Component.doc.json> [--json]
|
|
8
|
+
// Output: one warning per line — `<file>: <block-path>: <rule>: <message>`;
|
|
9
|
+
// --json emits {"warnings":[{path, rule, message}]}.
|
|
10
|
+
// Exit: 0 for any parseable record; 2 for unusable invocation.
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
|
|
15
|
+
// Machinery vocabulary banned from user-facing prose — the system's own
|
|
16
|
+
// build-compliance language. Real names of things (aria-label, role, Enter)
|
|
17
|
+
// are not banned; readers search for those.
|
|
18
|
+
const MACHINERY = [
|
|
19
|
+
'token', 'tokens', 'variable', 'variables', 'binding', 'bindings',
|
|
20
|
+
'fingerprint', 'fingerprints', 'provenance', 'projection', 'projections',
|
|
21
|
+
'surface', 'surfaces',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Visual-treatment words that must not LEAD a variant/state meaning.
|
|
25
|
+
const TREATMENT = [
|
|
26
|
+
'fill', 'filled', 'solid', 'stroke', 'border', 'bordered', 'outline',
|
|
27
|
+
'shadow', 'opacity', 'elevation',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const STOPWORDS = new Set([
|
|
31
|
+
'a', 'an', 'the', 'and', 'or', 'but', 'of', 'to', 'in', 'on', 'for',
|
|
32
|
+
'with', 'as', 'at', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'it',
|
|
33
|
+
'its', 'this', 'that', 'these', 'those', 'you', 'your', 'not', 'no', 'do',
|
|
34
|
+
'does', 'did', 'has', 'have', 'had', 'can', 'could', 'should', 'would',
|
|
35
|
+
'will', 'may', 'might', 'must', 'when', 'how', 'what', 'which', 'who',
|
|
36
|
+
'while', 'than', 'then', 'so', 'if', 'into', 'onto', 'from', 'over',
|
|
37
|
+
'under', 'up', 'down', 'out', 'about',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const words = (s) => String(s).toLowerCase().match(/[a-z0-9'’-]+/g) || [];
|
|
41
|
+
// Naive plural/verb-s stemming: enough to match "Triggers" to "trigger".
|
|
42
|
+
const stem = (w) => (w.length > 3 && w.endsWith('s') ? w.slice(0, -1) : w);
|
|
43
|
+
|
|
44
|
+
export function lintRecord(record) {
|
|
45
|
+
const warnings = [];
|
|
46
|
+
const warn = (path, rule, message) => warnings.push({ path, rule, message });
|
|
47
|
+
|
|
48
|
+
// Every user-facing prose field, as [block-path, text].
|
|
49
|
+
const prose = [];
|
|
50
|
+
if (typeof record.summary === 'string') prose.push(['summary', record.summary]);
|
|
51
|
+
if (typeof record.description === 'string') prose.push(['description', record.description]);
|
|
52
|
+
for (const key of ['whenToUse', 'whenNotToUse', 'dos', 'donts']) {
|
|
53
|
+
(Array.isArray(record[key]) ? record[key] : []).forEach((t, i) => prose.push([`${key}[${i}]`, t]));
|
|
54
|
+
}
|
|
55
|
+
const meanings = [];
|
|
56
|
+
for (const axis of Object.keys(record.variants || {})) {
|
|
57
|
+
for (const [term, meaning] of Object.entries(record.variants[axis] || {})) {
|
|
58
|
+
meanings.push([`variants.${axis}.${term}`, meaning]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const [term, meaning] of Object.entries(record.states || {})) {
|
|
62
|
+
meanings.push([`states.${term}`, meaning]);
|
|
63
|
+
}
|
|
64
|
+
prose.push(...meanings);
|
|
65
|
+
const a11y = record.accessibility || {};
|
|
66
|
+
(Array.isArray(a11y.keyboard) ? a11y.keyboard : []).forEach((t, i) => prose.push([`accessibility.keyboard[${i}]`, t]));
|
|
67
|
+
(Array.isArray(a11y.notes) ? a11y.notes : []).forEach((t, i) => prose.push([`accessibility.notes[${i}]`, t]));
|
|
68
|
+
|
|
69
|
+
for (const [path, text] of prose) {
|
|
70
|
+
const ws = words(text);
|
|
71
|
+
const banned = MACHINERY.find((b) => ws.includes(b));
|
|
72
|
+
if (banned) {
|
|
73
|
+
warn(path, 'machinery-vocabulary',
|
|
74
|
+
`"${banned}" is the system's machinery vocabulary — describe the thing and how to use it, never how it was made`);
|
|
75
|
+
}
|
|
76
|
+
if (/`[^`]+`/.test(String(text))) {
|
|
77
|
+
warn(path, 'no-inline-code',
|
|
78
|
+
'inline-code backticks render as literal characters on the doc card and are stripped from the Figma description — write the term as plain text');
|
|
79
|
+
}
|
|
80
|
+
for (const sentence of String(text).split(/[.!?]+/)) {
|
|
81
|
+
const n = words(sentence).length;
|
|
82
|
+
if (n > 35) warn(path, 'run-on-sentence', `sentence has ${n} words (max 35)`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof record.summary === 'string') {
|
|
87
|
+
const n = words(record.summary).length;
|
|
88
|
+
if (n > 12) warn('summary', 'summary-length', `${n} words (max 12)`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof record.description === 'string') {
|
|
92
|
+
const n = words(record.description).length;
|
|
93
|
+
if (n < 15 || n > 70) warn('description', 'description-length', `${n} words (want 15–70)`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof record.summary === 'string' && Array.isArray(record.whenToUse)
|
|
97
|
+
&& typeof record.whenToUse[0] === 'string') {
|
|
98
|
+
const summaryStems = [...new Set(
|
|
99
|
+
words(record.summary).filter((w) => !STOPWORDS.has(w)).map(stem),
|
|
100
|
+
)];
|
|
101
|
+
const targetStems = new Set(words(record.whenToUse[0]).map(stem));
|
|
102
|
+
if (summaryStems.length > 0) {
|
|
103
|
+
const matched = summaryStems.filter((s) => targetStems.has(s)).length;
|
|
104
|
+
const pct = matched / summaryStems.length;
|
|
105
|
+
if (pct > 0.6) {
|
|
106
|
+
warn('whenToUse[0]', 'summary-echo',
|
|
107
|
+
`${Math.round(pct * 100)}% of the summary's content words reappear — describe a situation, not the summary again`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const key of ['dos', 'donts']) {
|
|
113
|
+
(Array.isArray(record[key]) ? record[key] : []).forEach((entry, i) => {
|
|
114
|
+
const path = `${key}[${i}]`;
|
|
115
|
+
const n = words(entry).length;
|
|
116
|
+
if (n > 14) warn(path, 'guidance-length', `${n} words (max 14)`);
|
|
117
|
+
if (!/\.$/.test(String(entry).trim())) {
|
|
118
|
+
warn(path, 'terminal-stop', 'end the entry with a full stop');
|
|
119
|
+
}
|
|
120
|
+
if (key === 'donts' && !/^(don['’]?t|do not|never|avoid)\b/i.test(String(entry).trim())) {
|
|
121
|
+
warn(path, 'dont-shape', "open with Don't / Never / Avoid and name the alternative");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const [path, meaning] of meanings) {
|
|
127
|
+
const ws = words(meaning);
|
|
128
|
+
if (ws.length < 3) {
|
|
129
|
+
warn(path, 'empty-meaning', `${ws.length} word(s) — say what it means, not just that it exists`);
|
|
130
|
+
}
|
|
131
|
+
if (ws.slice(0, 4).some((w) => TREATMENT.includes(w))) {
|
|
132
|
+
warn(path, 'treatment-lead', 'leads with visual treatment — lead with meaning; treatment is optional detail');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return warnings;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const invokedAsCli = process.argv[1]
|
|
140
|
+
&& import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
141
|
+
if (invokedAsCli) {
|
|
142
|
+
const args = process.argv.slice(2);
|
|
143
|
+
const asJson = args.includes('--json');
|
|
144
|
+
const file = args.find((a) => !a.startsWith('--'));
|
|
145
|
+
if (!file) {
|
|
146
|
+
console.error('usage: docs-lint.mjs <path/to/Component.doc.json> [--json]');
|
|
147
|
+
process.exit(2);
|
|
148
|
+
}
|
|
149
|
+
let record;
|
|
150
|
+
try {
|
|
151
|
+
record = JSON.parse(readFileSync(file, 'utf8'));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
console.error(`docs-lint: cannot read ${file}: ${e.message}`);
|
|
154
|
+
process.exit(2);
|
|
155
|
+
}
|
|
156
|
+
const warnings = lintRecord(record);
|
|
157
|
+
if (asJson) {
|
|
158
|
+
console.log(JSON.stringify({ warnings }, null, 2));
|
|
159
|
+
} else {
|
|
160
|
+
for (const w of warnings) console.log(`${file}: ${w.path}: ${w.rule}: ${w.message}`);
|
|
161
|
+
}
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
package/scripts/install.mjs
CHANGED
|
@@ -99,6 +99,19 @@ function stagePayload(srcRoot, destRoot, skip) {
|
|
|
99
99
|
return written;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Scripts that never run from a consuming repo: the installer itself, the two
|
|
103
|
+
// reference-doc generators, and the renderer template one of them inlines (the
|
|
104
|
+
// renderer reaches Figma pre-inlined inside references/doc-card-builder.md).
|
|
105
|
+
const PLUGIN_INTERNAL = new Set([
|
|
106
|
+
'install.mjs',
|
|
107
|
+
'build-doc-card-builder.mjs',
|
|
108
|
+
'build-native-adapter-config.mjs',
|
|
109
|
+
'lib/doc-card-render.figma.js',
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
export const skipScript = (relPosix) =>
|
|
113
|
+
relPosix.startsWith('adapters/') || relPosix.endsWith('.test.mjs') || PLUGIN_INTERNAL.has(relPosix);
|
|
114
|
+
|
|
102
115
|
export function install({ target, dir, pkgRoot = PKG_ROOT }) {
|
|
103
116
|
if (!TARGETS.includes(target)) {
|
|
104
117
|
throw new Error(`unknown target "${target}"; expected one of: ${TARGETS.join(', ')}`);
|
|
@@ -124,7 +137,7 @@ export function install({ target, dir, pkgRoot = PKG_ROOT }) {
|
|
|
124
137
|
}
|
|
125
138
|
const payload = [
|
|
126
139
|
...stagePayload(join(pkgRoot, 'references'), join(dir, BASE, 'references')),
|
|
127
|
-
...stagePayload(join(pkgRoot, 'scripts'), join(dir, BASE, 'scripts'),
|
|
140
|
+
...stagePayload(join(pkgRoot, 'scripts'), join(dir, BASE, 'scripts'), skipScript),
|
|
128
141
|
];
|
|
129
142
|
return { target, dir, written, payload };
|
|
130
143
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Pure layout planner for the component doc card's Usage band.
|
|
2
|
+
// ZERO imports, `export const`/`export function` only — this module is inlined
|
|
3
|
+
// verbatim into the generated Figma snippet (references/doc-card-builder.md) by
|
|
4
|
+
// build-doc-card-builder.mjs, so it must run in both Node and the Figma plugin
|
|
5
|
+
// sandbox. build-doc-card-builder.mjs enforces the no-imports rule.
|
|
6
|
+
//
|
|
7
|
+
// Layout contract: docs/superpowers/specs/2026-08-09-doc-card-layout-and-voice-design.md
|
|
8
|
+
|
|
9
|
+
// Single source of truth for the doc-card layout version. Imported by
|
|
10
|
+
// docs-check.mjs and embedded (via inlining) into the generated builder snippet.
|
|
11
|
+
export const DOC_CARD_RENDERER_VERSION = '4';
|
|
12
|
+
|
|
13
|
+
// columnUnit = clamp(round(bodyFontSize × 30), 280, 480) px.
|
|
14
|
+
// 30 ≈ 60ch × ~0.5em average glyph width for UI text faces. Layout chrome, not
|
|
15
|
+
// a design value — the one documented exception to the no-hardcoded-px rule.
|
|
16
|
+
export function columnUnit(bodyFontSize) {
|
|
17
|
+
return Math.min(480, Math.max(280, Math.round(bodyFontSize * 30)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// columns = max(max blocks in any row, 3). Content alone decides: the grid
|
|
21
|
+
// never mints a column no row can fill, and never drops below the 3-unit floor.
|
|
22
|
+
// The specimen is deliberately NOT an input — the render widens the card, the
|
|
23
|
+
// card's hug propagates into FILL siblings including the specimen, so any
|
|
24
|
+
// specimen measurement is a value this render mutates and the next one reads.
|
|
25
|
+
export function cardColumns(maxBlocksPerRow) {
|
|
26
|
+
return Math.max(3, maxBlocksPerRow);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listBlock(eyebrow, items) {
|
|
30
|
+
if (!Array.isArray(items) || items.length === 0) return null;
|
|
31
|
+
return { type: 'list', name: `Block: ${eyebrow}`, eyebrow, items };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function definitionBlock(eyebrow, meanings) {
|
|
35
|
+
const terms = Object.keys(meanings || {}).map((k) => ({ term: k, meaning: meanings[k] }));
|
|
36
|
+
if (terms.length === 0) return null;
|
|
37
|
+
return { type: 'definition', name: `Block: ${eyebrow}`, eyebrow, terms };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The whole layout decision, as data. Rows keep canonical numbering (an absent
|
|
41
|
+
// row's number is skipped, never renumbered) so node names stay stable across
|
|
42
|
+
// sparse records. bodyTextStyle: only .fontSize is read — passing a full Figma
|
|
43
|
+
// TextStyle object is fine.
|
|
44
|
+
export function planDocCard(record, bodyTextStyle) {
|
|
45
|
+
const unit = columnUnit(bodyTextStyle.fontSize);
|
|
46
|
+
|
|
47
|
+
const row1 = [];
|
|
48
|
+
if (typeof record.description === 'string' && record.description.trim() !== '') {
|
|
49
|
+
row1.push({ type: 'prose', name: 'Block: Overview', eyebrow: 'Overview', text: record.description });
|
|
50
|
+
}
|
|
51
|
+
const whenTo = listBlock('When to use', record.whenToUse);
|
|
52
|
+
if (whenTo) row1.push(whenTo);
|
|
53
|
+
const whenNot = listBlock('When not to use', record.whenNotToUse);
|
|
54
|
+
if (whenNot) row1.push(whenNot);
|
|
55
|
+
|
|
56
|
+
const row2 = [];
|
|
57
|
+
if (Array.isArray(record.dos) && record.dos.length) {
|
|
58
|
+
row2.push({ type: 'list-tone', name: 'Block: Do', eyebrow: '✓ Do', tone: 'positive', items: record.dos });
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(record.donts) && record.donts.length) {
|
|
61
|
+
row2.push({ type: 'list-tone', name: "Block: Don't", eyebrow: "✕ Don't", tone: 'negative', items: record.donts });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const row3 = [];
|
|
65
|
+
for (const axis of Object.keys(record.variants || {})) {
|
|
66
|
+
const block = definitionBlock(`What each ${axis} means`, record.variants[axis]);
|
|
67
|
+
if (block) row3.push(block);
|
|
68
|
+
}
|
|
69
|
+
const stateBlock = definitionBlock('What each state means', record.states);
|
|
70
|
+
if (stateBlock) row3.push(stateBlock);
|
|
71
|
+
const a11y = record.accessibility || {};
|
|
72
|
+
// role is not rendered on the card — it lives in the description field / MDX.
|
|
73
|
+
const a11yBlock = listBlock('Accessibility', [...(a11y.keyboard || []), ...(a11y.notes || [])]);
|
|
74
|
+
if (a11yBlock) row3.push(a11yBlock);
|
|
75
|
+
|
|
76
|
+
const rows = [
|
|
77
|
+
{ name: 'Usage Row 1', blocks: row1 },
|
|
78
|
+
{ name: 'Usage Row 2', blocks: row2 },
|
|
79
|
+
{ name: 'Usage Row 3', blocks: row3 },
|
|
80
|
+
].filter((r) => r.blocks.length > 0);
|
|
81
|
+
|
|
82
|
+
const maxBlocksPerRow = rows.reduce((m, r) => Math.max(m, r.blocks.length), 0);
|
|
83
|
+
const columns = cardColumns(maxBlocksPerRow);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
rendererVersion: DOC_CARD_RENDERER_VERSION,
|
|
87
|
+
columnUnit: unit,
|
|
88
|
+
columns,
|
|
89
|
+
cardWidth: columns * unit,
|
|
90
|
+
termColumn: Math.round(unit * 0.3),
|
|
91
|
+
// The header band's record-derived content. Carried in the plan (not read
|
|
92
|
+
// straight off the record by the renderer) so renderHash describes every
|
|
93
|
+
// string the builder writes onto the card, header included. Always strings:
|
|
94
|
+
// an undefined would drop the key from JSON.stringify and move the hash.
|
|
95
|
+
header: {
|
|
96
|
+
summary: typeof record.summary === 'string' ? record.summary : '',
|
|
97
|
+
updatedAt: typeof record.updatedAt === 'string' ? record.updatedAt : '',
|
|
98
|
+
},
|
|
99
|
+
rows,
|
|
100
|
+
};
|
|
101
|
+
}
|