@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
@@ -43,6 +43,60 @@ export const COMPONENT_RULES = {
43
43
  'default-not-token': 'error',
44
44
  'phantom-link': 'warn',
45
45
  'dimension-literal': 'warn',
46
+ // `--tests` (bin/contractRunner.mjs). Fixed by design decision 8 so
47
+ // `fix-findings` can map them; every one is an error, including the setup
48
+ // rules, which `--tests` treats as never-silenceable (see cli.mjs).
49
+ 'contract-registry': 'error',
50
+ 'contract-render': 'error',
51
+ 'contract-alias': 'error',
52
+ 'contract-persist': 'error',
53
+ 'contract-theme': 'error',
54
+ 'contract-states': 'error',
55
+ 'contract-interaction': 'error',
56
+ 'contract-listed': 'error',
57
+ 'contract-sketch': 'error',
58
+ 'tests-not-installed': 'error',
59
+ 'tests-setup': 'error',
60
+ 'tests-incomplete': 'error',
61
+ };
62
+
63
+ /**
64
+ * Where a rule is fixed, as a stable slug carried on the finding. The skills
65
+ * resolve the slug to one of their own headings, so adding a rule here costs
66
+ * no skill edit as long as it reuses a slug. Two skills used to enumerate every
67
+ * rule id in their own tables, with nothing holding the two in agreement or
68
+ * checking either against this list.
69
+ */
70
+ export const COMPONENT_RULE_FIX = {
71
+ 'unknown-suffix': 'property-name',
72
+ 'state-after-property': 'property-name',
73
+ 'disabled-is-terminal': 'property-name',
74
+ 'default-not-token': 'property-token',
75
+ 'color-literal': 'property-token',
76
+ 'dimension-literal': 'property-token',
77
+ 'unknown-token-ref': 'property-token',
78
+ 'contract-theme': 'property-token',
79
+ 'invalid-id': 'runtime',
80
+ 'missing-file': 'runtime',
81
+ 'missing-root-block': 'runtime',
82
+ 'no-tokens': 'runtime',
83
+ 'contract-persist': 'runtime-defaults',
84
+ 'missing-component-const': 'editor',
85
+ 'missing-all-tokens': 'editor',
86
+ 'phantom-editor-token': 'editor',
87
+ 'phantom-link': 'editor',
88
+ 'deep-import': 'editor',
89
+ 'contract-render': 'editor',
90
+ 'contract-alias': 'editor',
91
+ 'contract-states': 'editor',
92
+ 'contract-interaction': 'editor',
93
+ 'missing-registration': 'registration',
94
+ 'contract-registry': 'registration',
95
+ 'contract-listed': 'registration',
96
+ 'contract-sketch': 'sketch',
97
+ 'tests-not-installed': 'tooling',
98
+ 'tests-setup': 'tooling',
99
+ 'tests-incomplete': 'coverage',
46
100
  };
47
101
 
48
102
  // Shipped components keep their editor beside the other editors; a
@@ -97,6 +151,35 @@ function capitalize(id) {
97
151
  return id.charAt(0).toUpperCase() + id.slice(1);
98
152
  }
99
153
 
154
+ /**
155
+ * Where `id`'s runtime and editor files live, resolved the same way
156
+ * `checkComponent` does: the real on-disk filename rather than a capitalised
157
+ * guess, since `cornerbadge` ships as `CornerBadge.svelte`. Exported so a
158
+ * caller outside the lint (the `--tests` runner, attributing a contract
159
+ * finding to a consumer artifact) doesn't re-derive it differently.
160
+ */
161
+ export function resolveComponentPaths(id, root = process.cwd()) {
162
+ // A consumer names a shipped id and owns no copy of its files. Falling back
163
+ // to the package keeps the lint reading the same source the contract run
164
+ // resolves, instead of reporting the component's own files missing. In this
165
+ // repo the consumer paths always exist, so the fallback never fires here.
166
+ const roots = builtInIds(root).has(id) ? [root, PKG_ROOT] : [root];
167
+ for (const base of roots) {
168
+ const dir = join(base, 'src/system/components');
169
+ const Id =
170
+ (existsSync(dir) ? readdirSync(dir) : [])
171
+ .find((f) => f.toLowerCase() === `${id}.svelte`)
172
+ ?.replace('.svelte', '') ?? capitalize(id);
173
+ const runtimePath = join(dir, `${Id}.svelte`);
174
+ const editorPath =
175
+ EDITOR_DIRS.map((d) => join(base, d, `${Id}Editor.svelte`)).find(existsSync) ??
176
+ join(base, EDITOR_DIRS[0], `${Id}Editor.svelte`);
177
+ if (base === roots.at(-1) || (existsSync(runtimePath) && existsSync(editorPath))) {
178
+ return { Id, runtimePath, editorPath };
179
+ }
180
+ }
181
+ }
182
+
100
183
  function extractImports(source) {
101
184
  const out = [];
102
185
  const re = /import\s+(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/g;
@@ -267,10 +350,10 @@ function checkDefaultsAreSemantic({ blocks, runtime, editor, root, runtimePath,
267
350
  record(
268
351
  'unknown-token-ref',
269
352
  STATE_TOKENS.includes(ref.replace(/^--/, '').split('-')[0])
270
- ? `${rel}: ${name} reads ${ref}, but a state is a segment of a property name, not a token of its own; read the token the state should paint`
353
+ ? `${rel}: ${name} reads ${ref}, but a state is a segment of a property name. Read the token the state should paint`
271
354
  : isContractToken(ref)
272
- ? `${rel}: ${name} reads ${ref}, which looks like a theme token but no longer exists; check tokens.css for a rename`
273
- : `${rel}: ${name} reads ${ref}, which is not a theme token or a component token`,
355
+ ? `${rel}: ${name} reads ${ref}, which has the shape of a design token but no longer exists. Check tokens.css for a rename`
356
+ : `${rel}: ${name} reads ${ref}, which is not a design token or a semantic property`,
274
357
  at,
275
358
  );
276
359
  }
@@ -278,7 +361,7 @@ function checkDefaultsAreSemantic({ blocks, runtime, editor, root, runtimePath,
278
361
  if (hasColorLiteral(painted)) {
279
362
  record(
280
363
  'color-literal',
281
- `${rel}: ${name}: ${value} is a colour literal; defaults must reference theme tokens (e.g. var(--surface-primary))`,
364
+ `${rel}: ${name}: ${value} is a colour literal; defaults must reference design tokens (e.g. var(--surface-primary))`,
282
365
  at,
283
366
  );
284
367
  continue;
@@ -287,7 +370,7 @@ function checkDefaultsAreSemantic({ blocks, runtime, editor, root, runtimePath,
287
370
  if (refs.length === 0 && !matchers.some((re) => re.test(name))) {
288
371
  record(
289
372
  'default-not-token',
290
- `${rel}: ${name}: ${value} has no theme token behind it; back it with a token, or declare it in the editor's \`intrinsics\` if it is a structural keyword`,
373
+ `${rel}: ${name}: ${value} has no design token behind it. Back it with a token, or declare it in the editor's \`intrinsics\` when it is a structural keyword`,
291
374
  at,
292
375
  );
293
376
  }
@@ -326,18 +409,7 @@ export function checkComponent(id, root = process.cwd(), { vocabulary } = {}) {
326
409
  return done();
327
410
  }
328
411
 
329
- // Resolve the real filename rather than capitalising the id: `cornerbadge`
330
- // ships as `CornerBadge.svelte`, and the casing is what gives the kebab form
331
- // of its token prefix.
332
- const dir = join(root, 'src/system/components');
333
- const Id =
334
- (existsSync(dir) ? readdirSync(dir) : [])
335
- .find((f) => f.toLowerCase() === `${id}.svelte`)
336
- ?.replace('.svelte', '') ?? capitalize(id);
337
- const runtimePath = join(dir, `${Id}.svelte`);
338
- const editorPath =
339
- EDITOR_DIRS.map((d) => join(root, d, `${Id}Editor.svelte`)).find(existsSync) ??
340
- join(root, EDITOR_DIRS[0], `${Id}Editor.svelte`);
412
+ const { Id, runtimePath, editorPath } = resolveComponentPaths(id, root);
341
413
 
342
414
  file = relative(root, runtimePath);
343
415
  if (!existsSync(runtimePath)) {
@@ -381,7 +453,7 @@ export function checkComponent(id, root = process.cwd(), { vocabulary } = {}) {
381
453
  record(
382
454
  'state-after-property',
383
455
  `${relative(root, runtimePath)}: ${token} has '${trailingState}' after the property; ` +
384
- `state must come before property (e.g. -${trailingState}-surface, not -surface-${trailingState})`,
456
+ `state must come before property (e.g. -${trailingState}-surface)`,
385
457
  runtime.indexOf(token),
386
458
  );
387
459
  }
@@ -512,7 +584,7 @@ export function checkComponent(id, root = process.cwd(), { vocabulary } = {}) {
512
584
  if (!registrationFile) {
513
585
  record(
514
586
  'missing-registration',
515
- `no registration for '${id}' under src/ — expected registerComponent({ id: '${id}', ... }) or bootLiveTokens({ components: [{ id: '${id}', ... }] })`,
587
+ `no registration for '${id}' under src/. Expected registerComponent({ id: '${id}', ... }) or bootLiveTokens({ components: [{ id: '${id}', ... }] })`,
516
588
  );
517
589
  } else {
518
590
  // Check the registration file's imports too.
@@ -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,6 +48,13 @@ 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
59
  // token scale, and `set-geometry` moves them. Sizing (a hero's height, a
50
60
  // column's minimum width, a max content width) is layout, has no scale, and
@@ -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)) {