@motion-proto/live-tokens 0.71.0 → 0.72.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 (65) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +7 -1
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
  3. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +99 -0
  4. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +1 -2
  5. package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -0
  6. package/CHANGELOG.md +77 -0
  7. package/README.md +1 -1
  8. package/bin/check-component.mjs +2 -11
  9. package/bin/check-page.mjs +3 -2
  10. package/bin/cli.mjs +2 -1
  11. package/bin/lib/catalogue.mjs +6 -2
  12. package/bin/lib/findings.mjs +17 -0
  13. package/bin/lib/report.mjs +1 -1
  14. package/bin/lib/tokenVocabulary.mjs +20 -0
  15. package/dist-plugin/adjust/index.cjs +33 -6
  16. package/dist-plugin/adjust/index.d.cts +2 -2
  17. package/dist-plugin/adjust/index.d.ts +2 -2
  18. package/dist-plugin/adjust/index.js +7 -6
  19. package/dist-plugin/{chunk-2YNERPXY.js → chunk-RIXO2E55.js} +1 -1
  20. package/dist-plugin/{chunk-RVE3MNKM.js → chunk-W6Y4BWFB.js} +3 -1
  21. package/dist-plugin/{chunk-GPIBU44G.js → chunk-YLCOIGQC.js} +25 -1
  22. package/dist-plugin/{dataPaths-bJTCEO4H.d.ts → dataPaths-BhWzd5cL.d.cts} +4 -0
  23. package/dist-plugin/{dataPaths-bJTCEO4H.d.cts → dataPaths-BhWzd5cL.d.ts} +4 -0
  24. package/dist-plugin/fontPairing/index.cjs +3 -1
  25. package/dist-plugin/fontPairing/index.d.cts +1 -1
  26. package/dist-plugin/fontPairing/index.d.ts +1 -1
  27. package/dist-plugin/fontPairing/index.js +1 -1
  28. package/dist-plugin/generateColorsAndType/index.cjs +28 -2
  29. package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
  30. package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
  31. package/dist-plugin/generateColorsAndType/index.js +2 -2
  32. package/dist-plugin/index.cjs +28 -2
  33. package/dist-plugin/index.d.cts +1 -1
  34. package/dist-plugin/index.d.ts +1 -1
  35. package/dist-plugin/index.js +3 -3
  36. package/dist-plugin/migrateData/index.cjs +28 -2
  37. package/dist-plugin/migrateData/index.d.cts +1 -1
  38. package/dist-plugin/migrateData/index.d.ts +1 -1
  39. package/dist-plugin/migrateData/index.js +3 -3
  40. package/dist-plugin/tokensCssMigrations/index.cjs +3 -1
  41. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  42. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  43. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  44. package/package.json +5 -1
  45. package/src/editor/component-editor/SectionDividerEditor.svelte +1 -11
  46. package/src/editor/component-editor/contract.ts +175 -0
  47. package/src/editor/component-editor/scaffolding/StateBlock.svelte +0 -8
  48. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +5 -0
  49. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +0 -21
  50. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +2 -2
  51. package/src/editor/component-editor/scaffolding/types.ts +1 -7
  52. package/src/editor/core/components/aliasKinds.ts +6 -2
  53. package/src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts +39 -0
  54. package/src/editor/core/themes/migrations/index.ts +2 -0
  55. package/src/live-tokens/data/themes/autumn.json +1 -7
  56. package/src/live-tokens/data/themes/halloween.json +1 -7
  57. package/src/live-tokens/data/themes/midnight-study.json +1 -7
  58. package/src/live-tokens/data/themes/ocean.json +1 -7
  59. package/src/live-tokens/data/themes/royal-velvet.json +1 -7
  60. package/src/live-tokens/data/themes/sketchy.json +1 -7
  61. package/src/live-tokens/data/themes/spring-meadow.json +1 -7
  62. package/src/live-tokens/data/themes/sunset.json +1 -7
  63. package/src/system/components/SectionDivider.svelte +3 -143
  64. package/src/system/components/FloatingTokenTags.css +0 -284
  65. package/src/system/components/FloatingTokenTags.svelte +0 -570
@@ -52,7 +52,13 @@ in a line what the rule holds and which of two kinds the fix is:
52
52
  Where a finding looks deliberate, a translucent overlay on an app shell or a
53
53
  layout size the project owns, say so and name the config entry that would
54
54
  record the decision: `"checks": { "rules": { "<rule>": "warn" } }` in
55
- `live-tokens.config.json`. Recording it is the user's call, not yours.
55
+ `live-tokens.config.json`. Where a whole file is not a themed surface at all,
56
+ hand-tuned artwork or vendored CSS, the entry is
57
+ `"checks": { "exclude": ["src/art/hero.css"] }` — a project-relative path, a
58
+ directory covering what is under it, and naming the file on the command line
59
+ still checks it. Prefer the narrower one: an exclusion drops one file, a
60
+ severity change drops a rule everywhere. Recording either is the user's call,
61
+ not yours.
56
62
 
57
63
  ## Report
58
64
 
@@ -230,9 +230,9 @@ It *warns* (non-fatal) when a token-backed default still carries a px or rem ter
230
230
 
231
231
  Exit code 0 means the static contract is met. Resolve warnings before shipping, or run with `--strict` to make them fail. `--json` prints findings with a stable `rule` id, so you can work through one rule at a time and re-run.
232
232
 
233
- **Then run the registry contract test.** If you're authoring inside the package itself, `src/editor/component-editor/registryContract.test.ts` runs `describe.each(getComponentRegistryEntries())` and verifies, per component, that the registration resolves to a real `sourceFile` and a non-empty schema, that schema variables are unique, that every editable token (excluding `hidden: true`, `kind: 'gradient'`, and padding-side suffixes) is declared in the runtime `<style>` block and seeded in `src/live-tokens/data/component-configs/<id>/default.json`, and that `setComponentAlias` round-trips the alias through the slice.
233
+ **Then run the registry contract test.** `checkRegistryEntry`, from `@motion-proto/live-tokens/component-editor/contract`, takes one registry entry and returns a violation line per failure, so a suite over your own components is a `describe.each` and one call. Per component it verifies that the registration resolves to a real `sourceFile` and a non-empty schema, that schema variables are unique, that every editable token is declared in the runtime `<style>` block and seeded in `component-configs/<id>/default.json`, that a token declaring `minOpacity` seeds at or above its floor, and that `setComponentAlias` round-trips the alias through the slice. The test file, the two lines in it that are load-bearing, and the path options are in `references/contract-tests.md`.
234
234
 
235
- A new first-party component is auto-covered the moment it lands in `builtInRegistry` — `npm test` will fail if any of the five checks miss. For a consumer-authored component, mirror this pattern in your own test suite if you want the same drift protection: `getComponentRegistryEntries` is exported from `@motion-proto/live-tokens` and returns every registration, shipped and custom, once your `main.ts` has run.
235
+ Inside the package, `src/editor/component-editor/registryContract.test.ts` is that same file over `builtInRegistry`, so a new first-party component is auto-covered the moment it lands there — `npm test` fails if any check misses.
236
236
 
237
237
  **If your component declares `intrinsics`, the intrinsics contract test covers it too.** `src/editor/component-editor/intrinsicsContract.test.ts` iterates every entry with an `intrinsics` array and asserts, per (intrinsic, variant), that the runtime `:global(:root)` declares a default, the default is one of the spec's `values`, and the editor's `default` equals the runtime default. This is what would have caught a getter defaulting to `center` while `:global(:root)` says `start`. Same auto-coverage rule: declare `intrinsics` on the registry entry and the test picks it up.
238
238
 
@@ -0,0 +1,99 @@
1
+ # The registry contract as a test in your own project
2
+
3
+ `checkRegistryEntry` is the contract the package holds its own 26 components
4
+ to, exported so a project outside the package can run it over its own. It takes
5
+ one registry entry and returns a violation line per failure; an empty array is
6
+ the pass.
7
+
8
+ ```ts
9
+ // tests/registryContract.test.ts
10
+ // @vitest-environment happy-dom
11
+ import { describe, it, expect } from 'vitest';
12
+ import { getComponentRegistryEntries, registerComponent } from '@motion-proto/live-tokens';
13
+ import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';
14
+ import MyWidgetEditor, { allTokens } from '../src/system/components/MyWidgetEditor.svelte';
15
+
16
+ registerComponent({
17
+ id: 'mywidget',
18
+ label: 'My Widget',
19
+ icon: 'fas fa-magic',
20
+ sourceFile: 'src/system/components/MyWidget.svelte',
21
+ editorComponent: MyWidgetEditor,
22
+ schema: allTokens,
23
+ });
24
+
25
+ const mine = getComponentRegistryEntries().filter((e) => e.origin === 'custom');
26
+
27
+ describe.each(mine.map((e) => [e.id, e] as const))('%s', (_id, entry) => {
28
+ it('meets the registry contract', () => {
29
+ expect(checkRegistryEntry(entry)).toEqual([]);
30
+ });
31
+ });
32
+ ```
33
+
34
+ Two lines there are load-bearing.
35
+
36
+ - **Register at the top of the test file**, rather than importing `main.ts`.
37
+ The entries have to exist before `describe.each` reads them, and
38
+ `bootLiveTokens` would mount the app.
39
+ - **Filter on `origin`.** The registry always carries the shipped components
40
+ too, and their `sourceFile` paths are relative to the package root, not
41
+ yours. Without the filter every built-in fails on a path that does not exist
42
+ in your project.
43
+
44
+ ## Setup
45
+
46
+ `vitest` and `happy-dom` as devDependencies, and the svelte plugin already in
47
+ `vite.config.ts` so the editor `.svelte` import resolves. The helper reads the
48
+ runtime file and `default.json` off disk, which is why it is node-only and has
49
+ its own subpath.
50
+
51
+ The package ships Svelte and TypeScript source, and `bootLiveTokens` imports the
52
+ FontAwesome stylesheet. Left external, Node meets that `.css` and stops with
53
+ `Unknown file extension ".css"`, before a single test runs. Inline both so Vite
54
+ transforms them:
55
+
56
+ ```ts
57
+ // vitest.config.ts
58
+ import { defineConfig, mergeConfig } from 'vitest/config';
59
+ import viteConfig from './vite.config';
60
+
61
+ export default mergeConfig(
62
+ viteConfig,
63
+ defineConfig({
64
+ test: {
65
+ server: { deps: { inline: [/@motion-proto\/live-tokens/, /@fortawesome/] } },
66
+ },
67
+ }),
68
+ );
69
+ ```
70
+
71
+ ## Paths
72
+
73
+ Paths resolve against `process.cwd()` and `src/live-tokens/data/component-configs`.
74
+ A project that moved either passes them:
75
+
76
+ ```ts
77
+ checkRegistryEntry(entry, { projectRoot, componentConfigsDir });
78
+ ```
79
+
80
+ `componentConfigsDir` is the same directory `live-tokens.config.json` names.
81
+
82
+ ## What it holds
83
+
84
+ 1. **Registration** — `sourceFile` resolves to a real file, the schema is non-empty.
85
+ 2. **Uniqueness** — no schema variable is declared twice.
86
+ 3. **Editor to runtime** — every editable token's CSS var is declared in the
87
+ runtime's `<style>` block, so an edit has something to repaint.
88
+ 4. **Editor to default config** — every editable token has a seed alias in
89
+ `component-configs/<id>/default.json`, so the component adopts with full
90
+ defaults. A component with no `default.json` is editor-only; this check and
91
+ the next one skip it.
92
+ 5. **Opacity floors** — a token declaring `minOpacity` ships a default at or
93
+ above it, so a floating panel starts out legible over page content.
94
+ 6. **Round-trip** — `setComponentAlias` persists into the slice under the same key.
95
+
96
+ Checks 3 and 4 exclude `hidden: true` tokens, `kind: 'gradient'` tokens (stored
97
+ as gradient objects, not vars), and `-padding-(top|right|bottom|left)` suffixes
98
+ (written on demand by the split-padding UI and read through the `themed-padding`
99
+ mixin's fallback chain, so they exist as neither `:root` declarations nor seeds).
@@ -176,8 +176,7 @@ inherits, so one declaration covers everything under it:
176
176
 
177
177
  Travel is stated in px against a glyph whose size the layer cannot know, so the
178
178
  dial that suits a card's worth of artwork tears a 16px icon apart. Reach for the
179
- soft bank before reaching for `none`. The shipped `SectionDivider` is the worked
180
- example.
179
+ soft bank before reaching for `none`.
181
180
 
182
181
  ## First-party components
183
182
 
@@ -64,6 +64,12 @@ to that list, so the two cannot drift apart.
64
64
  | `-padding` | Internal spacing |
65
65
  | `-gap` | Spacing between sibling elements |
66
66
 
67
+ `-width`, `-height` and `-size` are the fall-through: any dimension with no
68
+ more specific name behind it. They read the `--space-*` scale through the same
69
+ picker `-gap` uses, and they match last, so `-border-width`, `-divider-height`,
70
+ `-icon-size` and the rest claim their token first. Reach for the specific name
71
+ when one fits; a stroke is `-border-width` even where the CSS says `outline:`.
72
+
67
73
  ## Typography
68
74
 
69
75
  | Suffix | Meaning |
package/CHANGELOG.md CHANGED
@@ -1,5 +1,82 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.72.0 — The contract a consumer can run
4
+
5
+ ### Added
6
+
7
+ - **The registry contract is a function a consumer can run.** The six
8
+ per-component checks that hold this package's own components — registration
9
+ resolves, schema variables are unique, every editable token is declared in
10
+ the runtime `<style>` and seeded in `default.json`, a declared `minOpacity`
11
+ is honoured, `setComponentAlias` round-trips — are now `checkRegistryEntry`,
12
+ exported from `@motion-proto/live-tokens/component-editor/contract`. It takes
13
+ one registry entry and returns a violation line per failure, so a project
14
+ outside this package holds its own components to the same contract with a
15
+ `describe.each` and one call. Paths resolve against `process.cwd()` and
16
+ `src/live-tokens/data/component-configs`, or the `projectRoot` and
17
+ `componentConfigsDir` passed in. Node-only, hence its own subpath.
18
+ `live-tokens-create-component` carries the test file in
19
+ `references/contract-tests.md`; filtering the registry on
20
+ `origin === 'custom'` is the line that matters, since the shipped entries
21
+ name paths relative to the package root.
22
+
23
+ - **`"checks": { "exclude": [...] }` in `live-tokens.config.json`.** Paths the
24
+ checkers skip when they discover their own targets: a project-relative path,
25
+ a directory covering what is under it. For a file that is not a themed
26
+ surface at all — hand-tuned artwork, vendored CSS — where the only other way
27
+ out was downgrading a rule for the whole project. Naming the file on the
28
+ command line still checks it, so the escape hatch cannot hide a file from
29
+ someone looking straight at it. The dev plugin now also recognises `checks`
30
+ and `componentDirs`, which the CLI has read for some time while the plugin
31
+ warned they were unknown keys.
32
+
33
+ ### Changed (breaking)
34
+
35
+ - **`FloatingTokenTags` is demo artwork, not a shipped component.** It moved to
36
+ `src/demo/`, so it leaves the published package, the component query and the
37
+ count, which now agree with the registry at 26. **Breaking for anyone
38
+ importing `@motion-proto/live-tokens/components/FloatingTokenTags.svelte`;**
39
+ it was only ever the hero animation of the demo, and it never had a
40
+ `:global(:root)` block, an editor, or a registry entry.
41
+
42
+ - **The SectionDivider title outline is removed.** It was the only reason the title was an
43
+ SVG `<text>` behind a `feMorphology` filter rather than an element, and that
44
+ SVG carried a `getBBox()` viewBox, a per-instance `MutationObserver` on the
45
+ document's inline style (filter primitives cannot read a CSS var, so the
46
+ resolved width and colour had to be read back and pushed onto them by hand),
47
+ font-load listeners to re-measure, and a title that was neither selectable nor
48
+ findable. It shipped transparent in the default and in all eight presets, and
49
+ the trap-out it was built for is done by the layout: the `through-label`
50
+ hairlines flank the title in a flex row, so no rule ever runs behind the
51
+ glyphs. The title is now a span that inherits typography like every other
52
+ component's. `--sectiondivider-{lg,md,sm}-title-outline-width` and
53
+ `-title-outline-color` are dropped by a component-config migration; the
54
+ `outlineWidthVariable` / `outlineColorVariable` rows on `TypeGroupConfig` go
55
+ with them, since nothing else declared one.
56
+
57
+ ### Changed
58
+
59
+ - **`registered` is read from the registry for shipped components too.** The
60
+ query assumed a component under `src/system/components` was registered, so
61
+ `live-tokens components` could name one the editor cannot open and
62
+ pick-component does not list. It now reads the package's own
63
+ `builtInRegistry`, the same parse `check-component` uses, which the two now
64
+ share. A component that is discovered but registered nowhere reports
65
+ `registered: false`, prints `(NOT registered)`, and is counted out of the
66
+ catalogue in the summary line.
67
+
68
+ ### Fixed
69
+
70
+ - **A bare `-width`, `-height` or `-size` no longer renders a colour picker.**
71
+ The three sat in `KIND_RULES` under `surface`, so `--widget-panel-width` drew
72
+ a palette while the naming vocabulary documented it as geometry. They are now
73
+ a `length` kind, matched last among the geometry rules so `-border-width`,
74
+ `-divider-height`, `-icon-size` and the rest still claim their token first,
75
+ and drawn with the `--space-*` picker `-gap` uses. The suffix vocabulary is
76
+ unchanged, so no token is renamed and no project has to migrate. `adjust`
77
+ does not take the kind: its ladders are density and shape, and a panel width
78
+ is neither.
79
+
3
80
  ## 0.71.0 — Check this project
4
81
 
5
82
  ### Added
package/README.md CHANGED
@@ -11,7 +11,7 @@ The editor is dev-only. Production builds get plain CSS variables and the compon
11
11
  ## Features
12
12
 
13
13
  - **Live token editing.** Colors, typography, spacing, radii, shadows, motion, palettes, and gradients. Every input writes a CSS variable, so the page repaints with no reload and no build step.
14
- - **Live component editing.** 26 shipped Svelte components (Button, IconButton, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, CodeSnippet, SideNavigation, Panel, InlineEditActions) declare their design-token aliases in a `:global(:root)` block. Rewire an alias from the component's editor and it updates everywhere that component is used, on your real pages.
14
+ - **Live component editing.** 26 shipped Svelte components (Button, IconButton, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, CodeSnippet, SideNavigation, Panel, InlineEditActions, Slider) declare their design-token aliases in a `:global(:root)` block. Rewire an alias from the component's editor and it updates everywhere that component is used, on your real pages.
15
15
  - **Four dev-only routes.** `/live-tokens/editor` for tokens, `/live-tokens/colors` for palettes, `/live-tokens/components` for per-component aliases, `/live-tokens/docs` for the user guide.
16
16
  - **Editor overlay.** Pins to the top right of every dev page and opens the editor in a side panel or floating window, so you edit on the page you are styling. Its "Page Source" button opens the current page's `.svelte` file in VS Code.
17
17
  - **Themes.** A theme is a whole look in one file: colors and type plus a config for every component, stored by value. Loading one changes a single pointer file, and nothing your site ships changes until you Adopt. Export a theme and import it into another project to restore the look in one step.
@@ -23,7 +23,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
23
23
  import { extname, join, relative } from 'node:path';
24
24
  import { hasColorLiteral, hasDimensionLiteral, stripVarFallbacks } from './lib/cssValues.mjs';
25
25
  import { lineOf } from './lib/findings.mjs';
26
- import { PKG_ROOT, declaredCustomProperties, extractGlobalRootBlocks, isContractToken, loadVocabulary } from './lib/tokenVocabulary.mjs';
26
+ import { PKG_ROOT, builtInIds, declaredCustomProperties, extractGlobalRootBlocks, isContractToken, loadVocabulary } from './lib/tokenVocabulary.mjs';
27
27
 
28
28
  export const COMPONENT_RULES = {
29
29
  'invalid-id': 'error',
@@ -71,18 +71,9 @@ function readKnownSuffixes(root) {
71
71
  return [];
72
72
  }
73
73
 
74
- const BUILT_IN_REGISTRY = 'src/editor/component-editor/registry.ts';
75
-
76
74
  /** True when `id` is one of the package's own components. */
77
75
  function isBuiltIn(id) {
78
- for (const base of [PKG_ROOT, process.cwd()]) {
79
- const path = join(base, BUILT_IN_REGISTRY);
80
- if (!existsSync(path)) continue;
81
- const src = readFileSync(path, 'utf8');
82
- const block = src.match(/builtInRegistry[^=]*=\s*Object\.freeze\(\{([\s\S]*?)\n\}\);/);
83
- if (block && new RegExp(`\\bid:\\s*'${id}'`).test(block[1])) return true;
84
- }
85
- return false;
76
+ return builtInIds(process.cwd(), PKG_ROOT).has(id);
86
77
  }
87
78
 
88
79
  // Per-side padding names (`--card-body-padding-top`) are written by the padding
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { existsSync, readFileSync, statSync } from 'node:fs';
14
14
  import { join, relative, resolve, basename } from 'node:path';
15
- import { lineOf } from './lib/findings.mjs';
15
+ import { isExcluded, lineOf } from './lib/findings.mjs';
16
16
  import { blankStrings, hasColorLiteral, hasDimensionLiteral, stripVarFallbacks } from './lib/cssValues.mjs';
17
17
  import { isContractToken, loadVocabulary, walk } from './lib/tokenVocabulary.mjs';
18
18
  import { resolveTokensCssPath } from './migrate.mjs';
@@ -382,6 +382,7 @@ export function discoverPages(root) {
382
382
  return walk(src, ['.svelte', '.css', '.ts', '.js']).filter((f) => {
383
383
  const rel = relative(root, f);
384
384
  if (NOT_PAGES.some((d) => rel.startsWith(`${d}/`))) return false;
385
+ if (isExcluded(rel, root)) return false;
385
386
  if (f === tokensCss || TOKEN_SOURCES.includes(basename(f))) return false;
386
387
  if (/\.(test|spec)\.[tj]s$/.test(rel)) return false;
387
388
  if (rel.endsWith('.ts') || rel.endsWith('.js')) return /main\.(ts|js)$/.test(rel);
@@ -396,7 +397,7 @@ export function checkPages(targets, { root = process.cwd(), vocabulary } = {}) {
396
397
  const full = resolve(root, t);
397
398
  if (!existsSync(full)) continue;
398
399
  if (statSync(full).isDirectory()) {
399
- files.push(...walk(full, ['.svelte', '.css']));
400
+ files.push(...walk(full, ['.svelte', '.css']).filter((f) => !isExcluded(relative(root, f), root)));
400
401
  } else {
401
402
  files.push(full);
402
403
  }
package/bin/cli.mjs CHANGED
@@ -75,7 +75,8 @@ Both check commands accept:
75
75
  --strict Treat warnings as errors
76
76
  --off=<rule,...> Silence rules; --warn=/--error= change severity
77
77
  (or set "checks": { "rules": {...} } in
78
- live-tokens.config.json)
78
+ live-tokens.config.json; "checks": { "exclude":
79
+ [...] } drops paths from discovery entirely)
79
80
  generate-theme <brief.json> [--no-activate] [--dry-run] [--carry-from <name>]
80
81
  Build a full theme from a 10-seed OKLCH brief
81
82
  (see the live-tokens-generate-theme skill),
@@ -44,7 +44,7 @@ export function describeComponents(vocab, { root = process.cwd() } = {}) {
44
44
  name: entry.name,
45
45
  origin: entry.origin,
46
46
  file: relative(root, entry.file),
47
- registered: entry.origin === 'shipped' || vocab.registered.has(entry.id),
47
+ registered: vocab.builtIn.has(entry.id) || vocab.registered.has(entry.id),
48
48
  description: descriptionOf(source),
49
49
  variants: entry.props?.enums.get('variant') ? [...entry.props.enums.get('variant')] : [],
50
50
  props,
@@ -99,7 +99,11 @@ export function formatComponents(list, { id } = {}) {
99
99
  if (c.description) lines.push(`${''.padEnd(29)} ${c.description}`);
100
100
  }
101
101
  lines.push('');
102
- lines.push(`${list.length} component(s). \`live-tokens components <id>\` prints one with its props and tokens.`);
102
+ const unregistered = list.filter((c) => !c.registered).length;
103
+ lines.push(
104
+ `${list.length} component(s)${unregistered ? `, ${unregistered} not registered and so not in the catalogue` : ''}. ` +
105
+ '`live-tokens components <id>` prints one with its props and tokens.',
106
+ );
103
107
  return lines.join('\n');
104
108
  }
105
109
 
@@ -30,6 +30,23 @@ export function readChecksConfig(root) {
30
30
  }
31
31
  }
32
32
 
33
+ /**
34
+ * `checks.exclude` in live-tokens.config.json: paths the checkers do not read
35
+ * when they discover their own targets. Each entry is a project-relative path;
36
+ * a directory entry covers everything under it. For a file that is not a themed
37
+ * surface — hand-tuned artwork, vendored CSS — where the alternative is
38
+ * downgrading a rule for the whole project. Naming the file explicitly on the
39
+ * command line still checks it.
40
+ */
41
+ export function isExcluded(relPath, root) {
42
+ const list = readChecksConfig(root).exclude;
43
+ if (!Array.isArray(list)) return false;
44
+ return list.some((entry) => {
45
+ const e = String(entry).replace(/\/+$/, '');
46
+ return relPath === e || relPath.startsWith(`${e}/`);
47
+ });
48
+ }
49
+
33
50
  /**
34
51
  * Parse `--off=a,b --warn=c --error=d --strict --json` out of argv.
35
52
  * Unrecognised flags are returned in `rest` for the caller to handle.
@@ -61,7 +61,7 @@ export function buildReport(vocab, { root = process.cwd() } = {}) {
61
61
  name: c.name,
62
62
  origin: c.origin,
63
63
  file: relative(root, c.file),
64
- registered: c.origin === 'shipped' || vocab.registered.has(c.id),
64
+ registered: vocab.builtIn.has(c.id) || vocab.registered.has(c.id),
65
65
  described: /^\s*<!--[\s\S]*?-->/.test(source),
66
66
  tokens: c.tokens.size,
67
67
  unread: unreadTokens(source, c.tokens.keys()),
@@ -164,6 +164,24 @@ function componentFiles(roots) {
164
164
  return files;
165
165
  }
166
166
 
167
+ /** The package's own component ids, read from the frozen registry that declares
168
+ them. A shipped component is registered by the package rather than by the
169
+ project, so it never appears in the project's own `registerComponent` scan. */
170
+ const BUILT_IN_REGISTRY = 'src/editor/component-editor/registry.ts';
171
+
172
+ export function builtInIds(root = process.cwd(), pkgRoot = PKG_ROOT) {
173
+ const ids = new Set();
174
+ for (const base of [pkgRoot, root]) {
175
+ const path = join(base, BUILT_IN_REGISTRY);
176
+ if (!existsSync(path)) continue;
177
+ const block = readFileSync(path, 'utf8')
178
+ .match(/builtInRegistry[^=]*=\s*Object\.freeze\(\{([\s\S]*?)\n\}\);/);
179
+ if (!block) continue;
180
+ for (const m of block[1].matchAll(/\bid:\s*'([a-z][a-z0-9]*)'/g)) ids.add(m[1]);
181
+ }
182
+ return ids;
183
+ }
184
+
167
185
  function registeredIds(root) {
168
186
  const ids = new Set();
169
187
  for (const file of walk(join(root, 'src'), ['.ts', '.js', '.mjs', '.svelte'])) {
@@ -223,12 +241,14 @@ export function loadVocabulary({ root = process.cwd(), pkgRoot = PKG_ROOT } = {}
223
241
  });
224
242
  }
225
243
  const registered = registeredIds(root);
244
+ const builtIn = builtInIds(root, pkgRoot);
226
245
 
227
246
  return {
228
247
  themeTokens,
229
248
  componentTokens,
230
249
  components,
231
250
  registered,
251
+ builtIn,
232
252
  tokensCssPath,
233
253
  /** True when `name` resolves to something real at runtime. */
234
254
  knows(name) {
@@ -81,6 +81,10 @@ var KIND_RULES = [
81
81
  prefix: ["--border-width-"]
82
82
  },
83
83
  { kind: "border", suffix: ["-border"], prefix: ["--border-"] },
84
+ // A dimension with no more specific name behind it — a panel's width, an
85
+ // avatar's size. Last of the geometry rules, so every `-border-width`,
86
+ // `-divider-height`, `-icon-size` and the rest claim their token first.
87
+ { kind: "length", suffix: ["-width", "-height", "-size"] },
84
88
  // Fills. A tint is a wash over a surface, so it takes the surface picker: the
85
89
  // full palette with an alpha, not just the tint stops it defaults to.
86
90
  {
@@ -95,10 +99,7 @@ var KIND_RULES = [
95
99
  "-accent",
96
100
  "-color",
97
101
  "-tint",
98
- "-opacity",
99
- "-width",
100
- "-height",
101
- "-size"
102
+ "-opacity"
102
103
  ],
103
104
  prefix: ["--surface-", "--tint", "--color-"]
104
105
  }
@@ -281,7 +282,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
281
282
  "componentConfigsDir",
282
283
  "themesDir",
283
284
  "manifestsDir",
284
- "tokensCssPath"
285
+ "tokensCssPath",
286
+ "componentDirs",
287
+ "checks"
285
288
  ]);
286
289
  var cached = null;
287
290
  function readLiveTokensConfig() {
@@ -1125,6 +1128,29 @@ var componentMigration_2026_09_01_gateSuffixEnabled = {
1125
1128
  }
1126
1129
  };
1127
1130
 
1131
+ // src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts
1132
+ var DROPPED2 = new Set(
1133
+ ["lg", "md", "sm"].flatMap((v) => [
1134
+ `--sectiondivider-${v}-title-outline-width`,
1135
+ `--sectiondivider-${v}-title-outline-color`
1136
+ ])
1137
+ );
1138
+ var componentMigration_2026_09_02_sectiondividerDropTitleOutline = {
1139
+ id: "2026-09-02-sectiondivider-drop-title-outline",
1140
+ fromVersion: 25,
1141
+ toVersion: 26,
1142
+ appliesTo: "component-config",
1143
+ apply(rawVars, meta) {
1144
+ if (meta.component !== "sectiondivider") return { ...rawVars };
1145
+ const out = {};
1146
+ for (const [key, value] of Object.entries(rawVars)) {
1147
+ if (DROPPED2.has(key)) continue;
1148
+ out[key] = value;
1149
+ }
1150
+ return out;
1151
+ }
1152
+ };
1153
+
1128
1154
  // src/editor/core/themes/migrations/index.ts
1129
1155
  var MIGRATIONS = [
1130
1156
  colorsAndTypeMigration_2026_04_24_legacyKeysAndBgToCanvas,
@@ -1158,7 +1184,8 @@ var MIGRATIONS = [
1158
1184
  colorsAndTypeMigration_2026_09_01_tintRename,
1159
1185
  componentMigration_2026_09_01_tintRename,
1160
1186
  componentMigration_2026_09_01_tabbarActiveTint,
1161
- componentMigration_2026_09_01_gateSuffixEnabled
1187
+ componentMigration_2026_09_01_gateSuffixEnabled,
1188
+ componentMigration_2026_09_02_sectiondividerDropTitleOutline
1162
1189
  ];
1163
1190
  function countFor(kind) {
1164
1191
  return MIGRATIONS.filter((m) => m.appliesTo === kind).length;
@@ -1,5 +1,5 @@
1
1
  import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-DSV3Zisf.cjs';
2
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-bJTCEO4H.cjs';
2
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-BhWzd5cL.cjs';
3
3
  export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-4N-Orzzi.cjs';
4
4
 
5
5
  type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
@@ -37,7 +37,7 @@ interface AdjustResult {
37
37
  }
38
38
  declare function adjustAliases(configs: Record<string, ComponentConfig>, ops: readonly AdjustOp[], now: string): AdjustResult;
39
39
 
40
- type TokenKind = 'surface' | 'border' | 'border-width' | 'radius' | 'divider-width' | 'divider-height' | 'divider-inset' | 'dot-size' | 'blur' | 'scale' | 'shadow' | 'font-family' | 'font-weight' | 'font-size' | 'line-height' | 'letter-spacing' | 'padding' | 'gap' | 'duration' | 'easing' | 'text-color';
40
+ type TokenKind = 'surface' | 'border' | 'border-width' | 'radius' | 'divider-width' | 'divider-height' | 'divider-inset' | 'dot-size' | 'length' | 'blur' | 'scale' | 'shadow' | 'font-family' | 'font-weight' | 'font-size' | 'line-height' | 'letter-spacing' | 'padding' | 'gap' | 'duration' | 'easing' | 'text-color';
41
41
  /** Side-aware kind test. Per-side paddings (`--card-default-body-padding-top`)
42
42
  are written by UIPaddingSelector and never declared as editor tokens, so
43
43
  `rawKind` never meets one; the CLI does, and they belong with their parent. */
@@ -1,5 +1,5 @@
1
1
  import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-DSV3Zisf.js';
2
- export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-bJTCEO4H.js';
2
+ export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-BhWzd5cL.js';
3
3
  export { C as CURRENT_COMPONENT_SCHEMA_VERSION } from '../index-4N-Orzzi.js';
4
4
 
5
5
  type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
@@ -37,7 +37,7 @@ interface AdjustResult {
37
37
  }
38
38
  declare function adjustAliases(configs: Record<string, ComponentConfig>, ops: readonly AdjustOp[], now: string): AdjustResult;
39
39
 
40
- type TokenKind = 'surface' | 'border' | 'border-width' | 'radius' | 'divider-width' | 'divider-height' | 'divider-inset' | 'dot-size' | 'blur' | 'scale' | 'shadow' | 'font-family' | 'font-weight' | 'font-size' | 'line-height' | 'letter-spacing' | 'padding' | 'gap' | 'duration' | 'easing' | 'text-color';
40
+ type TokenKind = 'surface' | 'border' | 'border-width' | 'radius' | 'divider-width' | 'divider-height' | 'divider-inset' | 'dot-size' | 'length' | 'blur' | 'scale' | 'shadow' | 'font-family' | 'font-weight' | 'font-size' | 'line-height' | 'letter-spacing' | 'padding' | 'gap' | 'duration' | 'easing' | 'text-color';
41
41
  /** Side-aware kind test. Per-side paddings (`--card-default-body-padding-top`)
42
42
  are written by UIPaddingSelector and never declared as editor tokens, so
43
43
  `rawKind` never meets one; the CLI does, and they belong with their parent. */
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  CURRENT_COMPONENT_SCHEMA_VERSION
3
- } from "../chunk-GPIBU44G.js";
3
+ } from "../chunk-YLCOIGQC.js";
4
4
  import {
5
5
  readLiveTokensConfig,
6
6
  resolveDataDirs
7
- } from "../chunk-RVE3MNKM.js";
7
+ } from "../chunk-W6Y4BWFB.js";
8
8
 
9
9
  // src/editor/core/components/aliasKinds.ts
10
10
  var KIND_RULES = [
@@ -49,6 +49,10 @@ var KIND_RULES = [
49
49
  prefix: ["--border-width-"]
50
50
  },
51
51
  { kind: "border", suffix: ["-border"], prefix: ["--border-"] },
52
+ // A dimension with no more specific name behind it — a panel's width, an
53
+ // avatar's size. Last of the geometry rules, so every `-border-width`,
54
+ // `-divider-height`, `-icon-size` and the rest claim their token first.
55
+ { kind: "length", suffix: ["-width", "-height", "-size"] },
52
56
  // Fills. A tint is a wash over a surface, so it takes the surface picker: the
53
57
  // full palette with an alpha, not just the tint stops it defaults to.
54
58
  {
@@ -63,10 +67,7 @@ var KIND_RULES = [
63
67
  "-accent",
64
68
  "-color",
65
69
  "-tint",
66
- "-opacity",
67
- "-width",
68
- "-height",
69
- "-size"
70
+ "-opacity"
70
71
  ],
71
72
  prefix: ["--surface-", "--tint", "--color-"]
72
73
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CURRENT_COMPONENT_SCHEMA_VERSION,
3
3
  runMigrations
4
- } from "./chunk-GPIBU44G.js";
4
+ } from "./chunk-YLCOIGQC.js";
5
5
 
6
6
  // vite-plugin/files/versionedFileResourceServer.ts
7
7
  import fs from "fs";
@@ -8,7 +8,9 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
8
8
  "componentConfigsDir",
9
9
  "themesDir",
10
10
  "manifestsDir",
11
- "tokensCssPath"
11
+ "tokensCssPath",
12
+ "componentDirs",
13
+ "checks"
12
14
  ]);
13
15
  var cached = null;
14
16
  function readLiveTokensConfig() {
@@ -803,6 +803,29 @@ var componentMigration_2026_09_01_gateSuffixEnabled = {
803
803
  }
804
804
  };
805
805
 
806
+ // src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts
807
+ var DROPPED2 = new Set(
808
+ ["lg", "md", "sm"].flatMap((v) => [
809
+ `--sectiondivider-${v}-title-outline-width`,
810
+ `--sectiondivider-${v}-title-outline-color`
811
+ ])
812
+ );
813
+ var componentMigration_2026_09_02_sectiondividerDropTitleOutline = {
814
+ id: "2026-09-02-sectiondivider-drop-title-outline",
815
+ fromVersion: 25,
816
+ toVersion: 26,
817
+ appliesTo: "component-config",
818
+ apply(rawVars, meta) {
819
+ if (meta.component !== "sectiondivider") return { ...rawVars };
820
+ const out = {};
821
+ for (const [key, value] of Object.entries(rawVars)) {
822
+ if (DROPPED2.has(key)) continue;
823
+ out[key] = value;
824
+ }
825
+ return out;
826
+ }
827
+ };
828
+
806
829
  // src/editor/core/themes/migrations/index.ts
807
830
  var MIGRATIONS = [
808
831
  colorsAndTypeMigration_2026_04_24_legacyKeysAndBgToCanvas,
@@ -836,7 +859,8 @@ var MIGRATIONS = [
836
859
  colorsAndTypeMigration_2026_09_01_tintRename,
837
860
  componentMigration_2026_09_01_tintRename,
838
861
  componentMigration_2026_09_01_tabbarActiveTint,
839
- componentMigration_2026_09_01_gateSuffixEnabled
862
+ componentMigration_2026_09_01_gateSuffixEnabled,
863
+ componentMigration_2026_09_02_sectiondividerDropTitleOutline
840
864
  ];
841
865
  function countFor(kind) {
842
866
  return MIGRATIONS.filter((m) => m.appliesTo === kind).length;
@@ -16,6 +16,10 @@ interface LiveTokensFileConfig {
16
16
  * migrate` CLI has no plugin options, so it reads this key to locate the file.
17
17
  */
18
18
  tokensCssPath?: string;
19
+ /** Extra directories the component checkers and the registry query discover. */
20
+ componentDirs?: string[];
21
+ /** Checker settings: `rules` per-rule severities, `exclude` paths to skip. */
22
+ checks?: Record<string, unknown>;
19
23
  }
20
24
  interface ResolveDataDirsInput {
21
25
  dataDir?: string;
@@ -16,6 +16,10 @@ interface LiveTokensFileConfig {
16
16
  * migrate` CLI has no plugin options, so it reads this key to locate the file.
17
17
  */
18
18
  tokensCssPath?: string;
19
+ /** Extra directories the component checkers and the registry query discover. */
20
+ componentDirs?: string[];
21
+ /** Checker settings: `rules` per-rule severities, `exclude` paths to skip. */
22
+ checks?: Record<string, unknown>;
19
23
  }
20
24
  interface ResolveDataDirsInput {
21
25
  dataDir?: string;