@motion-proto/live-tokens 0.74.0 → 0.76.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 (171) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +33 -38
  2. package/.claude/skills/live-tokens-create-component/SKILL copy.md +196 -0
  3. package/.claude/skills/live-tokens-create-component/SKILL.md +198 -146
  4. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +95 -52
  5. package/.claude/skills/live-tokens-create-component/references/intrinsics.md +2 -2
  6. package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +2 -2
  7. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +12 -12
  8. package/.claude/skills/live-tokens-create-component/references/token-naming.md +2 -1
  9. package/.claude/skills/live-tokens-create-page/SKILL.md +187 -0
  10. package/.claude/skills/live-tokens-create-page/references/interaction-sources.md +66 -0
  11. package/.claude/skills/live-tokens-create-page/references/layout-sources.md +87 -0
  12. package/.claude/skills/live-tokens-create-theme/SKILL.md +52 -48
  13. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
  14. package/.claude/skills/live-tokens-fix-findings/SKILL.md +69 -60
  15. package/.claude/skills/live-tokens-pick-component/SKILL.md +64 -79
  16. package/.claude/skills/live-tokens-set-colors/SKILL.md +47 -38
  17. package/.claude/skills/live-tokens-set-geometry/SKILL.md +58 -35
  18. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +5 -3
  19. package/.claude/skills/live-tokens-set-type/SKILL.md +30 -30
  20. package/CHANGELOG.md +138 -0
  21. package/README.md +17 -7
  22. package/bin/check-component.mjs +91 -19
  23. package/bin/check-page.mjs +102 -21
  24. package/bin/cli.mjs +87 -113
  25. package/bin/contractRunner.mjs +945 -0
  26. package/bin/create.mjs +1 -1
  27. package/bin/lib/catalogue.mjs +37 -30
  28. package/bin/lib/findings.mjs +46 -15
  29. package/bin/lib/report.mjs +3 -3
  30. package/bin/lib/tokenVocabulary.mjs +4 -4
  31. package/bin/migrate-routes.mjs +5 -5
  32. package/bin/migrate.mjs +4 -4
  33. package/bin/save-theme.mjs +8 -9
  34. package/bin/set-colors.mjs +9 -11
  35. package/bin/set-geometry.mjs +7 -7
  36. package/bin/set-type.mjs +5 -7
  37. package/bin/setup-claude.mjs +110 -0
  38. package/dist-plugin/{chunk-W6Y4BWFB.js → chunk-6WGFOJXO.js} +22 -0
  39. package/dist-plugin/{chunk-7VRTBGJT.js → chunk-PDNL4NC5.js} +9 -2
  40. package/dist-plugin/{chunk-V3YF6CGT.js → chunk-SWXRVZKT.js} +43 -1
  41. package/dist-plugin/{dataPaths-BhWzd5cL.d.cts → dataPaths-BpIK_Obx.d.cts} +1 -0
  42. package/dist-plugin/{dataPaths-BhWzd5cL.d.ts → dataPaths-BpIK_Obx.d.ts} +1 -0
  43. package/dist-plugin/index.cjs +310 -147
  44. package/dist-plugin/index.d.cts +1 -1
  45. package/dist-plugin/index.d.ts +1 -1
  46. package/dist-plugin/index.js +218 -122
  47. package/dist-plugin/migrateData/index.cjs +66 -2
  48. package/dist-plugin/migrateData/index.d.cts +1 -1
  49. package/dist-plugin/migrateData/index.d.ts +1 -1
  50. package/dist-plugin/migrateData/index.js +3 -3
  51. package/dist-plugin/setColors/index.cjs +61 -4
  52. package/dist-plugin/setColors/index.d.cts +1 -1
  53. package/dist-plugin/setColors/index.d.ts +1 -1
  54. package/dist-plugin/setColors/index.js +5 -5
  55. package/dist-plugin/setGeometry/index.cjs +107 -44
  56. package/dist-plugin/setGeometry/index.d.cts +5 -5
  57. package/dist-plugin/setGeometry/index.d.ts +5 -5
  58. package/dist-plugin/setGeometry/index.js +51 -45
  59. package/dist-plugin/setType/index.cjs +15 -0
  60. package/dist-plugin/setType/index.d.cts +1 -1
  61. package/dist-plugin/setType/index.d.ts +1 -1
  62. package/dist-plugin/setType/index.js +1 -1
  63. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  64. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  65. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  66. package/package.json +35 -8
  67. package/src/app/site.css +19 -9
  68. package/src/editor/bootstrap.ts +2 -0
  69. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +4 -4
  70. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  71. package/src/editor/component-editor/MenuSelectEditor.svelte +4 -1
  72. package/src/editor/component-editor/SegmentedControlEditor.svelte +6 -1
  73. package/src/editor/component-editor/TabBarEditor.svelte +1 -1
  74. package/src/editor/component-editor/TableEditor.svelte +2 -2
  75. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +6 -3
  76. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +41 -20
  77. package/src/editor/core/components/adjustAliases.ts +59 -45
  78. package/src/editor/core/components/aliasKinds.ts +9 -5
  79. package/src/editor/core/preview/themePreview.ts +9 -2
  80. package/src/editor/core/sketch/sketchLayer.ts +22 -0
  81. package/src/editor/core/store/editorStore.ts +10 -1
  82. package/src/editor/core/themes/buildColors.ts +3 -3
  83. package/src/editor/core/themes/liveStateStream.ts +26 -0
  84. package/src/editor/core/themes/migrations/2026-09-07-stroke-role-renames.ts +52 -0
  85. package/src/editor/core/themes/migrations/index.ts +2 -0
  86. package/src/editor/core/themes/themeDocumentSync.ts +22 -11
  87. package/src/editor/core/themes/themeService.ts +9 -2
  88. package/src/editor/pages/ComponentEditorPage.svelte +17 -1
  89. package/src/editor/pages/liveTokensEditorHandle.ts +23 -0
  90. package/src/editor/skill-atlas/SkillAtlas.svelte +75 -573
  91. package/src/editor/skill-atlas/TreeCanvas.svelte +263 -0
  92. package/src/editor/skill-atlas/TreeNodeCard.svelte +136 -53
  93. package/src/editor/skill-atlas/edges.ts +31 -0
  94. package/src/editor/skill-atlas/skillSources.generated.ts +20 -19
  95. package/src/editor/skill-atlas/skillTrees.ts +19 -3840
  96. package/src/editor/skill-atlas/trees/check-compliance.ts +183 -0
  97. package/src/editor/skill-atlas/trees/create-component.ts +275 -0
  98. package/src/editor/skill-atlas/trees/create-page.ts +320 -0
  99. package/src/editor/skill-atlas/trees/create-theme.ts +255 -0
  100. package/src/editor/skill-atlas/trees/fix-findings.ts +469 -0
  101. package/src/editor/skill-atlas/trees/pick-component.ts +300 -0
  102. package/src/editor/skill-atlas/trees/set-colors.ts +148 -0
  103. package/src/editor/skill-atlas/trees/set-geometry.ts +136 -0
  104. package/src/editor/skill-atlas/trees/set-type.ts +142 -0
  105. package/src/editor/skill-atlas/types.ts +3 -4
  106. package/src/editor/skill-atlas/wireLayout.ts +287 -0
  107. package/src/live-tokens/data/themes/autumn.json +15 -15
  108. package/src/live-tokens/data/themes/halloween.json +15 -15
  109. package/src/live-tokens/data/themes/midnight-study.json +15 -15
  110. package/src/live-tokens/data/themes/ocean.json +15 -15
  111. package/src/live-tokens/data/themes/royal-velvet.json +15 -15
  112. package/src/live-tokens/data/themes/sketchy.json +15 -15
  113. package/src/live-tokens/data/themes/spring-meadow.json +15 -15
  114. package/src/live-tokens/data/themes/sunset.json +15 -15
  115. package/src/system/components/Badge.svelte +7 -0
  116. package/src/system/components/Button.svelte +7 -0
  117. package/src/system/components/Callout.svelte +10 -6
  118. package/src/system/components/Card.svelte +23 -4
  119. package/src/system/components/CodeSnippet.svelte +4 -3
  120. package/src/system/components/CollapsibleSection.svelte +23 -8
  121. package/src/system/components/CornerBadge.svelte +6 -0
  122. package/src/system/components/Dialog.svelte +13 -6
  123. package/src/system/components/IconButton.svelte +9 -0
  124. package/src/system/components/Image.svelte +8 -0
  125. package/src/system/components/ImageLightbox.svelte +6 -0
  126. package/src/system/components/InlineEditActions.svelte +7 -0
  127. package/src/system/components/Input.svelte +7 -0
  128. package/src/system/components/MenuSelect.svelte +7 -0
  129. package/src/system/components/Notification.svelte +7 -0
  130. package/src/system/components/Panel.svelte +6 -0
  131. package/src/system/components/ProgressBar.svelte +5 -0
  132. package/src/system/components/RadioButton.svelte +11 -5
  133. package/src/system/components/SectionDivider.svelte +8 -0
  134. package/src/system/components/SegmentedControl.svelte +6 -0
  135. package/src/system/components/SideNavigation.svelte +6 -0
  136. package/src/system/components/Slider.svelte +7 -4
  137. package/src/system/components/TabBar.svelte +15 -9
  138. package/src/system/components/Table.svelte +8 -3
  139. package/src/system/components/Toggle.svelte +4 -4
  140. package/src/system/components/Tooltip.svelte +6 -0
  141. package/src/testing-js/chunk-AO7EZHYV.js +776 -0
  142. package/src/testing-js/chunk-AO7EZHYV.js.map +1 -0
  143. package/src/testing-js/chunk-FAFOAWYL.js +39 -0
  144. package/src/testing-js/chunk-FAFOAWYL.js.map +1 -0
  145. package/src/testing-js/chunk-L73N4NSO.js +23 -0
  146. package/src/testing-js/chunk-L73N4NSO.js.map +1 -0
  147. package/src/testing-js/chunk-LXR3MN6N.js +3063 -0
  148. package/src/testing-js/chunk-LXR3MN6N.js.map +1 -0
  149. package/src/testing-js/chunk-ZMSX6CXR.js +53 -0
  150. package/src/testing-js/chunk-ZMSX6CXR.js.map +1 -0
  151. package/src/testing-js/component-alias.contract.js +81 -0
  152. package/src/testing-js/component-alias.contract.js.map +1 -0
  153. package/src/testing-js/component-editor.contract.js +62 -0
  154. package/src/testing-js/component-editor.contract.js.map +1 -0
  155. package/src/testing-js/component-render.contract.js +568 -0
  156. package/src/testing-js/component-render.contract.js.map +1 -0
  157. package/src/testing-js/index.d.ts +293 -0
  158. package/src/testing-js/index.js +222 -0
  159. package/src/testing-js/index.js.map +1 -0
  160. package/src/testing-js/registry.contract.js +39 -0
  161. package/src/testing-js/registry.contract.js.map +1 -0
  162. package/src/testing-js/vitest-BE6uGF31.d.ts +73 -0
  163. package/src/testing-js/vitest.d.ts +3 -0
  164. package/src/testing-js/vitest.js +13 -0
  165. package/src/testing-js/vitest.js.map +1 -0
  166. package/template/README.md +13 -0
  167. package/template/_gitignore +6 -0
  168. package/template/package.json +3 -1
  169. package/template/src/pages/Home.svelte +4 -18
  170. package/.claude/skills/live-tokens-build-page/SKILL.md +0 -103
  171. package/.claude/skills/live-tokens-build-page/references/layout-sources.md +0 -48
package/bin/create.mjs CHANGED
@@ -78,7 +78,7 @@ export function runCreate({ targetDir, pkgRoot, force = false }) {
78
78
  export function formatCreateResult({ appName, targetDir }, targetArg) {
79
79
  return [
80
80
  ``,
81
- `Scaffolded ${appName} → ${targetDir}`,
81
+ `Scaffolded ${appName} in ${targetDir}`,
82
82
  ``,
83
83
  `Next steps:`,
84
84
  ` cd ${targetArg}`,
@@ -1,28 +1,33 @@
1
1
  // The registry as a query. Every component a project has, shipped or its own,
2
2
  // with the props each takes and the tokens each declares, and every theme token
3
- // grouped by family. Read from files through the same vocabulary the checkers
3
+ // grouped by scale. Read from files through the same vocabulary the checkers
4
4
  // use, so a skill or a script sees exactly what the checkers will hold it to.
5
5
 
6
6
  import { readFileSync } from 'node:fs';
7
7
  import { relative } from 'node:path';
8
- import { CONTRACT_FAMILIES } from './tokenVocabulary.mjs';
8
+ import { CONTRACT_SCALES } from './tokenVocabulary.mjs';
9
9
 
10
- /** The runtime file's leading HTML comment, which is where a component says what it is for. */
10
+ /** The runtime file's leading HTML comment, which is where a component says what
11
+ it is for. A labelled line (`Use for:`, `Not for:`, `Emphasis:`) opens a line
12
+ of the description and every other line continues the one above it, so the
13
+ comment wraps in the source and still reads as its four lines here. */
11
14
  function descriptionOf(source) {
12
15
  const m = source.match(/^\s*<!--([\s\S]*?)-->/);
13
16
  if (!m) return '';
14
- return m[1]
15
- .split('\n')
16
- .map((line) => line.trim())
17
- .join(' ')
18
- .replace(/\s+/g, ' ')
19
- .trim()
20
- .replace(/^\S+\.svelte\s*[—–-]+\s*/, '');
17
+ const lines = [];
18
+ for (const raw of m[1].split('\n')) {
19
+ const line = raw.trim().replace(/\s+/g, ' ');
20
+ if (!line) continue;
21
+ if (lines.length && !/^[A-Z][A-Za-z ]{0,20}:/.test(line)) lines[lines.length - 1] += ` ${line}`;
22
+ else lines.push(line);
23
+ }
24
+ if (lines.length) lines[0] = lines[0].replace(/^\S+\.svelte\s*(?:[—–-]+|\.)\s*/, '');
25
+ return lines.join('\n');
21
26
  }
22
27
 
23
- function familyOf(name) {
28
+ function scaleOf(name) {
24
29
  const stem = name.replace(/^--/, '');
25
- const hit = CONTRACT_FAMILIES
30
+ const hit = CONTRACT_SCALES
26
31
  .filter((f) => stem === f || stem.startsWith(`${f}-`))
27
32
  .sort((a, b) => b.length - a.length)[0];
28
33
  return hit ?? stem.split('-')[0];
@@ -62,15 +67,15 @@ export function describeTokens(vocab, { root = process.cwd() } = {}) {
62
67
  if (!values.has(m[1])) values.set(m[1], m[2].trim());
63
68
  }
64
69
  }
65
- const byFamily = new Map();
70
+ const byScale = new Map();
66
71
  for (const name of vocab.themeTokens) {
67
- const family = familyOf(name);
68
- if (!byFamily.has(family)) byFamily.set(family, []);
69
- byFamily.get(family).push({ name, value: values.get(name) ?? '' });
72
+ const scale = scaleOf(name);
73
+ if (!byScale.has(scale)) byScale.set(scale, []);
74
+ byScale.get(scale).push({ name, value: values.get(name) ?? '' });
70
75
  }
71
76
  return {
72
77
  tokensCss: vocab.tokensCssPath ? relative(root, vocab.tokensCssPath) : null,
73
- families: [...byFamily].map(([family, tokens]) => ({ family, tokens })),
78
+ scales: [...byScale].map(([scale, tokens]) => ({ scale, tokens })),
74
79
  components: [...vocab.components.values()].map((c) => ({
75
80
  id: c.id,
76
81
  tokens: [...c.tokens].map(([name, value]) => ({ name, default: value })),
@@ -78,13 +83,15 @@ export function describeTokens(vocab, { root = process.cwd() } = {}) {
78
83
  };
79
84
  }
80
85
 
86
+ const describeLines = (c) => (c.description ? c.description.split('\n') : []);
87
+
81
88
  export function formatComponents(list, { id } = {}) {
82
89
  const lines = [];
83
90
  if (id) {
84
91
  const c = list.find((x) => x.id === id);
85
92
  if (!c) return `No component "${id}". Run \`live-tokens components\` for the list.`;
86
- lines.push(`${c.name} (${c.id}, ${c.origin}${c.registered ? '' : ', NOT registered'}) ${c.file}`);
87
- if (c.description) lines.push(` ${c.description}`);
93
+ lines.push(`${c.name} (${c.id}, ${c.origin}${c.registered ? '' : ', unregistered'}) ${c.file}`);
94
+ for (const line of describeLines(c)) lines.push(` ${line}`);
88
95
  if (c.props.length) {
89
96
  lines.push(' props:');
90
97
  for (const p of c.props) lines.push(` ${p.name}${p.values ? `: ${p.values.join(' | ')}` : p.type ? `: ${p.type}` : ''}`);
@@ -96,7 +103,7 @@ export function formatComponents(list, { id } = {}) {
96
103
  for (const c of list) {
97
104
  const variants = c.variants.length ? ` variants: ${c.variants.join(', ')}` : '';
98
105
  lines.push(`${c.id.padEnd(20)} ${c.origin.padEnd(8)} ${c.name}${c.registered ? '' : ' (NOT registered)'}${variants}`);
99
- if (c.description) lines.push(`${''.padEnd(29)} ${c.description}`);
106
+ for (const line of describeLines(c)) lines.push(` ${line}`);
100
107
  }
101
108
  lines.push('');
102
109
  const unregistered = list.filter((c) => !c.registered).length;
@@ -107,21 +114,21 @@ export function formatComponents(list, { id } = {}) {
107
114
  return lines.join('\n');
108
115
  }
109
116
 
110
- export function formatTokens(desc, { family } = {}) {
117
+ export function formatTokens(desc, { scale } = {}) {
111
118
  const lines = [];
112
- const families = family ? desc.families.filter((f) => f.family === family) : desc.families;
113
- if (family && families.length === 0) {
114
- return `No family "${family}". Families: ${desc.families.map((f) => f.family).join(', ')}.`;
119
+ const scales = scale ? desc.scales.filter((s) => s.scale === scale) : desc.scales;
120
+ if (scale && scales.length === 0) {
121
+ return `No token scale "${scale}". Scales: ${desc.scales.map((s) => s.scale).join(', ')}.`;
115
122
  }
116
- lines.push(`Theme tokens from ${desc.tokensCss ?? '(no tokens.css found)'}`);
117
- for (const f of families) {
123
+ lines.push(`Design tokens from ${desc.tokensCss ?? '(no tokens.css found)'}`);
124
+ for (const s of scales) {
118
125
  lines.push('');
119
- lines.push(`${f.family} (${f.tokens.length})`);
120
- for (const t of f.tokens) lines.push(` ${t.name}: ${t.value}`);
126
+ lines.push(`${s.scale} (${s.tokens.length})`);
127
+ for (const t of s.tokens) lines.push(` ${t.name}: ${t.value}`);
121
128
  }
122
- if (!family) {
129
+ if (!scale) {
123
130
  lines.push('');
124
- lines.push(`Component tokens: ${desc.components.reduce((n, c) => n + c.tokens.length, 0)} across ${desc.components.length} component(s). \`live-tokens components <id>\` lists one component's.`);
131
+ lines.push(`Semantic properties: ${desc.components.reduce((n, c) => n + c.tokens.length, 0)} across ${desc.components.length} component(s). \`live-tokens components <id>\` lists one component's.`);
125
132
  }
126
133
  return lines.join('\n');
127
134
  }
@@ -52,38 +52,65 @@ export function isExcluded(relPath, root) {
52
52
  * Unrecognised flags are returned in `rest` for the caller to handle.
53
53
  */
54
54
  export function parseCheckFlags(argv) {
55
- const opts = { off: [], warn: [], error: [], strict: false, json: false, rest: [] };
55
+ const opts = { off: [], warn: [], error: [], strict: false, json: false, tests: false, rest: [] };
56
56
  for (const arg of argv) {
57
57
  const m = arg.match(/^--(off|warn|error)=(.+)$/);
58
58
  if (m) {
59
59
  opts[m[1]].push(...m[2].split(',').map((s) => s.trim()).filter(Boolean));
60
60
  } else if (arg === '--strict') opts.strict = true;
61
61
  else if (arg === '--json') opts.json = true;
62
+ else if (arg === '--tests') opts.tests = true;
62
63
  else opts.rest.push(arg);
63
64
  }
64
65
  return opts;
65
66
  }
66
67
 
68
+ /** The same last-wins resolution `applySeverity` applies per finding, exposed
69
+ * standalone so coverage (which has no findings to attach a severity to, but
70
+ * still has to honor `--off`) can ask the same question. */
71
+ export function resolveRuleSeverity(ruleId, rules, opts = {}, config = {}) {
72
+ const configured = config.rules ?? {};
73
+ let severity = rules[ruleId] ?? 'error';
74
+ if (SEVERITIES.includes(configured[ruleId])) severity = configured[ruleId];
75
+ if (opts.off?.includes(ruleId)) severity = 'off';
76
+ if (opts.warn?.includes(ruleId)) severity = 'warn';
77
+ if (opts.error?.includes(ruleId)) severity = 'error';
78
+ if (opts.strict && severity === 'warn') severity = 'error';
79
+ return severity;
80
+ }
81
+
67
82
  /**
68
83
  * Resolve each finding's severity and drop the ones turned off.
69
84
  * `rules` maps rule id to its default severity.
70
85
  */
71
- export function applySeverity(findings, rules, opts = {}, config = {}) {
72
- const configured = config.rules ?? {};
73
- const resolve = (id) => {
74
- let severity = rules[id] ?? 'error';
75
- if (SEVERITIES.includes(configured[id])) severity = configured[id];
76
- if (opts.off?.includes(id)) severity = 'off';
77
- if (opts.warn?.includes(id)) severity = 'warn';
78
- if (opts.error?.includes(id)) severity = 'error';
79
- if (opts.strict && severity === 'warn') severity = 'error';
80
- return severity;
81
- };
86
+ export function applySeverity(findings, rules, opts = {}, config = {}, fixes = {}) {
82
87
  return findings
83
- .map((f) => ({ ...f, severity: resolve(f.rule) }))
88
+ .map((f) => ({
89
+ ...f,
90
+ severity: resolveRuleSeverity(f.rule, rules, opts, config),
91
+ ...(fixes[f.rule] ? { fix: fixes[f.rule] } : {}),
92
+ }))
84
93
  .filter((f) => f.severity !== 'off');
85
94
  }
86
95
 
96
+ /**
97
+ * A coverage entry for a rule resolved to severity 'off' becomes 'disabled',
98
+ * regardless of what actually happened: the plan requires `--off` to stay
99
+ * visible as disabled coverage rather than silently reading as a pass (an
100
+ * obligation that failed but was silenced is not the same as one that ran
101
+ * clean), and disabled coverage can never establish a complete pass.
102
+ */
103
+ export function applyCoverageSeverity(coverage, rules, opts = {}, config = {}) {
104
+ const out = {};
105
+ for (const [id, ruleMap] of Object.entries(coverage)) {
106
+ out[id] = {};
107
+ for (const [rule, entry] of Object.entries(ruleMap)) {
108
+ out[id][rule] = resolveRuleSeverity(rule, rules, opts, config) === 'off' ? { status: 'disabled' } : entry;
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+
87
114
  export function countBySeverity(findings) {
88
115
  return {
89
116
  errors: findings.filter((f) => f.severity === 'error').length,
@@ -117,7 +144,11 @@ export function formatFindings(findings, { label, checked = 0 } = {}) {
117
144
  return lines.join('\n');
118
145
  }
119
146
 
120
- export function toJson(findings, { label, checked = 0 } = {}) {
147
+ export function toJson(findings, { label, checked = 0, coverage } = {}) {
121
148
  const { errors, warnings } = countBySeverity(findings);
122
- return JSON.stringify({ check: label, checked, errors, warnings, findings }, null, 2);
149
+ return JSON.stringify(
150
+ { check: label, checked, errors, warnings, findings, ...(coverage ? { coverage } : {}) },
151
+ null,
152
+ 2,
153
+ );
123
154
  }
@@ -120,7 +120,7 @@ const list = (items, max = 20) =>
120
120
 
121
121
  export function formatReport(r) {
122
122
  const out = [];
123
- out.push(`Project: ${r.project.pages} page file(s), ${r.project.components} component(s), ${r.project.themeTokens} theme tokens from ${r.project.tokensCss ?? '(no tokens.css)'}`);
123
+ out.push(`Project: ${r.project.pages} page file(s), ${r.project.components} component(s), ${r.project.themeTokens} design tokens from ${r.project.tokensCss ?? '(no tokens.css)'}`);
124
124
  if (r.migrations) {
125
125
  out.push('');
126
126
  out.push(`Migrations: ${r.migrations.status}${r.migrations.pending?.length ? ` (${list(r.migrations.pending)})` : ''}`);
@@ -131,11 +131,11 @@ export function formatReport(r) {
131
131
  const unread = r.components.filter((c) => c.unread.length);
132
132
  out.push(` tokens declared and read by their own CSS: ${r.components.reduce((n, c) => n + c.tokens - c.unread.length, 0)} of ${r.components.reduce((n, c) => n + c.tokens, 0)}`);
133
133
  for (const c of unread) out.push(` ${c.id}: ${c.unread.length} unread (${list(c.unread, 6)})`);
134
+ const undescribed = r.components.filter((c) => !c.described).map((c) => c.id);
135
+ if (undescribed.length) out.push(` no description comment: ${list(undescribed)}`);
134
136
  const custom = r.components.filter((c) => c.origin === 'custom');
135
137
  out.push(` custom: ${custom.length}${custom.length ? ` (${list(custom.map((c) => c.id))})` : ''}`);
136
138
  if (r.usage.customUnregistered.length) out.push(` not registered: ${list(r.usage.customUnregistered)}`);
137
- const undescribed = custom.filter((c) => !c.described).map((c) => c.id);
138
- if (undescribed.length) out.push(` no description comment: ${list(undescribed)}`);
139
139
 
140
140
  out.push('');
141
141
  out.push('Usage');
@@ -25,18 +25,18 @@ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
25
25
 
26
26
  const SHIPPED_COMPONENTS_DIR = 'src/system/components';
27
27
 
28
- /** Families whose names are governed by the token contract (see TOKENS.md). */
29
- export const CONTRACT_FAMILIES = [
28
+ /** Token scales whose names are governed by the token contract (see TOKENS.md). */
29
+ export const CONTRACT_SCALES = [
30
30
  'surface', 'text', 'border', 'color', 'space', 'radius', 'font', 'line-height',
31
31
  'letter-spacing', 'shadow', 'blur', 'icon-size', 'scrim', 'tint', 'columns',
32
32
  'heading', 'body', 'editorial', 'eyebrow', 'code', 'easing', 'duration', 'zoom',
33
33
  'gradient', 'stroke',
34
34
  ];
35
35
 
36
- /** True when `name` belongs to a contract-governed family, so a miss is a typo. */
36
+ /** True when `name` belongs to a contract-governed scale, so a miss is a typo. */
37
37
  export function isContractToken(name) {
38
38
  const stem = name.replace(/^--/, '');
39
- return CONTRACT_FAMILIES.some((f) => stem === f || stem.startsWith(`${f}-`));
39
+ return CONTRACT_SCALES.some((f) => stem === f || stem.startsWith(`${f}-`));
40
40
  }
41
41
 
42
42
  /** Every `--name:` declared anywhere in a stylesheet or style block. */
@@ -149,8 +149,8 @@ export function formatRouteResult(result, { check = false } = {}) {
149
149
  const { rewritten, pendingWrite, advisory } = result;
150
150
  if (!rewritten.length && !pendingWrite.length && !advisory.length) return '';
151
151
 
152
- const ref = (h) => ` ${h.file}:${h.line} ${h.kind} '${h.old}' → '${h.next}'`;
153
- const lines = ['Route references — /editor, /components, /docs moved to /live-tokens/* in 0.35.0:'];
152
+ const ref = (h) => ` ${h.file}:${h.line} ${h.kind} '${h.old}' to '${h.next}'`;
153
+ const lines = ['Route references: /editor, /components, /docs moved to /live-tokens/* in 0.35.0:'];
154
154
 
155
155
  if (rewritten.length) {
156
156
  lines.push(` ✓ Rewrote ${rewritten.length} reference(s):`);
@@ -160,7 +160,7 @@ export function formatRouteResult(result, { check = false } = {}) {
160
160
  lines.push(
161
161
  check
162
162
  ? ` Would rewrite ${pendingWrite.length} reference(s) with --write:`
163
- : ` ${pendingWrite.length} reference(s) can be rewritten — re-run with --write to apply:`,
163
+ : ` ${pendingWrite.length} reference(s) can be rewritten. Re-run with --write to apply:`,
164
164
  );
165
165
  pendingWrite.forEach((h) => lines.push(ref(h)));
166
166
  }
@@ -169,8 +169,8 @@ export function formatRouteResult(result, { check = false } = {}) {
169
169
  advisory.forEach((h) => {
170
170
  const why =
171
171
  h.reason === 'docs-never'
172
- ? `/docs is never auto-rewritten — update to '${h.next}' only if it points at the package guide`
173
- : `you declare or relocate '${h.old}' yourself — leave it if it's your route`;
172
+ ? `/docs is never auto-rewritten. Update to '${h.next}' only when it points at the package guide`
173
+ : `the project declares or relocates '${h.old}'. Leave it when it is the project's own route`;
174
174
  lines.push(` ${h.file}:${h.line} ${h.kind} '${h.old}' (${why})`);
175
175
  });
176
176
  }
package/bin/migrate.mjs CHANGED
@@ -127,13 +127,13 @@ export function formatMigrateDataResult(result) {
127
127
  // `sketch-presets/` → `sketch-styles/`. Told apart by the destination
128
128
  // name, since `LegacyRename` carries no reason of its own.
129
129
  const reason = basename(to) === 'sketch-styles' ? 'sketchstyle rename' : '0.48 layout';
130
- lines.push(` ${planned ? 'would move' : 'moved'} ${rel(from)} → ${rel(to)} (${reason})`);
130
+ lines.push(` ${planned ? 'would move' : 'moved'} ${rel(from)} to ${rel(to)} (${reason})`);
131
131
  }
132
132
  for (const p of result.upgradedThemes) {
133
133
  lines.push(` ${planned ? 'would carry' : 'carried'} ${rel(p)} by value (was a pre-v3 theme naming files)`);
134
134
  }
135
135
  if (result.production) {
136
- lines.push(` production theme → "${result.production.slug}" (${HOW_SAID[result.production.how]})`);
136
+ lines.push(` production theme: "${result.production.slug}" (${HOW_SAID[result.production.how]})`);
137
137
  }
138
138
  if (result.recoveredThemePath) {
139
139
  lines.push(` ${planned ? 'would write' : 'wrote'} ${rel(result.recoveredThemePath)}`);
@@ -148,13 +148,13 @@ export function formatMigrateDataResult(result) {
148
148
  lines.push(` ${planned ? 'would delete' : 'deleted'} ${rel(p)}: a copy of a saved theme`);
149
149
  }
150
150
  for (const p of result.keptUserFiles) {
151
- lines.push(` kept ${rel(p)}: it matches no theme, so it is yours`);
151
+ lines.push(` kept ${rel(p)}: it matches no theme, so it stays`);
152
152
  }
153
153
  for (const note of result.droppedRefs) {
154
154
  lines.push(` ${note}`);
155
155
  }
156
156
  for (const p of result.notThemes) {
157
- lines.push(` left ${rel(p)} alone: it is a colors-and-type file, not a theme`);
157
+ lines.push(` left ${rel(p)} alone: it is a colors-and-type file`);
158
158
  }
159
159
  for (const p of result.shadowedDefaults) {
160
160
  lines.push(` kept ${rel(p)}: it shadows the default the package ships, which has moved on`);
@@ -142,35 +142,34 @@ export function formatSaveThemeResult(result) {
142
142
  const root = process.cwd();
143
143
  const lines = [];
144
144
  const wrote = result.dryRun ? 'Would write' : result.existed ? 'Updated' : 'Created';
145
- lines.push(`${wrote} theme "${result.name}" → ${relative(root, result.themePath)}`);
145
+ lines.push(`${wrote} theme "${result.name}" at ${relative(root, result.themePath)}`);
146
146
  lines.push(`It carries the colors and type and ${result.components} component config(s) by value.`);
147
147
 
148
148
  const edited = [
149
149
  ...(result.buffered.colorsAndType ? ['colors and type'] : []),
150
150
  ...result.buffered.components,
151
151
  ];
152
- const open = result.openTheme ? `the open theme "${result.openTheme}"` : 'the package defaults';
152
+ const open = result.openTheme ? `the open theme "${result.openTheme}"` : 'the shipped defaults';
153
153
  const kept = result.dryRun ? 'Would save' : 'Saved';
154
154
  if (edited.length > 0) {
155
- lines.push(`${kept} your unsaved edits: ${edited.join(', ')}.`);
155
+ lines.push(`${kept} the buffer: ${edited.join(', ')}.`);
156
156
  lines.push(`Everything else came from ${open}, or the shipped defaults where it carries no entry.`);
157
157
  } else {
158
- lines.push(`No unsaved edits; ${kept.toLowerCase()} a copy of ${open}.`);
158
+ lines.push(`No buffer. ${kept} a copy of ${open}.`);
159
159
  }
160
160
  if (result.sketchSettings) lines.push(`Sketch settings rode through from the open theme.`);
161
161
 
162
162
  if (result.activated) {
163
163
  lines.push(
164
- `\nOpened "${result.slug}" (previously open: "${result.previousActive}"). ` +
165
- `Reload the app to see it; switch back any time from Load in the editor's Theme panel. ` +
166
- `Adopt it there to publish it to tokens.generated.css.`,
164
+ `\nLoaded "${result.slug}" (previously open: "${result.previousActive}"). ` +
165
+ `Adopt in the editor ships it to tokens.generated.css.`,
167
166
  );
168
167
  } else if (result.dryRun) {
169
168
  lines.push(`\nDry run: nothing written under ${relative(root, dirname(result.themePath))}.`);
170
169
  } else {
171
170
  lines.push(
172
- `\nNot opened (--no-activate). Your unsaved edits are still open, so the next ` +
173
- `save-theme starts from the same look. Load "${result.slug}" from the editor's Theme panel to see it.`,
171
+ `\nNot loaded (--no-activate). The buffer is still open, so the next ` +
172
+ `save-theme starts from the same theme.`,
174
173
  );
175
174
  }
176
175
  return lines.join('\n');
@@ -28,9 +28,9 @@ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
28
28
  const ENGINE = resolve(pkgRoot, 'dist-plugin/setColors/index.js');
29
29
 
30
30
  const SOURCE_LABELS = {
31
- working: 'your unsaved edits',
31
+ working: 'the buffer',
32
32
  theme: 'the open theme',
33
- default: 'the package default',
33
+ default: 'the shipped default',
34
34
  };
35
35
 
36
36
  async function loadEngine() {
@@ -142,7 +142,7 @@ export function formatSetColorsResult(result) {
142
142
  if (result.ignoredName !== null) {
143
143
  lines.push(
144
144
  `Ignored "name": "${result.ignoredName}". The base color file no longer names a theme; ` +
145
- `name it when you run save-theme.`,
145
+ `name it in save-theme.`,
146
146
  );
147
147
  }
148
148
 
@@ -156,13 +156,13 @@ export function formatSetColorsResult(result) {
156
156
  );
157
157
  }
158
158
  if (result.report.failures.length > 0) {
159
- lines.push(`\nUnmet floors — adjust the base colors and re-run:`);
159
+ lines.push(`\nUnmet floors. Adjust the base colors and re-run:`);
160
160
  for (const f of result.report.failures) lines.push(` ! ${f}`);
161
161
  }
162
162
 
163
163
  lines.push(
164
164
  result.report.gradients === 'carried'
165
- ? '\nGradients: kept your tuned swatch gradients.'
165
+ ? '\nGradients: kept the tuned swatch gradients.'
166
166
  : '\nGradients: swatch tokens rebuilt from the theme families.',
167
167
  );
168
168
  lines.push(`Shadows: ${result.report.shadows}; carried geometry kept.`);
@@ -172,17 +172,15 @@ export function formatSetColorsResult(result) {
172
172
 
173
173
  if (result.wrote === 'buffer') {
174
174
  lines.push(
175
- `\nReload the app to see it. This is an unsaved edit: save the open theme in the ` +
176
- `editor's Theme panel to keep it, or run save-theme to write a new one.`,
175
+ `\nThe buffer holds this edit. Run save-theme to keep it as a theme.`,
177
176
  );
178
177
  } else if (result.wrote === 'cleared') {
179
178
  const held =
180
- result.savedSource === 'theme' ? `theme "${result.openTheme}"` : 'the package default';
179
+ result.savedSource === 'theme' ? `theme "${result.openTheme}"` : 'the shipped default';
181
180
  lines.push(
182
181
  result.source === 'working'
183
- ? `\nThat is what ${held} already holds, so the unsaved buffer was discarded. ` +
184
- `Reload the app to see it.`
185
- : `\nThat is what ${held} already holds, and there was no unsaved buffer, so nothing was written.`,
182
+ ? `\nThat is what ${held} already holds, so the buffer was discarded.`
183
+ : `\nThat is what ${held} already holds, and there was no buffer, so nothing was written.`,
186
184
  );
187
185
  } else if (result.dryRun) {
188
186
  lines.push(`\nDry run: nothing written under ${relative(root, result.colorsAndTypeDir)}.`);
@@ -17,15 +17,16 @@ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
17
17
  const ENGINE = resolve(pkgRoot, 'dist-plugin/setGeometry/index.js');
18
18
 
19
19
  const SOURCE_LABELS = {
20
- working: 'your unsaved edits',
20
+ working: 'the buffer',
21
21
  theme: 'the open theme',
22
22
  };
23
23
 
24
24
  const SKIP_LABELS = [
25
- ['raw-value', 'raw value, not a token'],
26
- ['off-ladder', 'off the ladder'],
27
- ['clamped', 'already at the ladder end'],
25
+ ['raw-value', 'raw value'],
26
+ ['off-scale', 'off the scale'],
27
+ ['clamped', 'at the end of the scale'],
28
28
  ['pill-preserved', 'pill preserved (pass "full": true to move it)'],
29
+ ['none-preserved', 'no line drawn, preserved (use "set" to draw one)'],
29
30
  ];
30
31
 
31
32
  async function loadEngine() {
@@ -174,7 +175,7 @@ export function formatSetGeometryResult(result) {
174
175
 
175
176
  const width = Math.max(0, ...entry.changes.map((c) => c.variable.length));
176
177
  for (const c of entry.changes) {
177
- lines.push(` ${c.variable.padEnd(width)} ${c.from} → ${c.to}`);
178
+ lines.push(` ${c.variable.padEnd(width)} ${c.from} to ${c.to}`);
178
179
  }
179
180
 
180
181
  for (const [reason, label] of SKIP_LABELS) {
@@ -189,8 +190,7 @@ export function formatSetGeometryResult(result) {
189
190
  );
190
191
  if (result.buffered) {
191
192
  lines.push(
192
- `Reload the app to see it. This is an unsaved edit: save the open theme in the ` +
193
- `editor's Theme panel to keep it, or load a theme to discard it.`,
193
+ `The buffer holds this edit. Run save-theme to keep it as a theme.`,
194
194
  );
195
195
  } else if (result.dryRun) {
196
196
  lines.push(`Dry run: nothing written under ${relative(root, result.configsDir)}.`);
package/bin/set-type.mjs CHANGED
@@ -24,9 +24,9 @@ const BROWSER_UA =
24
24
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
25
25
 
26
26
  const SOURCE_LABELS = {
27
- working: 'your unsaved edits',
27
+ working: 'the buffer',
28
28
  theme: 'the open theme',
29
- default: 'the package default',
29
+ default: 'the shipped default',
30
30
  };
31
31
 
32
32
  async function loadEngine() {
@@ -196,7 +196,7 @@ export function formatSetTypeResult(result) {
196
196
 
197
197
  const width = Math.max(0, ...result.changes.map((c) => c.variable.length));
198
198
  for (const change of result.changes) {
199
- lines.push(` ${change.variable.padEnd(width)} ${change.from ?? '(none)'} → ${change.to}`);
199
+ lines.push(` ${change.variable.padEnd(width)} ${change.from ?? '(none)'} to ${change.to}`);
200
200
  }
201
201
 
202
202
  for (const face of result.faces) {
@@ -231,13 +231,11 @@ export function formatSetTypeResult(result) {
231
231
 
232
232
  if (result.wrote === 'buffer') {
233
233
  lines.push(
234
- `\nReload the app to see it. This is an unsaved edit: save the open theme in the ` +
235
- `editor's Theme panel to keep it, or load a theme to discard it.`,
234
+ `\nThe buffer holds this edit. Run save-theme to keep it as a theme.`,
236
235
  );
237
236
  } else if (result.wrote === 'cleared') {
238
237
  lines.push(
239
- `\nThat is what the open theme already holds, so the unsaved buffer was discarded. ` +
240
- `Reload the app to see it.`,
238
+ `\nThat is what the open theme already holds, so the buffer was discarded.`,
241
239
  );
242
240
  } else if (result.dryRun) {
243
241
  lines.push(`\nDry run: nothing written under ${relative(root, result.colorsAndTypeDir)}.`);
@@ -0,0 +1,110 @@
1
+ // Installs the bundled Claude Code skills into a project's ./.claude/skills/.
2
+ //
3
+ // Claude Code discovers skills only under .claude/skills/, never inside
4
+ // node_modules, so the package's own copy has to be duplicated into each
5
+ // project that wants them. That makes this command the only thing keeping the
6
+ // two trees in step, and `--force` has to mean "make the destination match this
7
+ // release" rather than "write over the files I happen to ship today":
8
+ //
9
+ // - a skill this release renamed leaves its old directory behind, where it
10
+ // shadows the new one with stale instructions;
11
+ // - a reference file this release dropped survives inside a skill that is
12
+ // otherwise current, because cpSync merges into an existing directory.
13
+ //
14
+ // Both are invisible until a model reads the stale text, so --force replaces
15
+ // each directory outright and prunes the ones this release no longer ships.
16
+
17
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+
20
+ // The package owns this prefix in a project's skills directory. A skill without
21
+ // it is the project's own and is never touched, whatever the flags say.
22
+ const SKILL_PREFIX = 'live-tokens-';
23
+
24
+ const SAMPLE_PROMPTS = {
25
+ 'live-tokens-create-page': 'build a pricing page using live-tokens components',
26
+ 'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
27
+ 'live-tokens-create-component': 'author a new Toggle component for my live-tokens project',
28
+ 'live-tokens-create-theme': 'make me a bright and cheerful theme',
29
+ 'live-tokens-set-colors': 'give me a cooler palette, same fonts',
30
+ 'live-tokens-set-type': 'pair some fonts for this theme',
31
+ 'live-tokens-set-geometry': 'make the buttons pill shaped',
32
+ 'live-tokens-fix-findings': 'make check:design pass',
33
+ 'live-tokens-check-compliance': 'check this project against the design system',
34
+ };
35
+
36
+ function directoriesIn(dir) {
37
+ if (!existsSync(dir)) return [];
38
+ return readdirSync(dir).filter((name) => statSync(join(dir, name)).isDirectory());
39
+ }
40
+
41
+ export function runSetupClaude({ pkgRoot, cwd, force = false }) {
42
+ const srcSkills = join(pkgRoot, '.claude', 'skills');
43
+
44
+ if (!existsSync(srcSkills)) {
45
+ throw new Error(`No bundled skills found at ${srcSkills}. Is the package installed correctly?`);
46
+ }
47
+
48
+ const shipped = directoriesIn(srcSkills);
49
+ if (shipped.length === 0) {
50
+ throw new Error('No bundled skills to install.');
51
+ }
52
+
53
+ const destSkills = join(cwd, '.claude', 'skills');
54
+ mkdirSync(destSkills, { recursive: true });
55
+
56
+ const installed = [];
57
+ const skipped = [];
58
+ const removed = [];
59
+
60
+ for (const skill of shipped) {
61
+ const dest = join(destSkills, skill);
62
+ if (existsSync(dest) && !force) {
63
+ skipped.push(skill);
64
+ continue;
65
+ }
66
+ // Replace rather than merge: a file this release dropped must not survive
67
+ // inside a directory that otherwise looks current.
68
+ rmSync(dest, { recursive: true, force: true });
69
+ cpSync(join(srcSkills, skill), dest, { recursive: true });
70
+ installed.push(skill);
71
+ }
72
+
73
+ if (force) {
74
+ for (const name of directoriesIn(destSkills)) {
75
+ if (!name.startsWith(SKILL_PREFIX) || shipped.includes(name)) continue;
76
+ rmSync(join(destSkills, name), { recursive: true, force: true });
77
+ removed.push(name);
78
+ }
79
+ }
80
+
81
+ return { installed, skipped, removed, destSkills, shipped };
82
+ }
83
+
84
+ export function formatSetupResult({ installed, skipped, removed, destSkills, shipped }) {
85
+ const lines = [];
86
+
87
+ for (const skill of shipped) {
88
+ if (installed.includes(skill)) lines.push(` ok ${skill}`);
89
+ else lines.push(` skip ${skill} (already exists; pass --force to overwrite)`);
90
+ }
91
+ for (const skill of removed) {
92
+ lines.push(` gone ${skill} (this release no longer ships it; removed)`);
93
+ }
94
+
95
+ const counts = [`${installed.length} installed`, `${skipped.length} skipped`];
96
+ if (removed.length > 0) counts.push(`${removed.length} removed`);
97
+ lines.push(`\n${counts.join(', ')}, in ${destSkills}`);
98
+
99
+ if (skipped.length > 0 && removed.length === 0) {
100
+ lines.push(`\nRe-run with --force to bring every skill up to this release.`);
101
+ }
102
+
103
+ const samples = shipped.map((s) => SAMPLE_PROMPTS[s] && [s, SAMPLE_PROMPTS[s]]).filter(Boolean);
104
+ if (samples.length > 0) {
105
+ lines.push(`\nIn Claude Code, prompts like these auto-trigger the matching skill:`);
106
+ for (const [skill, prompt] of samples) lines.push(` "${prompt}"\n ${skill}`);
107
+ }
108
+
109
+ return lines.join('\n');
110
+ }