@motion-proto/live-tokens 0.73.0 → 0.75.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 (196) 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 +84 -0
  13. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +94 -0
  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 +149 -0
  17. package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +80 -0
  18. package/.claude/skills/live-tokens-set-geometry/SKILL.md +121 -0
  19. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +63 -0
  20. package/.claude/skills/live-tokens-set-type/SKILL.md +93 -0
  21. package/.claude/skills/live-tokens-set-type/references/type-anchors.md +60 -0
  22. package/CHANGELOG.md +194 -0
  23. package/README.md +27 -18
  24. package/bin/check-component.mjs +91 -19
  25. package/bin/check-page.mjs +105 -24
  26. package/bin/cli.mjs +154 -97
  27. package/bin/contractRunner.mjs +945 -0
  28. package/bin/create.mjs +1 -1
  29. package/bin/lib/catalogue.mjs +37 -30
  30. package/bin/lib/findings.mjs +46 -15
  31. package/bin/lib/liveState.mjs +110 -0
  32. package/bin/lib/report.mjs +3 -3
  33. package/bin/lib/tokenVocabulary.mjs +4 -4
  34. package/bin/migrate-routes.mjs +5 -5
  35. package/bin/migrate.mjs +4 -4
  36. package/bin/save-theme.mjs +176 -0
  37. package/bin/set-colors.mjs +189 -0
  38. package/bin/{adjust.mjs → set-geometry.mjs} +25 -57
  39. package/bin/{set-fonts.mjs → set-type.mjs} +26 -62
  40. package/dist-plugin/{chunk-W6Y4BWFB.js → chunk-6WGFOJXO.js} +22 -0
  41. package/dist-plugin/{chunk-RIXO2E55.js → chunk-PDNL4NC5.js} +9 -2
  42. package/dist-plugin/{chunk-YLCOIGQC.js → chunk-SWXRVZKT.js} +98 -2
  43. package/dist-plugin/{dataPaths-BhWzd5cL.d.ts → dataPaths-BpIK_Obx.d.cts} +1 -0
  44. package/dist-plugin/{dataPaths-BhWzd5cL.d.cts → dataPaths-BpIK_Obx.d.ts} +1 -0
  45. package/dist-plugin/index.cjs +366 -148
  46. package/dist-plugin/index.d.cts +1 -1
  47. package/dist-plugin/index.d.ts +1 -1
  48. package/dist-plugin/index.js +226 -130
  49. package/dist-plugin/migrateData/index.cjs +121 -2
  50. package/dist-plugin/migrateData/index.d.cts +1 -1
  51. package/dist-plugin/migrateData/index.d.ts +1 -1
  52. package/dist-plugin/migrateData/index.js +3 -3
  53. package/dist-plugin/{generateColorsAndType → setColors}/index.cjs +594 -499
  54. package/dist-plugin/{generateColorsAndType → setColors}/index.d.cts +34 -25
  55. package/dist-plugin/{generateColorsAndType → setColors}/index.d.ts +34 -25
  56. package/dist-plugin/{generateColorsAndType → setColors}/index.js +48 -66
  57. package/dist-plugin/{adjust → setGeometry}/index.cjs +166 -48
  58. package/dist-plugin/{adjust → setGeometry}/index.d.cts +6 -6
  59. package/dist-plugin/{adjust → setGeometry}/index.d.ts +6 -6
  60. package/dist-plugin/{adjust → setGeometry}/index.js +51 -45
  61. package/dist-plugin/{fontPairing → setType}/index.cjs +19 -4
  62. package/dist-plugin/{fontPairing → setType}/index.d.cts +2 -2
  63. package/dist-plugin/{fontPairing → setType}/index.d.ts +2 -2
  64. package/dist-plugin/{fontPairing → setType}/index.js +1 -1
  65. package/dist-plugin/{themeTypes-DSV3Zisf.d.cts → themeTypes-BxRtuN5V.d.cts} +1 -1
  66. package/dist-plugin/{themeTypes-DSV3Zisf.d.ts → themeTypes-BxRtuN5V.d.ts} +1 -1
  67. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  68. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  69. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  70. package/package.json +36 -8
  71. package/src/app/site.css +19 -9
  72. package/src/editor/bootstrap.ts +2 -0
  73. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +4 -4
  74. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  75. package/src/editor/component-editor/MenuSelectEditor.svelte +4 -1
  76. package/src/editor/component-editor/SegmentedControlEditor.svelte +6 -1
  77. package/src/editor/component-editor/TabBarEditor.svelte +1 -1
  78. package/src/editor/component-editor/TableEditor.svelte +2 -2
  79. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +6 -3
  80. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +41 -20
  81. package/src/editor/core/components/adjustAliases.ts +59 -45
  82. package/src/editor/core/components/aliasKinds.ts +9 -5
  83. package/src/editor/core/preview/themePreview.ts +9 -2
  84. package/src/editor/core/sketch/sketchLayer.ts +22 -0
  85. package/src/editor/core/store/editorStore.ts +10 -1
  86. package/src/editor/core/themes/{generateColorsAndType.ts → buildColors.ts} +84 -98
  87. package/src/editor/core/themes/liveStateStream.ts +26 -0
  88. package/src/editor/core/themes/migrations/2026-09-03-drop-legacy-component-keys.ts +62 -0
  89. package/src/editor/core/themes/migrations/2026-09-07-stroke-role-renames.ts +52 -0
  90. package/src/editor/core/themes/migrations/index.ts +4 -0
  91. package/src/editor/core/themes/themeDocumentSync.ts +22 -11
  92. package/src/editor/core/themes/themeService.ts +9 -2
  93. package/src/editor/docs/content/themes-workflow.md +2 -2
  94. package/src/editor/docs/content.generated.ts +1 -1
  95. package/src/editor/overlay/LiveEditorOverlay.svelte +519 -28
  96. package/src/editor/pages/ComponentEditorPage.svelte +17 -1
  97. package/src/editor/pages/liveTokensEditorHandle.ts +23 -0
  98. package/src/editor/skill-atlas/SkillAtlas.svelte +76 -574
  99. package/src/editor/skill-atlas/TreeCanvas.svelte +263 -0
  100. package/src/editor/skill-atlas/TreeNodeCard.svelte +136 -53
  101. package/src/editor/skill-atlas/edges.ts +31 -0
  102. package/src/editor/skill-atlas/skillSources.generated.ts +29 -24
  103. package/src/editor/skill-atlas/skillTrees.ts +19 -3687
  104. package/src/editor/skill-atlas/trees/check-compliance.ts +183 -0
  105. package/src/editor/skill-atlas/trees/create-component.ts +275 -0
  106. package/src/editor/skill-atlas/trees/create-page.ts +320 -0
  107. package/src/editor/skill-atlas/trees/create-theme.ts +255 -0
  108. package/src/editor/skill-atlas/trees/fix-findings.ts +469 -0
  109. package/src/editor/skill-atlas/trees/pick-component.ts +300 -0
  110. package/src/editor/skill-atlas/trees/set-colors.ts +148 -0
  111. package/src/editor/skill-atlas/trees/set-geometry.ts +136 -0
  112. package/src/editor/skill-atlas/trees/set-type.ts +142 -0
  113. package/src/editor/skill-atlas/types.ts +3 -4
  114. package/src/editor/skill-atlas/wireLayout.ts +287 -0
  115. package/src/live-tokens/data/colors-and-type/autumn.json +1 -37
  116. package/src/live-tokens/data/colors-and-type/default.json +1 -37
  117. package/src/live-tokens/data/colors-and-type/halloween.json +1 -37
  118. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -37
  119. package/src/live-tokens/data/colors-and-type/ocean.json +1 -37
  120. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -37
  121. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -37
  122. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -37
  123. package/src/live-tokens/data/colors-and-type/sunset.json +1 -37
  124. package/src/live-tokens/data/themes/autumn.json +16 -52
  125. package/src/live-tokens/data/themes/halloween.json +16 -52
  126. package/src/live-tokens/data/themes/midnight-study.json +16 -52
  127. package/src/live-tokens/data/themes/ocean.json +16 -52
  128. package/src/live-tokens/data/themes/royal-velvet.json +16 -52
  129. package/src/live-tokens/data/themes/sketchy.json +16 -52
  130. package/src/live-tokens/data/themes/spring-meadow.json +16 -52
  131. package/src/live-tokens/data/themes/sunset.json +16 -52
  132. package/src/live-tokens/data/tokens.generated.css +0 -36
  133. package/src/system/components/Badge.svelte +7 -0
  134. package/src/system/components/Button.svelte +7 -0
  135. package/src/system/components/Callout.svelte +10 -6
  136. package/src/system/components/Card.svelte +23 -4
  137. package/src/system/components/CodeSnippet.svelte +4 -3
  138. package/src/system/components/CollapsibleSection.svelte +23 -8
  139. package/src/system/components/CornerBadge.svelte +6 -0
  140. package/src/system/components/Dialog.svelte +13 -6
  141. package/src/system/components/IconButton.svelte +9 -0
  142. package/src/system/components/Image.svelte +8 -0
  143. package/src/system/components/ImageLightbox.svelte +6 -0
  144. package/src/system/components/InlineEditActions.svelte +7 -0
  145. package/src/system/components/Input.svelte +7 -0
  146. package/src/system/components/MenuSelect.svelte +7 -0
  147. package/src/system/components/Notification.svelte +7 -0
  148. package/src/system/components/Panel.svelte +6 -0
  149. package/src/system/components/ProgressBar.svelte +5 -0
  150. package/src/system/components/RadioButton.svelte +11 -5
  151. package/src/system/components/SectionDivider.svelte +8 -0
  152. package/src/system/components/SegmentedControl.svelte +6 -0
  153. package/src/system/components/SideNavigation.svelte +6 -0
  154. package/src/system/components/Slider.svelte +7 -4
  155. package/src/system/components/TabBar.svelte +15 -9
  156. package/src/system/components/Table.svelte +8 -3
  157. package/src/system/components/Toggle.svelte +4 -4
  158. package/src/system/components/Tooltip.svelte +6 -0
  159. package/src/testing-js/chunk-AO7EZHYV.js +776 -0
  160. package/src/testing-js/chunk-AO7EZHYV.js.map +1 -0
  161. package/src/testing-js/chunk-FAFOAWYL.js +39 -0
  162. package/src/testing-js/chunk-FAFOAWYL.js.map +1 -0
  163. package/src/testing-js/chunk-L73N4NSO.js +23 -0
  164. package/src/testing-js/chunk-L73N4NSO.js.map +1 -0
  165. package/src/testing-js/chunk-LXR3MN6N.js +3063 -0
  166. package/src/testing-js/chunk-LXR3MN6N.js.map +1 -0
  167. package/src/testing-js/chunk-ZMSX6CXR.js +53 -0
  168. package/src/testing-js/chunk-ZMSX6CXR.js.map +1 -0
  169. package/src/testing-js/component-alias.contract.js +81 -0
  170. package/src/testing-js/component-alias.contract.js.map +1 -0
  171. package/src/testing-js/component-editor.contract.js +62 -0
  172. package/src/testing-js/component-editor.contract.js.map +1 -0
  173. package/src/testing-js/component-render.contract.js +568 -0
  174. package/src/testing-js/component-render.contract.js.map +1 -0
  175. package/src/testing-js/index.d.ts +293 -0
  176. package/src/testing-js/index.js +222 -0
  177. package/src/testing-js/index.js.map +1 -0
  178. package/src/testing-js/registry.contract.js +39 -0
  179. package/src/testing-js/registry.contract.js.map +1 -0
  180. package/src/testing-js/vitest-BE6uGF31.d.ts +73 -0
  181. package/src/testing-js/vitest.d.ts +3 -0
  182. package/src/testing-js/vitest.js +13 -0
  183. package/src/testing-js/vitest.js.map +1 -0
  184. package/template/README.md +13 -0
  185. package/template/_gitignore +2 -0
  186. package/template/package.json +2 -1
  187. package/template/src/pages/Home.svelte +4 -18
  188. package/.claude/skills/live-tokens-adjust-geometry/SKILL.md +0 -93
  189. package/.claude/skills/live-tokens-build-page/SKILL.md +0 -103
  190. package/.claude/skills/live-tokens-build-page/references/layout-sources.md +0 -48
  191. package/.claude/skills/live-tokens-generate-theme/SKILL.md +0 -156
  192. package/.claude/skills/live-tokens-generate-theme/references/mood-vocabulary.md +0 -43
  193. package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +0 -18
  194. package/.claude/skills/live-tokens-generate-theme/references/style-vocabulary.md +0 -35
  195. package/.claude/skills/live-tokens-pair-fonts/SKILL.md +0 -90
  196. package/bin/generate-theme.mjs +0 -260
@@ -1,7 +1,7 @@
1
1
  // Static validator for a live-tokens page.
2
2
  //
3
3
  // Asserts that a page satisfies the contract described in the
4
- // live-tokens-build-page skill: it is assembled from catalogue components, and
4
+ // live-tokens-create-page skill: it is assembled from catalogue components, and
5
5
  // every value in its CSS is a theme token rather than a literal. The rules and
6
6
  // their default severities are in PAGE_RULES; each is overridable per project
7
7
  // (live-tokens.config.json) or per run (--off/--warn/--error/--strict), because
@@ -26,10 +26,13 @@ export const PAGE_RULES = {
26
26
  'color-literal': 'error',
27
27
  'reserved-route': 'error',
28
28
  'site-css-in-main': 'error',
29
+ 'raw-text-axis': 'error',
29
30
  'dimension-literal': 'warn',
30
31
  'hardcoded-columns': 'warn',
31
- 'raw-text-axis': 'warn',
32
32
  'missing-source': 'warn',
33
+ 'control-size': 'warn',
34
+ 'multiple-primary': 'warn',
35
+ 'danger-without-dialog': 'warn',
33
36
  };
34
37
 
35
38
  // Directories that hold the system, not pages built on it.
@@ -45,10 +48,17 @@ const DEEP_IMPORT_PATTERNS = [
45
48
 
46
49
  const TEXT_AXES = ['font-size', 'font-family', 'font-weight', 'line-height', 'letter-spacing'];
47
50
 
51
+ // The single-axis scales in tokens.css. A text style bundle carries its axis
52
+ // as a suffix (--body-md-font-size, --code-font-family), so no bundle name
53
+ // matches, and neither does a custom property the page declares itself. A
54
+ // weight alone cannot move the scale or the fonts, so --font-weight-* is not one.
55
+ const SINGLE_AXIS_TOKEN =
56
+ /^--(?:font-size|line-height|letter-spacing)-|^--font-(?:sans|serif|mono|display|editorial)$/;
57
+
48
58
  // The geometry the theme owns: spacing, stroke, radius, and shadow all have a
49
- // token scale, and `adjust` moves them. Sizing (a hero's height, a column's
50
- // minimum width, a max content width) is layout, has no scale, and stays
51
- // literal.
59
+ // token scale, and `set-geometry` moves them. Sizing (a hero's height, a
60
+ // column's minimum width, a max content width) is layout, has no scale, and
61
+ // stays literal.
52
62
  const THEMED_GEOMETRY = /^(padding|margin|gap|row-gap|column-gap|border|outline|inset|top|right|bottom|left|box-shadow|text-shadow)(-|$)|-radius$/;
53
63
 
54
64
  // A local two-up or three-up is a layout. From four columns on, a hardcoded
@@ -184,7 +194,10 @@ function tagAttributes(code, start) {
184
194
  return { attrs, end: tagEnd };
185
195
  }
186
196
 
187
- /** Props a page passes that the component does not declare, or values outside a prop's union. */
197
+ /**
198
+ * Props a page passes that the component does not declare, values outside a
199
+ * prop's union, and a shipped component the page sizes itself.
200
+ */
188
201
  function checkComponentUsage(code, imports, add) {
189
202
  for (const [local, entry] of imports) {
190
203
  const props = entry.props;
@@ -203,11 +216,64 @@ function checkComponentUsage(code, imports, add) {
203
216
  if (allowed && value !== null && !allowed.has(value)) {
204
217
  add('unknown-prop-value', index, `${entry.name} ${name}="${value}" is not one of ${[...allowed].join(', ')}`);
205
218
  }
219
+ if (name === 'size' && entry.origin === 'shipped') {
220
+ add(
221
+ 'control-size',
222
+ index,
223
+ `${entry.name} ${value === null ? 'is sized here' : `size="${value}"`}. Drop it for the shipped default, or retune ${entry.name} for the whole project in /live-tokens/components.`,
224
+ );
225
+ }
206
226
  }
207
227
  }
208
228
  }
209
229
  }
210
230
 
231
+ /**
232
+ * One finding when a page holds more than one primary Button, at the second of
233
+ * them. Emphasis is what the variant carries, so a second primary leaves the
234
+ * page with no single most important action. A Button with no variant is
235
+ * primary, the component's default.
236
+ */
237
+ function checkPrimaryActions(code, imports, add) {
238
+ const primaries = [];
239
+ for (const [local, entry] of imports) {
240
+ if (entry.id !== 'button') continue;
241
+ for (const m of code.matchAll(new RegExp(`<${local}(?=[\\s/>])`, 'g'))) {
242
+ const tag = tagAttributes(code, m.index);
243
+ const variant = tag?.attrs.find((a) => a.name === 'variant');
244
+ if (tag && (variant === undefined || variant.value === 'primary')) primaries.push(m.index);
245
+ }
246
+ }
247
+ if (primaries.length < 2) return;
248
+ primaries.sort((a, b) => a - b);
249
+ add(
250
+ 'multiple-primary',
251
+ primaries[1],
252
+ `${primaries.length} primary Buttons in this page. Keep the most important action primary and make the other ${primaries.length - 1} secondary.`,
253
+ );
254
+ }
255
+
256
+ /**
257
+ * One finding when a page holds a danger Button and imports no Dialog. A
258
+ * danger action destroys saved work, and the page has nothing to confirm it.
259
+ */
260
+ function checkDestructiveActions(code, imports, add) {
261
+ if ([...imports.values()].some((entry) => entry.id === 'dialog')) return;
262
+ for (const [local, entry] of imports) {
263
+ if (entry.id !== 'button' && entry.id !== 'iconbutton') continue;
264
+ for (const m of code.matchAll(new RegExp(`<${local}(?=[\\s/>])`, 'g'))) {
265
+ const tag = tagAttributes(code, m.index);
266
+ if (!tag?.attrs.some((a) => a.name === 'variant' && a.value === 'danger')) continue;
267
+ add(
268
+ 'danger-without-dialog',
269
+ m.index,
270
+ `${entry.name} variant="danger" with no Dialog in this page. A destructive action confirms in a Dialog before it runs.`,
271
+ );
272
+ return;
273
+ }
274
+ }
275
+ }
276
+
211
277
  /** The object literal enclosing `index`, found by balancing braces outward. */
212
278
  function enclosingObject(text, index) {
213
279
  let depth = 0;
@@ -267,6 +333,8 @@ function checkFile(file, text, vocab, root) {
267
333
  if (local) imports.set(local, entry);
268
334
  }
269
335
  checkComponentUsage(code, imports, add);
336
+ checkPrimaryActions(code, imports, add);
337
+ checkDestructiveActions(code, imports, add);
270
338
 
271
339
  for (const m of code.matchAll(/['"](\/live-tokens[^'"]*)['"]\s*:/g)) {
272
340
  add('reserved-route', m.index, `route '${m[1]}' is inside the reserved /live-tokens/* namespace`);
@@ -315,8 +383,8 @@ function checkFile(file, text, vocab, root) {
315
383
  'unknown-token',
316
384
  at(m.index),
317
385
  isContractToken(name)
318
- ? `${name} looks like a theme token but no longer exists; check tokens.css for a rename`
319
- : `${name} is not a theme token, a component token, or declared in this file`,
386
+ ? `${name} has the shape of a design token but no longer exists. Check tokens.css for a rename`
387
+ : `${name} is not a design token, a semantic property, or declared in this file`,
320
388
  );
321
389
  }
322
390
 
@@ -328,25 +396,38 @@ function checkFile(file, text, vocab, root) {
328
396
  // inside one is not the page's value.
329
397
  const painted = stripVarFallbacks(value);
330
398
  if (!TEXT_AXES.includes(prop) && hasColorLiteral(painted)) {
331
- add('color-literal', at(index), `${prop}: ${value}. Use a theme token, not a colour literal.`);
399
+ add('color-literal', at(index), `${prop}: ${value}. Use a design token.`);
332
400
  continue;
333
401
  }
334
402
 
335
- // Only absolute type values are a finding. `em`, `%`, and a unitless
336
- // line-height are relative to the inherited type, so they ride whatever
337
- // the theme sets rather than overriding it.
338
- if (
339
- (TEXT_AXES.includes(prop) || prop === 'font') &&
340
- !value.includes('var(') &&
341
- !/^(inherit|initial|unset|normal)$/.test(value) &&
342
- /\d(px|rem|pt)\b|^[a-z"']/i.test(value)
343
- ) {
344
- add(
345
- 'raw-text-axis',
346
- at(index),
347
- `${prop}: ${value}. Set type from a text style bundle (--heading-*, --body-*, --editorial-*).`,
348
- );
349
- continue;
403
+ if (TEXT_AXES.includes(prop) || prop === 'font') {
404
+ const axis = [...painted.matchAll(/var\(\s*(--[a-z0-9-]+)/g)]
405
+ .map((m) => m[1])
406
+ .find((name) => SINGLE_AXIS_TOKEN.test(name));
407
+ if (axis) {
408
+ add(
409
+ 'raw-text-axis',
410
+ at(index),
411
+ `${prop}: ${value}. ${axis} is one axis. Set every axis from one text style bundle (--heading-*, --body-*, --editorial-*, --code-*).`,
412
+ );
413
+ continue;
414
+ }
415
+
416
+ // Of the literals only absolute type values are a finding. `em`, `%`,
417
+ // and a unitless line-height are relative to the inherited type, so
418
+ // they ride whatever the theme sets rather than overriding it.
419
+ if (
420
+ !value.includes('var(') &&
421
+ !/^(inherit|initial|unset|normal)$/.test(value) &&
422
+ /\d(px|rem|pt)\b|^[a-z"']/i.test(value)
423
+ ) {
424
+ add(
425
+ 'raw-text-axis',
426
+ at(index),
427
+ `${prop}: ${value}. Set type from a text style bundle (--heading-*, --body-*, --editorial-*, --code-*).`,
428
+ );
429
+ continue;
430
+ }
350
431
  }
351
432
 
352
433
  if (THEMED_GEOMETRY.test(prop) && hasDimensionLiteral(painted)) {
package/bin/cli.mjs CHANGED
@@ -4,25 +4,27 @@
4
4
  // create <dir> Scaffold a new app that depends on this package.
5
5
  // setup-claude [--force] Copy bundled Claude Code skills into ./.claude/skills/.
6
6
  // components [id] List every component the project has, shipped and its own, with props and tokens.
7
- // tokens [--family <name>] List every theme token by family, with its value.
7
+ // tokens [--scale <name>] List every design token by scale, with its value.
8
8
  // report The project as facts: tokens read, components used, findings by rule. Always exits 0.
9
9
  // check-component [id] Validate a component (or every authored one) against the create-component skill contract.
10
- // check-page [paths...] Validate pages against the build-page skill contract.
11
- // generate-theme <brief> Build a theme from a 10-seed OKLCH brief and open it.
12
- // adjust <ops.json> Apply radius/padding/gap/border-width ops to the open buffer.
13
- // set-fonts <brief.json> Bind Google Fonts families to the theme's font stacks.
10
+ // check-page [paths...] Validate pages against the create-page skill contract.
11
+ // set-colors <colors> Build the color identity from 10 OKLCH base colors, into the open buffer.
12
+ // set-geometry <ops> Apply radius/padding/gap/border-width ops to the open buffer.
13
+ // set-type <pairing> Bind Google Fonts families to the theme's font stacks.
14
+ // save-theme <name> Compose the live state into themes/<slug>.json and open it.
14
15
  // migrate [...] Reconcile tokens.css, the data tree, and route references.
15
16
 
16
17
  import { cpSync, existsSync, mkdirSync, readdirSync, statSync, writeSync } from 'node:fs';
17
18
  import { dirname, join, resolve } from 'node:path';
18
19
  import { fileURLToPath } from 'node:url';
19
20
  import process from 'node:process';
20
- import { COMPONENT_RULES, checkComponent, discoverComponents, formatReport } from './check-component.mjs';
21
+ import { COMPONENT_RULES, COMPONENT_RULE_FIX, checkComponent, discoverComponents, formatReport } from './check-component.mjs';
21
22
  import { PAGE_RULES, checkPages, discoverPages } from './check-page.mjs';
22
23
  import { describeComponents, describeTokens, formatComponents, formatTokens } from './lib/catalogue.mjs';
23
24
  import { buildReport, formatReport as formatProjectReport } from './lib/report.mjs';
24
25
  import { loadVocabulary } from './lib/tokenVocabulary.mjs';
25
26
  import {
27
+ applyCoverageSeverity,
26
28
  applySeverity,
27
29
  countBySeverity,
28
30
  formatFindings,
@@ -38,38 +40,45 @@ import {
38
40
  } from './migrate.mjs';
39
41
  import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
40
42
  import { runCreate, formatCreateResult } from './create.mjs';
41
- import { runGenerateTheme, formatGenerateThemeResult } from './generate-theme.mjs';
42
- import { runAdjust, formatAdjustResult } from './adjust.mjs';
43
- import { runSetFonts, formatSetFontsResult } from './set-fonts.mjs';
43
+ import { runSetColors, formatSetColorsResult } from './set-colors.mjs';
44
+ import { runSetGeometry, formatSetGeometryResult } from './set-geometry.mjs';
45
+ import { runSetType, formatSetTypeResult } from './set-type.mjs';
46
+ import { runSaveTheme, formatSaveThemeResult } from './save-theme.mjs';
44
47
 
45
48
  const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
46
49
 
47
50
  Commands:
48
51
  create <dir> [--force] Scaffold a new Svelte + Vite app wired up with
49
- live-tokens (editor, components, theme tokens)
52
+ live-tokens (editor, components, design tokens)
50
53
  setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
51
54
  components [id] [--json] List every component the project has, shipped and
52
55
  its own (src/system/components plus any
53
- "componentDirs" in live-tokens.config.json), with
54
- the props each takes; with an id, that component's
55
- props, variants, tokens, and defaults
56
- tokens [--family <name>] [--json]
57
- List every theme token the project's tokens.css
58
- declares, by family, with its value
56
+ "componentDirs" in live-tokens.config.json). With
57
+ an id, that component's props, variants, tokens,
58
+ and defaults
59
+ tokens [--scale <name>] [--json]
60
+ List every design token the project's tokens.css
61
+ declares, by scale, with its value
59
62
  report [--json] The project as facts: pending migrations, tokens
60
63
  each component declares and reads, which page
61
64
  renders which component, and both checkers'
62
65
  findings by rule under the project's severities
63
- and under --strict. A reading, not a gate: always
64
- exits 0
65
- check-component [id] Validate <id>'s runtime, editor, and registration
66
- against the live-tokens-create-component contract
67
- check-page [paths...] Validate pages against the live-tokens-build-page
66
+ and under --strict. Always exits 0
67
+ check-component [id] [--tests]
68
+ Validate <id>'s runtime, editor, and registration
69
+ against the live-tokens-create-component contract.
70
+ --tests also runs the registry contract under
71
+ vitest and the component contract suites under
72
+ Playwright, reporting coverage by rule. Needs
73
+ @playwright/test, vitest, and happy-dom; a
74
+ missing one is a tests-not-installed finding
75
+ naming the install command
76
+ check-page [paths...] Validate pages against the live-tokens-create-page
68
77
  contract: catalogue components only, and every CSS
69
- value a theme token. Checks every page under src/
78
+ value a design token. Checks every page under src/
70
79
  when given no paths.
71
80
 
72
- Both check commands accept:
81
+ check-component and check-page also accept:
73
82
  --json Machine-readable findings, for a skill to iterate
74
83
  against until the exit code is 0
75
84
  --strict Treat warnings as errors
@@ -77,40 +86,47 @@ Both check commands accept:
77
86
  (or set "checks": { "rules": {...} } in
78
87
  live-tokens.config.json; "checks": { "exclude":
79
88
  [...] } drops paths from discovery entirely)
80
- generate-theme <brief.json> [--no-activate] [--dry-run] [--carry-from <name>]
81
- Build a full theme from a 10-seed OKLCH brief
82
- (see the live-tokens-generate-theme skill),
83
- enforce AA contrast on derived text tokens, write
84
- themes/<slug>.json, and open it in the editor.
85
- Opening never changes what your site ships; Adopt
86
- in the editor does that.
87
- --no-activate writes the theme without opening it;
88
- --dry-run prints the contrast report without
89
- writing. Non-color content (gradients, fonts,
90
- component aliases) carries forward from the live
91
- look, or from theme <name> with --carry-from.
92
- adjust <ops.json> [--dry-run]
89
+ set-colors <base-colors.json> [--dry-run]
90
+ Build the theme's whole color identity from 10
91
+ OKLCH base colors (see the live-tokens-set-colors
92
+ skill) and enforce AA contrast on the derived text
93
+ tokens. Reads the live colors and type and writes
94
+ the result to the colors-and-type buffer. Run
95
+ save-theme to keep it as a theme. Fonts and every
96
+ override no palette owns carry forward. --dry-run
97
+ prints the contrast report without writing.
98
+ set-geometry <ops.json> [--dry-run]
93
99
  Move radius, padding, gap, and border-width
94
100
  aliases along their token scales (see the
95
- live-tokens-adjust-geometry skill). Reads each
101
+ live-tokens-set-geometry skill). Reads each
96
102
  component's live config and writes the result to
97
- that component's unsaved buffer, so save the open
98
- theme in the editor to keep it. --dry-run prints
99
- the report without writing.
100
- set-fonts <brief.json> [--dry-run] [--no-verify]
103
+ that component's buffer. Run save-theme to keep it
104
+ as a theme. --dry-run prints the report without
105
+ writing.
106
+ set-type <pairing.json> [--dry-run] [--no-verify]
101
107
  Bind Google Fonts families to --font-display,
102
108
  --font-sans, --font-serif, --font-mono and
103
109
  --font-editorial (see
104
- the live-tokens-pair-fonts skill). Each family is
110
+ the live-tokens-set-type skill). Each family is
105
111
  verified against the Google Fonts API and the URL
106
- is negotiated from the weights it actually has.
107
- Writes the result to the unsaved colors-and-type
108
- buffer, so save the open theme in the editor to
109
- keep it. --dry-run prints the report without
110
- writing; --no-verify skips the network and
111
- requires an explicit URL per family.
112
+ is negotiated from the weights it has. Writes the
113
+ result to the colors-and-type buffer. Run
114
+ save-theme to keep it as a theme. --dry-run prints
115
+ the report without writing. --no-verify skips the
116
+ network and requires an explicit URL per family.
117
+ save-theme <name> [--no-activate] [--dry-run]
118
+ Compose the live state (the buffers, the open
119
+ theme under them, the shipped defaults under that)
120
+ into themes/<slug>.json and load it, which clears
121
+ the buffers. With no buffer it saves a copy of the
122
+ open theme under the new name. Loading never
123
+ changes what the site ships. Adopt in the editor
124
+ does that. --no-activate writes the theme and does
125
+ not load it, so a set of themes comes off one
126
+ starting theme. --dry-run prints the report
127
+ without writing.
112
128
  migrate [--check] [--write] [--tokens <path>]
113
- Reconcile your project with the installed package:
129
+ Reconcile the project with the installed package:
114
130
  applies additive tokens.css migrations, moves a
115
131
  pre-0.48 data tree onto the current directory
116
132
  names, heals what the retired pointer files named,
@@ -150,7 +166,7 @@ if (!command || command === '--help' || command === '-h') {
150
166
 
151
167
  const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
152
168
 
153
- if (command === 'create' || command === 'init') {
169
+ if (command === 'create') {
154
170
  const targetArg = rest.find((a) => !a.startsWith('-'));
155
171
  if (!targetArg) {
156
172
  fail(`Usage: npx @motion-proto/live-tokens create <project-directory>`);
@@ -166,14 +182,19 @@ if (command === 'create' || command === 'init') {
166
182
  }
167
183
  }
168
184
 
169
- function reportChecks(label, findings, checked, rules, opts) {
170
- const resolved = applySeverity(findings, rules, opts, readChecksConfig(process.cwd()));
185
+ function reportChecks(label, findings, checked, rules, opts, { coverage, hardFailure, fixes } = {}) {
186
+ const checksConfig = readChecksConfig(process.cwd());
187
+ const resolved = applySeverity(findings, rules, opts, checksConfig, fixes ?? {});
188
+ const resolvedCoverage = coverage ? applyCoverageSeverity(coverage, rules, opts, checksConfig) : coverage;
171
189
  console.log(
172
190
  opts.json
173
- ? toJson(resolved, { label, checked })
191
+ ? toJson(resolved, { label, checked, coverage: resolvedCoverage })
174
192
  : formatFindings(resolved, { label, checked }),
175
193
  );
176
- process.exit(countBySeverity(resolved).errors === 0 ? 0 : 1);
194
+ // Decision 2: a missing tool or a setup failure under --tests is an error
195
+ // even if a project silenced its rule id, since silencing it would read as
196
+ // "the tests passed" rather than "the tests did not run".
197
+ process.exit(countBySeverity(resolved).errors === 0 && !hardFailure ? 0 : 1);
177
198
  }
178
199
 
179
200
  if (command === 'components') {
@@ -187,14 +208,14 @@ if (command === 'components') {
187
208
 
188
209
  if (command === 'tokens') {
189
210
  const opts = parseCheckFlags(rest);
190
- const at = opts.rest.indexOf('--family');
191
- const family = at >= 0 ? opts.rest[at + 1] : undefined;
211
+ const at = opts.rest.indexOf('--scale');
212
+ const scale = at >= 0 ? opts.rest[at + 1] : undefined;
192
213
  const desc = describeTokens(loadVocabulary());
193
- if (family && !desc.families.some((f) => f.family === family)) fail(formatTokens(desc, { family }));
214
+ if (scale && !desc.scales.some((s) => s.scale === scale)) fail(formatTokens(desc, { scale }));
194
215
  writeOut(
195
216
  opts.json
196
- ? JSON.stringify(family ? desc.families.find((f) => f.family === family) : desc, null, 2)
197
- : formatTokens(desc, { family }),
217
+ ? JSON.stringify(scale ? desc.scales.find((s) => s.scale === scale) : desc, null, 2)
218
+ : formatTokens(desc, { scale }),
198
219
  );
199
220
  process.exit(0);
200
221
  }
@@ -220,18 +241,28 @@ if (command === 'report') {
220
241
  if (command === 'check-component') {
221
242
  const opts = parseCheckFlags(rest);
222
243
  const ids = opts.rest.length > 0 ? [opts.rest[0]] : discoverComponents();
223
- if (ids.length === 0) {
244
+ if (ids.length === 0 && !opts.tests) {
224
245
  console.log('✓ check-component: no component authored under src/system/components yet.');
225
246
  process.exit(0);
226
247
  }
227
248
  const results = ids.map((id) => [id, checkComponent(id)]);
228
- if (ids.length === 1 && !opts.json && !opts.strict && opts.off.length + opts.warn.length + opts.error.length === 0) {
249
+ if (!opts.tests && ids.length === 1 && !opts.json && !opts.strict && opts.off.length + opts.warn.length + opts.error.length === 0) {
229
250
  const [id, result] = results[0];
230
251
  console.log(formatReport(id, result));
231
252
  process.exit(result.errors.length === 0 ? 0 : 1);
232
253
  }
233
- const label = ids.length === 1 ? `check-component ${ids[0]}` : 'check-component';
234
- reportChecks(label, results.flatMap(([, r]) => r.findings), ids.length, COMPONENT_RULES, opts);
254
+ const label = ids.length === 1 ? `check-component ${ids[0]}${opts.tests ? ' --tests' : ''}` : `check-component${opts.tests ? ' --tests' : ''}`;
255
+ if (!opts.tests) {
256
+ reportChecks(label, results.flatMap(([, r]) => r.findings), ids.length, COMPONENT_RULES, opts, { fixes: COMPONENT_RULE_FIX });
257
+ }
258
+ const { hasHardFailure, runContractTests } = await import('./contractRunner.mjs');
259
+ const testOutcome = await runContractTests(opts.rest[0], { root: process.cwd() });
260
+ const findings = [...results.flatMap(([, r]) => r.findings), ...testOutcome.findings];
261
+ reportChecks(label, findings, Math.max(ids.length, 1), COMPONENT_RULES, opts, {
262
+ fixes: COMPONENT_RULE_FIX,
263
+ coverage: testOutcome.coverage,
264
+ hardFailure: hasHardFailure(testOutcome.findings),
265
+ });
235
266
  }
236
267
 
237
268
  if (command === 'check-page') {
@@ -241,72 +272,97 @@ if (command === 'check-page') {
241
272
  reportChecks('check-page', findings, checked, PAGE_RULES, opts);
242
273
  }
243
274
 
244
- if (command === 'generate-theme') {
245
- const briefPath = rest.find((a) => !a.startsWith('-'));
246
- if (!briefPath) {
247
- fail(`Usage: npx @motion-proto/live-tokens generate-theme <brief.json> [--no-activate] [--dry-run]`);
275
+ if (command === 'set-colors') {
276
+ const baseColorsPath = rest.find((a) => !a.startsWith('-'));
277
+ if (!baseColorsPath) {
278
+ fail(`Usage: npx @motion-proto/live-tokens set-colors <base-colors.json> [--dry-run]`);
279
+ }
280
+ if (rest.includes('--no-activate')) {
281
+ fail(
282
+ `set-colors has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
283
+ `Nothing is activated until save-theme, which takes the flag.`,
284
+ );
285
+ }
286
+ if (rest.includes('--carry-from')) {
287
+ fail(
288
+ `set-colors has no --carry-from: it reads the live theme and edits it in place, so a second theme ` +
289
+ `already starts from the first. Run save-theme --no-activate between themes.`,
290
+ );
248
291
  }
249
292
  try {
250
- const carryIdx = rest.indexOf('--carry-from');
251
- const carryFrom = carryIdx !== -1 ? rest[carryIdx + 1] : undefined;
252
- if (carryIdx !== -1 && !carryFrom) fail(`--carry-from requires a theme name`);
253
- const result = await runGenerateTheme({
254
- briefPath,
255
- activate: !rest.includes('--no-activate'),
293
+ const result = await runSetColors({
294
+ baseColorsPath,
256
295
  dryRun: rest.includes('--dry-run'),
257
- carryFrom,
258
296
  });
259
- console.log(formatGenerateThemeResult(result));
297
+ console.log(formatSetColorsResult(result));
260
298
  process.exit(result.report.failures.length === 0 ? 0 : 1);
261
299
  } catch (err) {
262
- fail(`generate-theme failed: ${err instanceof Error ? err.message : String(err)}`);
300
+ fail(`set-colors failed: ${err instanceof Error ? err.message : String(err)}`);
263
301
  }
264
302
  }
265
303
 
266
- if (command === 'adjust') {
304
+ if (command === 'set-geometry') {
267
305
  const opsPath = rest.find((a) => !a.startsWith('-'));
268
306
  if (!opsPath) {
269
- fail(`Usage: npx @motion-proto/live-tokens adjust <ops.json> [--dry-run]`);
307
+ fail(`Usage: npx @motion-proto/live-tokens set-geometry <ops.json> [--dry-run]`);
270
308
  }
271
309
  if (rest.includes('--no-activate')) {
272
310
  fail(
273
- `adjust has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
311
+ `set-geometry has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
274
312
  `Drop the flag and re-run.`,
275
313
  );
276
314
  }
277
315
  try {
278
- const result = await runAdjust({
316
+ const result = await runSetGeometry({
279
317
  opsPath,
280
318
  dryRun: rest.includes('--dry-run'),
281
319
  });
282
- console.log(formatAdjustResult(result));
320
+ console.log(formatSetGeometryResult(result));
283
321
  process.exit(0);
284
322
  } catch (err) {
285
- fail(`adjust failed: ${err instanceof Error ? err.message : String(err)}`);
323
+ fail(`set-geometry failed: ${err instanceof Error ? err.message : String(err)}`);
286
324
  }
287
325
  }
288
326
 
289
- if (command === 'set-fonts') {
290
- const briefPath = rest.find((a) => !a.startsWith('-'));
291
- if (!briefPath) {
292
- fail(`Usage: npx @motion-proto/live-tokens set-fonts <brief.json> [--dry-run] [--no-verify]`);
327
+ if (command === 'set-type') {
328
+ const pairingPath = rest.find((a) => !a.startsWith('-'));
329
+ if (!pairingPath) {
330
+ fail(`Usage: npx @motion-proto/live-tokens set-type <pairing.json> [--dry-run] [--no-verify]`);
293
331
  }
294
332
  if (rest.includes('--no-activate')) {
295
333
  fail(
296
- `set-fonts has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
334
+ `set-type has no --no-activate: it edits the open buffer, which is what the page already runs. ` +
297
335
  `Drop the flag and re-run.`,
298
336
  );
299
337
  }
300
338
  try {
301
- const result = await runSetFonts({
302
- briefPath,
339
+ const result = await runSetType({
340
+ pairingPath,
303
341
  dryRun: rest.includes('--dry-run'),
304
342
  verify: !rest.includes('--no-verify'),
305
343
  });
306
- console.log(formatSetFontsResult(result));
344
+ console.log(formatSetTypeResult(result));
345
+ process.exit(0);
346
+ } catch (err) {
347
+ fail(`set-type failed: ${err instanceof Error ? err.message : String(err)}`);
348
+ }
349
+ }
350
+
351
+ if (command === 'save-theme') {
352
+ const name = rest.find((a) => !a.startsWith('-'));
353
+ if (!name) {
354
+ fail(`Usage: npx @motion-proto/live-tokens save-theme <name> [--no-activate] [--dry-run]`);
355
+ }
356
+ try {
357
+ const result = await runSaveTheme({
358
+ name,
359
+ activate: !rest.includes('--no-activate'),
360
+ dryRun: rest.includes('--dry-run'),
361
+ });
362
+ console.log(formatSaveThemeResult(result));
307
363
  process.exit(0);
308
364
  } catch (err) {
309
- fail(`set-fonts failed: ${err instanceof Error ? err.message : String(err)}`);
365
+ fail(`save-theme failed: ${err instanceof Error ? err.message : String(err)}`);
310
366
  }
311
367
  }
312
368
 
@@ -385,15 +441,16 @@ for (const skill of skills) {
385
441
  installed++;
386
442
  }
387
443
 
388
- console.log(`\n${installed} installed, ${skipped} skipped → ${destSkills}`);
444
+ console.log(`\n${installed} installed, ${skipped} skipped, in ${destSkills}`);
389
445
 
390
446
  const SAMPLE_PROMPTS = {
391
- 'live-tokens-build-page': 'build a pricing page using live-tokens components',
447
+ 'live-tokens-create-page': 'build a pricing page using live-tokens components',
392
448
  'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
393
449
  'live-tokens-create-component': 'author a new Toggle component for my live-tokens project',
394
- 'live-tokens-generate-theme': 'make me a bright and cheerful theme',
395
- 'live-tokens-adjust-geometry': 'make the buttons pill shaped',
396
- 'live-tokens-pair-fonts': 'pair some fonts for this theme',
450
+ 'live-tokens-create-theme': 'make me a bright and cheerful theme',
451
+ 'live-tokens-set-colors': 'give me a cooler palette, same fonts',
452
+ 'live-tokens-set-type': 'pair some fonts for this theme',
453
+ 'live-tokens-set-geometry': 'make the buttons pill shaped',
397
454
  'live-tokens-fix-findings': 'make check:design pass',
398
455
  'live-tokens-check-compliance': 'check this project against the design system',
399
456
  };
@@ -405,6 +462,6 @@ const installedSamples = skills
405
462
  if (installedSamples.length > 0) {
406
463
  console.log(`\nIn Claude Code, prompts like these auto-trigger the matching skill:`);
407
464
  for (const [skill, prompt] of installedSamples) {
408
- console.log(` • "${prompt}"\n → ${skill}`);
465
+ console.log(` "${prompt}"\n ${skill}`);
409
466
  }
410
467
  }