@motion-proto/live-tokens 0.69.0 → 0.71.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.
@@ -7,7 +7,7 @@ description: Apply the @motion-proto/live-tokens project conventions when buildi
7
7
 
8
8
  Two rules above all else:
9
9
 
10
- 1. **Use a shipped component if one fits.** Import from `@motion-proto/live-tokens/components/<Name>.svelte`. See **live-tokens-pick-component** for the catalogue and the confusing-pair decisions. Pass only the props its `interface Props` declares, with variant and size values from its union: a prop it does not declare is dropped silently at runtime, and the checker reports it. Author custom markup only when nothing fits, and then consider **live-tokens-create-component** so the new piece is editable too.
10
+ 1. **Use a shipped component if one fits.** Import from `@motion-proto/live-tokens/components/<Name>.svelte`. See **live-tokens-pick-component** for the catalogue and the confusing-pair decisions. Pass only the props it declares, with variant and size values from its union: `npx live-tokens components <id>` prints them (`--json` for data), and the list includes the project's own components beside the shipped ones. A prop a component does not declare is dropped silently at runtime, and the checker reports it. Author custom markup only when nothing fits, and then consider **live-tokens-create-component** so the new piece is editable too.
11
11
  2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`, whether it sits in the `<style>` block, an inline `style=` attribute, or a `style:` directive. No colour literals in any notation, `white` and `rgb()` included. No px or rem in spacing, stroke, radius, or shadow: that is the geometry the theme owns and `adjust` moves. Sizing is layout, not theme: a hero's height, a max content width, or a column's minimum width stays a literal. A change in `/live-tokens/editor` should repaint your page.
12
12
 
13
13
  For text, reach for a whole text style rather than assembling one: `--heading-xl` through `--heading-sm`, `--body-md`, `--body-sm`, `--editorial-xl` through `--editorial-sm`, `--eyebrow`, and `--code` each carry a `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. A heading set from `--heading-lg-*` retypes when the theme's fonts change; one set from a raw `font-size` does not.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: live-tokens-check-compliance
3
+ description: Check an existing @motion-proto/live-tokens project against its design system and report, without changing a file: which tokens each component reads, which page renders which component, what the two checkers find, and a list of recommended fixes handed to live-tokens-fix-findings. Use when the user asks to check, audit, validate, or review the project, a page, or a component against the design system; asks how compliant it is, what is off, or what it would take to make the build pass; or wants a look before an upgrade. Not for making the changes (live-tokens-fix-findings), and not for a single token (use the editor).
4
+ ---
5
+
6
+ # Checking a project against its design system
7
+
8
+ The answer to "check this project" is a report, and every fact in it comes
9
+ from one command. This skill runs it, reads it, and says what the facts mean
10
+ and what fixing them would involve. It edits nothing. When the user wants the
11
+ changes made, that is **live-tokens-fix-findings**, and the report is what it
12
+ starts from.
13
+
14
+ ## Run the report
15
+
16
+ ```sh
17
+ npx live-tokens report --json
18
+ ```
19
+
20
+ It always exits 0: it is a reading, not a gate. Unknown command means the
21
+ installed package predates it; upgrade `@motion-proto/live-tokens` first. The
22
+ sections, in the order the report gives them:
23
+
24
+ | Section | Fact | What it means when it is not clean |
25
+ | --- | --- | --- |
26
+ | `migrations` | Whether `tokens.css` is behind the installed package | A stale file shows up downstream as unknown tokens. This is the first fix, and it is one command: `npx live-tokens migrate --check`, then `--write` (`--tokens <path>` for a tokens.css in an unusual place). |
27
+ | `components[].unread` | Tokens a component declares that nothing in its file reads | An editor row that edits nothing. Each is a token to wire into the CSS or to remove. |
28
+ | `components[].registered` | A component file with no `bootLiveTokens` or `registerComponent` entry | It renders on the page but has no editor. |
29
+ | `components[].described` | Whether the runtime file has the header comment the picker reads | Without one, `live-tokens components` cannot say what it is for. |
30
+ | `usage.byPage` | Which catalogue component each page renders, and how many times | A page rendering none is either chrome or hand-rolled markup that a shipped component covers. |
31
+ | `usage.unusedShipped` | Shipped components no page renders | Information, not a finding. |
32
+ | `usage.customUnregistered`, `usage.customUnused` | The project's own components that are unregistered or unused | Dead or half-wired work. |
33
+ | `findings.pages`, `findings.components` | Both checkers' findings by rule, under the project's severities and again under `--strict` | The errors are what fails the build today; the strict count is what a fully tokenized project would fail. |
34
+
35
+ `npx live-tokens components <id>` and `npx live-tokens tokens --family <name>`
36
+ (both take `--json`) answer any question the report raises about one
37
+ component or one scale.
38
+
39
+ ## Read it
40
+
41
+ Facts are the report's; the reading is yours. For each rule with findings, say
42
+ in a line what the rule holds and which of two kinds the fix is:
43
+
44
+ - **Mechanical**: a spacing literal to its nearest `--space-*` step, a stroke
45
+ to `--border-width-*`, a hardcoded column count to `var(--columns-count)`,
46
+ `site.css` moved out of `main.ts`, a route given its `source`. Name any
47
+ visible shift, such as a `14px` margin becoming `16px`.
48
+ - **Judgement**: a colour literal mapped by the role it plays rather than its
49
+ hue, a raw type axis set from a text style, a prop the component does not
50
+ declare mapped or dropped. Say what the choice is, not what you would pick.
51
+
52
+ Where a finding looks deliberate, a translucent overlay on an app shell or a
53
+ layout size the project owns, say so and name the config entry that would
54
+ record the decision: `"checks": { "rules": { "<rule>": "warn" } }` in
55
+ `live-tokens.config.json`. Recording it is the user's call, not yours.
56
+
57
+ ## Report
58
+
59
+ In this order, each line carrying its count:
60
+
61
+ 1. Migrations pending, and the one command that clears them.
62
+ 2. What fails the build now: errors by rule, with the files.
63
+ 3. What `--strict` would add: warnings by rule.
64
+ 4. Components: unread tokens, unregistered, undescribed.
65
+ 5. Usage: what each page renders, and what is used nowhere.
66
+ 6. Recommended fixes, in the order **live-tokens-fix-findings** would take
67
+ them: migrations, then the largest group of errors, then the rest, then
68
+ warnings. Mark each as mechanical or judgement.
69
+
70
+ End with the hand-off: "Run live-tokens-fix-findings to apply these", or the
71
+ subset the user chooses. Do not start applying them here, even when the fix is
72
+ one line, because the user asked how things stand.
@@ -15,11 +15,10 @@ For pattern reference, read any shipped component's source directly from the con
15
15
  - Simplest reads (no state, no linked-block): `Card` (single variant with parts), `Badge` and `Callout` (multi-variant).
16
16
  - Multi-state (hover, disabled, focus): `Button`, `Input`.
17
17
  - Multi-part (overlay / header / body / footer): `Dialog`.
18
- - Multi-variant with linked siblings (`canBeLinked` + `groupKey`): `SegmentedControl`, `TabBar`.
19
- - Composes another shipped component: `CodeSnippet` (renders a `Tooltip` for the copy-confirmation popover).
18
+ - Multi-variant with linked siblings (`canBeLinked` + `groupKey`): `SegmentedControl`, `TabBar`. Composes another shipped component: `CodeSnippet`.
20
19
  - Editor files: `node_modules/@motion-proto/live-tokens/src/editor/component-editor/<Name>Editor.svelte`.
21
20
 
22
- **File-location note.** Shipped editors live in `src/editor/component-editor/` because they're library-internal. For *your* component, **co-locate** both files in `src/system/components/` per the recipe below. Read the shipped files for pattern, ignore their location.
21
+ **File-location note.** Shipped editors live in `src/editor/component-editor/` because they're library-internal. For *your* component, **co-locate** both files in `src/system/components/`. Read the shipped files for pattern, ignore their location.
23
22
 
24
23
  ## The recipe
25
24
 
@@ -43,7 +42,15 @@ For pattern reference, read any shipped component's source directly from the con
43
42
  });
44
43
  ```
45
44
  The schema side-effect happens inside `registerComponent` (which `bootLiveTokens` calls for you), so you don't call `registerComponentSchema` separately. **Do not place a standalone `registerComponent(...)` *before* `bootLiveTokens`** — that registers before the editor's init hooks run, which is the wrong window and can leave editor changes disconnected from the live page. Only call `registerComponent` directly if your app mounts manually (no `bootLiveTokens`), in which case call it before `mount(App, ...)`.
46
- 4. **Tell the picker** — open `.claude/skills/live-tokens-pick-component/SKILL.md` and add your new component to the **Catalogue** line under the family it belongs to (Action / Input / Selection / Containers / Messaging / Display). If it's confusable with an existing component (a second selection control, a competing container), add a row to that family's decision table explaining the use-case it owns. Without this step, the component exists but **live-tokens-pick-component** can't recommend it when a user asks "which component should I use?" — the same rule applies whether the component is first-party (update the picker shipped in this package) or consumer-authored (update the local copy at `.claude/skills/live-tokens-pick-component/SKILL.md` that `setup-claude` placed in your project).
45
+ 4. **Say what it is for.** The runtime file's leading HTML comment is the
46
+ component's description. `npx live-tokens components` prints it beside the
47
+ id with the variants and props read from `interface Props` (`--json` for
48
+ data); that is how **live-tokens-pick-component** weighs a project's own
49
+ component against the shipped set, so no skill file is edited and nothing
50
+ is lost when `setup-claude` refreshes the skills. Name the job it does and
51
+ what it is not for. A directory other than `src/system/components` goes in
52
+ `"componentDirs"` in `live-tokens.config.json`; a first-party component is
53
+ also added to the picker's **Catalogue** line, which `check:skills` holds.
47
54
  5. **Join the sketch layer** — the effect draws a fixed set of parts, so a new
48
55
  component stays crisp while the page around it goes hand-drawn until it opts
49
56
  in. A consumer component carries one of four reserved classes on its root and
@@ -223,15 +230,9 @@ It *warns* (non-fatal) when a token-backed default still carries a px or rem ter
223
230
 
224
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.
225
232
 
226
- **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:
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.
227
234
 
228
- 1. Registration resolves to a real `sourceFile` and a non-empty schema.
229
- 2. Schema variables are unique within the component.
230
- 3. Every editable token (excluding `hidden: true`, `kind: 'gradient'`, and padding-side suffixes) is declared in the runtime `<style>` block.
231
- 4. Every editable token is seeded in `src/live-tokens/data/component-configs/<id>/default.json`.
232
- 5. `setComponentAlias` round-trips the alias through the slice.
233
-
234
- 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 (the same test logic works against any `registerComponent` registration; iterate `getComponentRegistryEntries()` after your `main.ts` has run).
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
236
 
236
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.
237
238
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: live-tokens-fix-findings
3
- description: Bring an existing @motion-proto/live-tokens project into line with its design system by running check-page and check-component, reading the findings, and fixing each by rule until both exit 0. Use when the user asks to make the build pass, fix the design-system errors or warnings, clean up the literals, replace hex or pixel values with tokens, make a page or component themeable, audit the site or a file against the design system, or asks why check:design fails. Not for building a new page (live-tokens-build-page) or a new component (live-tokens-create-component), which run the same gate as their last step, and not for a single token edit (use the editor).
3
+ description: Bring an existing @motion-proto/live-tokens project into line with its design system by running check-page and check-component, reading the findings, and fixing each by rule until both exit 0. Use when the user asks to make the build pass, fix the design-system errors or warnings, clean up the literals, replace hex or pixel values with tokens, make a page or component themeable, or apply what a check reported. Not for the check itself (live-tokens-check-compliance reports and edits nothing), not for building a new page (live-tokens-build-page) or a new component (live-tokens-create-component), which run the same gate as their last step, and not for a single token edit (use the editor).
4
4
  ---
5
5
 
6
6
  # Fixing what the checkers report
@@ -14,7 +14,10 @@ passes repaints when the theme changes. One that does not has opted out of the
14
14
  system silently, and these findings are where.
15
15
 
16
16
  This skill is the loop for code that already exists. Run the checker, fix one
17
- rule at a time, run it again, and stop only when both exit 0.
17
+ rule at a time, run it again, and stop only when both exit 0. When the user has
18
+ not seen the state of the project yet, `npx live-tokens report --json` is the
19
+ reading to start from, and **live-tokens-check-compliance** is the skill that
20
+ presents it without editing; this one edits.
18
21
 
19
22
  ## Reach the checkers
20
23
 
@@ -64,7 +67,9 @@ Three things the loop never does:
64
67
  `color-literal` is the finding that takes judgement. The replacement is the
65
68
  token for what the colour *does*, not the token that happens to be closest in
66
69
  hue, because the theme will move every role together and the page must move
67
- with it. Read `tokens.css` for the names; the families are fixed.
70
+ with it. `npx live-tokens tokens --family surface` prints a family's names and
71
+ values (`text`, `border`, `scrim`, `tint` likewise; `--json` for data); the
72
+ families are fixed.
68
73
 
69
74
  | The literal is | Token family | Notes |
70
75
  | --- | --- | --- |
@@ -89,7 +94,7 @@ and is never reported, so leave it.
89
94
 
90
95
  | The literal is | Token | Notes |
91
96
  | --- | --- | --- |
92
- | Padding, margin, gap, an offset | `--space-<px>` | Steps: 0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64, 96, 128. Round to the nearest step and name the shift. |
97
+ | Padding, margin, gap, an offset | `--space-<px>` | `npx live-tokens tokens --family space` prints the steps. Round to the nearest one and name the shift. |
93
98
  | A stroke width | `--border-width-1`, `-2`, `-4` | Also for `outline`. |
94
99
  | A corner | `--radius-sm` through `-4xl`, `--radius-full` | |
95
100
  | A shadow | `--shadow-sm` through `-xl` | Replace the whole value, never one offset. |
@@ -105,7 +110,7 @@ though no rule reports them, and a `blur()` takes `--blur-*`.
105
110
  | `unknown-token` | A typo or a rename. Search `tokens.css` for the stem. A contract-family name (`--surface-…`, `--text-…`) that is gone was renamed: `npx live-tokens migrate --check` names the migration. |
106
111
  | `raw-text-axis` | Set the whole axis set from one text style: `--heading-xl` through `-sm`, `--body-md`, `--body-sm`, `--editorial-*`, `--eyebrow`, `--code`, each carrying `-font-family`, `-font-size`, `-font-weight`, `-line-height`, `-letter-spacing`. A `font:` shorthand is rewritten the same way. `em`, `%`, and a unitless line-height are relative and fine. |
107
112
  | `unknown-component` | Not in the catalogue. Read **live-tokens-pick-component** for the shipped one that fits, or author it with **live-tokens-create-component**. |
108
- | `unknown-prop` | The component drops it at runtime. Read its `interface Props` in `node_modules/@motion-proto/live-tokens/src/system/components/<Name>.svelte` and either map it to a declared prop or delete it. A `class` on a component that declares none does nothing. |
113
+ | `unknown-prop` | The component drops it at runtime. `npx live-tokens components <id>` prints the props it declares and the values each union accepts; map the prop to one of them or delete it. A `class` on a component that declares none does nothing. |
109
114
  | `unknown-prop-value` | Pick a value from the union the message lists. |
110
115
  | `hardcoded-columns` | `repeat(var(--columns-count), 1fr)` for the page grid; `calc(var(--columns-count) - 2)` for a sub-grid spanning fewer page columns. A two-up or three-up is a layout and is not reported. |
111
116
  | `site-css-in-main` | Delete the import from `main.ts` and add it to each page's `<script>`, so page CSS never reaches the editor routes. |
@@ -13,6 +13,14 @@ For composing a page once you've picked components, see **live-tokens-build-page
13
13
 
14
14
  Action: `Button`, `IconButton`, `InlineEditActions`. Input: `Input`, `Slider`. Selection: `SegmentedControl`, `TabBar`, `RadioButton`, `MenuSelect`, `Toggle`. Containers: `Card`, `CollapsibleSection`, `Dialog`, `Panel`. Messaging: `Callout`, `Notification`, `Tooltip`, `Badge`, `CornerBadge`. Display: `Table`, `Image`, `ImageLightbox`, `ProgressBar`, `SectionDivider`, `SideNavigation`, `CodeSnippet`.
15
15
 
16
+ That line is the shipped set. A project can register components of its own,
17
+ and those never appear in this file: run `npx live-tokens components` before
18
+ choosing. It lists every component the project has, shipped and custom, with
19
+ the variants each takes and the purpose its header comment states, so a custom
20
+ component is weighed against the shipped set on the same footing.
21
+ `npx live-tokens components <id>` prints one component's props, the values each
22
+ union accepts, and its tokens with defaults; `--json` returns the same as data.
23
+
16
24
  ## Action family: Button vs IconButton
17
25
 
18
26
  Both trigger an action and share the same six variants (primary, secondary, outline, success, danger, warning), three states (default, hover, disabled) and two sizes (default, small). They differ only in content.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.71.0 — Check this project
4
+
5
+ ### Added
6
+
7
+ - **`npx live-tokens report` is the project as facts.** Pending `tokens.css`
8
+ migrations, the tokens each component declares and how many its own CSS
9
+ reads (a read counts a `var()`, a `style:` directive, a padding mixin's
10
+ string, or an SCSS-interpolated pattern), whether a component is registered
11
+ and carries the description comment the picker reads, which page renders
12
+ which component and how many times, the shipped and custom components used
13
+ nowhere, and both checkers' findings by rule under the project's severities
14
+ and again under `--strict`. It always exits 0: a reading, not a gate. `--json`
15
+ for data.
16
+
17
+ - **`live-tokens-check-compliance`, the eighth skill.** "Check this project"
18
+ runs the report and presents it without editing a file: what fails the
19
+ build now, what `--strict` would add, the components and usage facts, and a
20
+ list of recommended fixes marked mechanical or judgement with any visible
21
+ shift named, handed to `live-tokens-fix-findings`. A finding that looks
22
+ deliberate is flagged with the config entry that would record the decision,
23
+ which stays the user's call. `live-tokens-fix-findings` no longer claims the
24
+ audit wording, and starts from the report when the user has not seen it.
25
+
26
+ ## 0.70.0 — The registry is a query
27
+
28
+ ### Added
29
+
30
+ - **The registry is a query.** `npx live-tokens components` lists every
31
+ component a project has, shipped and its own, with the variants and props
32
+ read from each `interface Props` and the purpose its header comment states;
33
+ `components <id>` prints one component's props, unions, tokens, and
34
+ defaults. `npx live-tokens tokens` lists every theme token the project's
35
+ `tokens.css` declares by family with its value, `--family <name>` for one
36
+ scale. Both take `--json`. A project's components in a directory other than
37
+ `src/system/components` are found through `"componentDirs"` in
38
+ `live-tokens.config.json`. The same vocabulary the checkers read answers the
39
+ query, so a skill sees exactly what the checkers will hold it to.
40
+
41
+ - **`getComponentRegistryEntries` is exported from the package**, so a
42
+ project's own test suite can run the registry contract over every
43
+ registration, shipped and custom. The create-component skill pointed at it
44
+ before it was public.
45
+
46
+ ### Changed
47
+
48
+ - **The skills read the registry instead of carrying it.** The picker's
49
+ catalogue line is the shipped set only; a project's own component is found
50
+ by `live-tokens components`, weighed by the description its header comment
51
+ states, and never written into a skill file, so `setup-claude --force` no
52
+ longer loses anything. build-page and fix-findings read a component's props
53
+ from the same query, and fix-findings reads a token scale from `tokens`.
54
+
3
55
  ## 0.69.0 — Every value reads a token, and the build says so
4
56
 
5
57
  ### Changed
package/README.md CHANGED
@@ -323,6 +323,9 @@ npx @motion-proto/live-tokens <command>
323
323
  |---|---|
324
324
  | `create <dir> [--force]` | Scaffold a new Svelte + Vite app wired up with live-tokens. |
325
325
  | `setup-claude [--force]` | Install the bundled Claude Code skills into `./.claude/skills/`. |
326
+ | `components [id] [--json]` | List every component the project has, shipped and its own, with the props each takes; with an id, its props, variants, tokens, and defaults. |
327
+ | `tokens [--family <name>] [--json]` | List every theme token the project's `tokens.css` declares, by family, with its value. |
328
+ | `report [--json]` | The project as facts: pending migrations, tokens each component reads, which page renders which component, and both checkers' findings by rule. Always exits 0. |
326
329
  | `check-component [id]` | Validate a component's runtime, editor, and registration against the authoring contract; with no id, every component authored under `src/system/components`. |
327
330
  | `check-page [paths...]` | Validate pages against the build-page contract: catalogue components and their props, theme tokens over literals, route wiring. |
328
331
  | `generate-theme <brief.json> [--no-activate] [--dry-run] [--carry-from <name>]` | Build a full theme from a 10-seed OKLCH brief, enforce AA contrast, write `themes/<slug>.json`, and open it. |
@@ -334,7 +337,7 @@ Once installed in a project, the same commands are available as `npx live-tokens
334
337
 
335
338
  ## Claude Code skills
336
339
 
337
- The package bundles seven Claude Code skills. They encode the conventions this README cannot carry in full: which component fits a need, how a page is wired, what a valid theme looks like in OKLCH, how two typefaces sit together, how geometry moves along the token scales, and how an existing page or component is brought back into line with all of that. Each triggers from an ordinary request, so there are no slash commands to learn.
340
+ The package bundles eight Claude Code skills. They encode the conventions this README cannot carry in full: which component fits a need, how a page is wired, what a valid theme looks like in OKLCH, how two typefaces sit together, how geometry moves along the token scales, how a project is checked against all of that, and how an existing page or component is brought back into line. Each triggers from an ordinary request, so there are no slash commands to learn.
338
341
 
339
342
  ### Install
340
343
 
@@ -398,6 +401,12 @@ Ask for something the catalogue lacks: "author a Rating component", "make my Chi
398
401
 
399
402
  The skill covers the recipe: the runtime `.svelte` file with its `:global(:root)` token block, the editor `.svelte` file exporting `allTokens` and its variant groups, the `registerComponent()` call, and the catalogue entry that keeps `live-tokens-pick-component` current. It carries the naming scheme, the token suffix vocabulary, the state model (component states such as selected and disabled are separate from interaction states such as hover), and the public-imports rule, and points at the shipped `Toggle` in `node_modules` as the worked example. Linked siblings, intrinsics, and the fixed-overlay portal rule sit in reference files the skill reads only when a component needs them.
400
403
 
404
+ ### `live-tokens-check-compliance`
405
+
406
+ Ask how things stand: "check this project against the design system", "audit the pricing page", "what would it take to make the build pass?", "review this before I upgrade".
407
+
408
+ The skill runs `npx live-tokens report --json`, which is the project as facts: pending `tokens.css` migrations, the tokens each component declares and reads, which page renders which component, and both checkers' findings by rule under the project's severities and under `--strict`. It presents the report, says what each rule holds, marks each recommended fix as mechanical or a judgement call, names any visible shift, and flags a finding that looks deliberate together with the config entry that would record the decision. It edits nothing and ends by handing the list to `live-tokens-fix-findings`.
409
+
401
410
  ### `live-tokens-fix-findings`
402
411
 
403
412
  Ask for the existing code to catch up: "make check:design pass", "fix the design-system warnings", "replace the hex and pixel values with tokens", "why is check-page failing on the pricing page?".
@@ -35,7 +35,7 @@ export const PAGE_RULES = {
35
35
  // Directories that hold the system, not pages built on it.
36
36
  const NOT_PAGES = ['src/system', 'src/editor', 'src/lib', 'src/live-tokens'];
37
37
 
38
- const COMPONENT_IMPORT =
38
+ export const COMPONENT_IMPORT =
39
39
  /(?:@motion-proto\/live-tokens\/components|[./][^'"]*\/system\/components)\/([A-Za-z0-9]+)\.svelte$/;
40
40
 
41
41
  const DEEP_IMPORT_PATTERNS = [
package/bin/cli.mjs CHANGED
@@ -3,6 +3,9 @@
3
3
  // Subcommands:
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
+ // 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.
8
+ // report The project as facts: tokens read, components used, findings by rule. Always exits 0.
6
9
  // check-component [id] Validate a component (or every authored one) against the create-component skill contract.
7
10
  // check-page [paths...] Validate pages against the build-page skill contract.
8
11
  // generate-theme <brief> Build a theme from a 10-seed OKLCH brief and open it.
@@ -10,12 +13,15 @@
10
13
  // set-fonts <brief.json> Bind Google Fonts families to the theme's font stacks.
11
14
  // migrate [...] Reconcile tokens.css, the data tree, and route references.
12
15
 
13
- import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
16
+ import { cpSync, existsSync, mkdirSync, readdirSync, statSync, writeSync } from 'node:fs';
14
17
  import { dirname, join, resolve } from 'node:path';
15
18
  import { fileURLToPath } from 'node:url';
16
19
  import process from 'node:process';
17
20
  import { COMPONENT_RULES, checkComponent, discoverComponents, formatReport } from './check-component.mjs';
18
21
  import { PAGE_RULES, checkPages, discoverPages } from './check-page.mjs';
22
+ import { describeComponents, describeTokens, formatComponents, formatTokens } from './lib/catalogue.mjs';
23
+ import { buildReport, formatReport as formatProjectReport } from './lib/report.mjs';
24
+ import { loadVocabulary } from './lib/tokenVocabulary.mjs';
19
25
  import {
20
26
  applySeverity,
21
27
  countBySeverity,
@@ -42,6 +48,20 @@ Commands:
42
48
  create <dir> [--force] Scaffold a new Svelte + Vite app wired up with
43
49
  live-tokens (editor, components, theme tokens)
44
50
  setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
51
+ components [id] [--json] List every component the project has, shipped and
52
+ 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
59
+ report [--json] The project as facts: pending migrations, tokens
60
+ each component declares and reads, which page
61
+ renders which component, and both checkers'
62
+ findings by rule under the project's severities
63
+ and under --strict. A reading, not a gate: always
64
+ exits 0
45
65
  check-component [id] Validate <id>'s runtime, editor, and registration
46
66
  against the live-tokens-create-component contract
47
67
  check-page [paths...] Validate pages against the live-tokens-build-page
@@ -101,6 +121,20 @@ Both check commands accept:
101
121
  either is pending; route findings are advisory).
102
122
  `;
103
123
 
124
+ // A large body written through console.log is cut at the pipe buffer when the
125
+ // process exits before stdout drains, so a query writes synchronously.
126
+ function writeOut(text) {
127
+ const buf = Buffer.from(`${text}\n`);
128
+ let offset = 0;
129
+ while (offset < buf.length) {
130
+ try {
131
+ offset += writeSync(1, buf, offset, buf.length - offset);
132
+ } catch (error) {
133
+ if (error.code !== 'EAGAIN') throw error;
134
+ }
135
+ }
136
+ }
137
+
104
138
  function fail(message, code = 1) {
105
139
  console.error(message);
106
140
  process.exit(code);
@@ -141,6 +175,47 @@ function reportChecks(label, findings, checked, rules, opts) {
141
175
  process.exit(countBySeverity(resolved).errors === 0 ? 0 : 1);
142
176
  }
143
177
 
178
+ if (command === 'components') {
179
+ const opts = parseCheckFlags(rest);
180
+ const list = describeComponents(loadVocabulary());
181
+ const id = opts.rest[0];
182
+ if (id && !list.some((c) => c.id === id)) fail(formatComponents(list, { id }));
183
+ writeOut(opts.json ? JSON.stringify(id ? list.find((c) => c.id === id) : list, null, 2) : formatComponents(list, { id }));
184
+ process.exit(0);
185
+ }
186
+
187
+ if (command === 'tokens') {
188
+ const opts = parseCheckFlags(rest);
189
+ const at = opts.rest.indexOf('--family');
190
+ const family = at >= 0 ? opts.rest[at + 1] : undefined;
191
+ const desc = describeTokens(loadVocabulary());
192
+ if (family && !desc.families.some((f) => f.family === family)) fail(formatTokens(desc, { family }));
193
+ writeOut(
194
+ opts.json
195
+ ? JSON.stringify(family ? desc.families.find((f) => f.family === family) : desc, null, 2)
196
+ : formatTokens(desc, { family }),
197
+ );
198
+ process.exit(0);
199
+ }
200
+
201
+ if (command === 'report') {
202
+ const opts = parseCheckFlags(rest);
203
+ const report = buildReport(loadVocabulary());
204
+ try {
205
+ const plan = await runMigrate({ check: true });
206
+ report.migrations =
207
+ plan.status === 'no-path'
208
+ ? { status: 'no tokens.css' }
209
+ : plan.status === 'would-change'
210
+ ? { status: 'pending', pending: plan.applied ?? plan.migrations ?? [] }
211
+ : { status: 'none pending' };
212
+ } catch {
213
+ report.migrations = { status: 'unavailable (compiled engine not built)' };
214
+ }
215
+ writeOut(opts.json ? JSON.stringify(report, null, 2) : formatProjectReport(report));
216
+ process.exit(0);
217
+ }
218
+
144
219
  if (command === 'check-component') {
145
220
  const opts = parseCheckFlags(rest);
146
221
  const ids = opts.rest.length > 0 ? [opts.rest[0]] : discoverComponents();
@@ -319,6 +394,7 @@ const SAMPLE_PROMPTS = {
319
394
  'live-tokens-adjust-geometry': 'make the buttons pill shaped',
320
395
  'live-tokens-pair-fonts': 'pair some fonts for this theme',
321
396
  'live-tokens-fix-findings': 'make check:design pass',
397
+ 'live-tokens-check-compliance': 'check this project against the design system',
322
398
  };
323
399
 
324
400
  const installedSamples = skills
@@ -0,0 +1,123 @@
1
+ // The registry as a query. Every component a project has, shipped or its own,
2
+ // with the props each takes and the tokens each declares, and every theme token
3
+ // grouped by family. Read from files through the same vocabulary the checkers
4
+ // use, so a skill or a script sees exactly what the checkers will hold it to.
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { relative } from 'node:path';
8
+ import { CONTRACT_FAMILIES } from './tokenVocabulary.mjs';
9
+
10
+ /** The runtime file's leading HTML comment, which is where a component says what it is for. */
11
+ function descriptionOf(source) {
12
+ const m = source.match(/^\s*<!--([\s\S]*?)-->/);
13
+ if (!m) return '';
14
+ return m[1]
15
+ .split('\n')
16
+ .map((line) => line.trim())
17
+ .join(' ')
18
+ .replace(/\s+/g, ' ')
19
+ .trim()
20
+ .replace(/^\S+\.svelte\s*[—–-]+\s*/, '');
21
+ }
22
+
23
+ function familyOf(name) {
24
+ const stem = name.replace(/^--/, '');
25
+ const hit = CONTRACT_FAMILIES
26
+ .filter((f) => stem === f || stem.startsWith(`${f}-`))
27
+ .sort((a, b) => b.length - a.length)[0];
28
+ return hit ?? stem.split('-')[0];
29
+ }
30
+
31
+ export function describeComponents(vocab, { root = process.cwd() } = {}) {
32
+ const out = [];
33
+ for (const entry of vocab.components.values()) {
34
+ const source = readFileSync(entry.file, 'utf8');
35
+ const props = entry.props
36
+ ? [...entry.props.props].map((name) => ({
37
+ name,
38
+ type: entry.props.types.get(name) ?? '',
39
+ values: entry.props.enums.has(name) ? [...entry.props.enums.get(name)] : undefined,
40
+ }))
41
+ : [];
42
+ out.push({
43
+ id: entry.id,
44
+ name: entry.name,
45
+ origin: entry.origin,
46
+ file: relative(root, entry.file),
47
+ registered: entry.origin === 'shipped' || vocab.registered.has(entry.id),
48
+ description: descriptionOf(source),
49
+ variants: entry.props?.enums.get('variant') ? [...entry.props.enums.get('variant')] : [],
50
+ props,
51
+ tokens: [...entry.tokens].map(([name, value]) => ({ name, default: value })),
52
+ });
53
+ }
54
+ return out.sort((a, b) => a.origin.localeCompare(b.origin) || a.id.localeCompare(b.id));
55
+ }
56
+
57
+ export function describeTokens(vocab, { root = process.cwd() } = {}) {
58
+ const values = new Map();
59
+ if (vocab.tokensCssPath) {
60
+ const css = readFileSync(vocab.tokensCssPath, 'utf8').replace(/\/\*[\s\S]*?\*\//g, ' ');
61
+ for (const m of css.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)) {
62
+ if (!values.has(m[1])) values.set(m[1], m[2].trim());
63
+ }
64
+ }
65
+ const byFamily = new Map();
66
+ for (const name of vocab.themeTokens) {
67
+ const family = familyOf(name);
68
+ if (!byFamily.has(family)) byFamily.set(family, []);
69
+ byFamily.get(family).push({ name, value: values.get(name) ?? '' });
70
+ }
71
+ return {
72
+ tokensCss: vocab.tokensCssPath ? relative(root, vocab.tokensCssPath) : null,
73
+ families: [...byFamily].map(([family, tokens]) => ({ family, tokens })),
74
+ components: [...vocab.components.values()].map((c) => ({
75
+ id: c.id,
76
+ tokens: [...c.tokens].map(([name, value]) => ({ name, default: value })),
77
+ })),
78
+ };
79
+ }
80
+
81
+ export function formatComponents(list, { id } = {}) {
82
+ const lines = [];
83
+ if (id) {
84
+ const c = list.find((x) => x.id === id);
85
+ if (!c) return `No component "${id}". Run \`live-tokens components\` for the list.`;
86
+ lines.push(`${c.name} (${c.id}, ${c.origin}${c.registered ? '' : ', NOT registered'}) ${c.file}`);
87
+ if (c.description) lines.push(` ${c.description}`);
88
+ if (c.props.length) {
89
+ lines.push(' props:');
90
+ for (const p of c.props) lines.push(` ${p.name}${p.values ? `: ${p.values.join(' | ')}` : p.type ? `: ${p.type}` : ''}`);
91
+ }
92
+ lines.push(` tokens (${c.tokens.length}):`);
93
+ for (const t of c.tokens) lines.push(` ${t.name}: ${t.default}`);
94
+ return lines.join('\n');
95
+ }
96
+ for (const c of list) {
97
+ const variants = c.variants.length ? ` variants: ${c.variants.join(', ')}` : '';
98
+ lines.push(`${c.id.padEnd(20)} ${c.origin.padEnd(8)} ${c.name}${c.registered ? '' : ' (NOT registered)'}${variants}`);
99
+ if (c.description) lines.push(`${''.padEnd(29)} ${c.description}`);
100
+ }
101
+ lines.push('');
102
+ lines.push(`${list.length} component(s). \`live-tokens components <id>\` prints one with its props and tokens.`);
103
+ return lines.join('\n');
104
+ }
105
+
106
+ export function formatTokens(desc, { family } = {}) {
107
+ const lines = [];
108
+ const families = family ? desc.families.filter((f) => f.family === family) : desc.families;
109
+ if (family && families.length === 0) {
110
+ return `No family "${family}". Families: ${desc.families.map((f) => f.family).join(', ')}.`;
111
+ }
112
+ lines.push(`Theme tokens from ${desc.tokensCss ?? '(no tokens.css found)'}`);
113
+ for (const f of families) {
114
+ lines.push('');
115
+ lines.push(`${f.family} (${f.tokens.length})`);
116
+ for (const t of f.tokens) lines.push(` ${t.name}: ${t.value}`);
117
+ }
118
+ if (!family) {
119
+ lines.push('');
120
+ lines.push(`Component tokens: ${desc.components.reduce((n, c) => n + c.tokens.length, 0)} across ${desc.components.length} component(s). \`live-tokens components <id>\` lists one component's.`);
121
+ }
122
+ return lines.join('\n');
123
+ }
@@ -0,0 +1,158 @@
1
+ // The project as facts. Every section is deterministic: what the tokens.css
2
+ // declares, what each component reads, which component each page renders, and
3
+ // what the two checkers report under the project's severities and under
4
+ // --strict. Nothing here interprets; the check skill narrates it and the fix
5
+ // skill acts on it. Reads files only, like the vocabulary it is built on.
6
+
7
+ import { readFileSync } from 'node:fs';
8
+ import { relative } from 'node:path';
9
+ import { COMPONENT_RULES, checkComponent, discoverComponents } from '../check-component.mjs';
10
+ import { COMPONENT_IMPORT, PAGE_RULES, checkPages, discoverPages } from '../check-page.mjs';
11
+ import { applySeverity, readChecksConfig } from './findings.mjs';
12
+ import { extractGlobalRootBlocks } from './tokenVocabulary.mjs';
13
+
14
+ const SIDES = ['-top', '-right', '-bottom', '-left'];
15
+
16
+ /**
17
+ * Tokens a component declares that nothing in its file reads. A read is the
18
+ * name appearing outside the `:global(:root)` block: in a `var()`, in a `style:`
19
+ * directive, or as the string a padding mixin takes. SCSS interpolation
20
+ * (`--badge-#{$v}-surface`) reads every token the pattern covers. A per-side
21
+ * padding is read through its parent.
22
+ */
23
+ export function unreadTokens(source, tokens) {
24
+ let body = source.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/<!--[\s\S]*?-->/g, ' ');
25
+ for (const block of extractGlobalRootBlocks(body)) body = body.replace(block, ' ');
26
+ const patterns = [...body.matchAll(/--[a-z0-9-]*(?:#\{[^}]*\}[a-z0-9-]*)+/g)].map(
27
+ (m) => new RegExp(`^${m[0].replace(/[.*+?^()|[\]\\]/g, '\\$&').replace(/#\{[^}]*\}/g, '[a-z0-9-]+')}$`),
28
+ );
29
+ const isRead = (name) => body.includes(name) || patterns.some((re) => re.test(name));
30
+ return [...tokens].filter((name) => {
31
+ const side = SIDES.find((s) => name.endsWith(s));
32
+ return !isRead(name) && !(side && isRead(name.slice(0, -side.length)));
33
+ });
34
+ }
35
+
36
+ function countByRule(findings) {
37
+ const out = {};
38
+ for (const f of findings) out[f.rule] = (out[f.rule] ?? 0) + 1;
39
+ return out;
40
+ }
41
+
42
+ function summarise(findings, rules, config) {
43
+ const resolved = applySeverity(findings, rules, {}, config);
44
+ const strict = applySeverity(findings, rules, { strict: true }, config);
45
+ return {
46
+ errors: resolved.filter((f) => f.severity === 'error').length,
47
+ warnings: resolved.filter((f) => f.severity === 'warn').length,
48
+ strictErrors: strict.filter((f) => f.severity === 'error').length,
49
+ byRule: countByRule(resolved),
50
+ items: resolved.map((f) => ({ rule: f.rule, severity: f.severity, file: f.file, line: f.line, message: f.message })),
51
+ };
52
+ }
53
+
54
+ export function buildReport(vocab, { root = process.cwd() } = {}) {
55
+ const config = readChecksConfig(root);
56
+
57
+ const components = [...vocab.components.values()].map((c) => {
58
+ const source = readFileSync(c.file, 'utf8');
59
+ return {
60
+ id: c.id,
61
+ name: c.name,
62
+ origin: c.origin,
63
+ file: relative(root, c.file),
64
+ registered: c.origin === 'shipped' || vocab.registered.has(c.id),
65
+ described: /^\s*<!--[\s\S]*?-->/.test(source),
66
+ tokens: c.tokens.size,
67
+ unread: unreadTokens(source, c.tokens.keys()),
68
+ };
69
+ });
70
+
71
+ const pageFiles = discoverPages(root);
72
+ const byPage = [];
73
+ const pagesOf = new Map();
74
+ for (const file of pageFiles) {
75
+ if (!file.endsWith('.svelte')) continue;
76
+ const text = readFileSync(file, 'utf8').replace(/<style[^>]*>[\s\S]*?<\/style>/g, ' ');
77
+ const used = [];
78
+ for (const m of text.matchAll(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g)) {
79
+ const comp = m[2].match(COMPONENT_IMPORT);
80
+ if (!comp) continue;
81
+ const id = comp[1].toLowerCase();
82
+ const rendered = [...text.matchAll(new RegExp(`<${m[1]}(?=[\\s/>])`, 'g'))].length;
83
+ used.push({ id, rendered });
84
+ if (!pagesOf.has(id)) pagesOf.set(id, []);
85
+ pagesOf.get(id).push(relative(root, file));
86
+ }
87
+ byPage.push({ file: relative(root, file), components: used });
88
+ }
89
+ const byComponent = components.map((c) => ({ id: c.id, origin: c.origin, pages: pagesOf.get(c.id) ?? [] }));
90
+
91
+ const pageFindings = checkPages(pageFiles, { root, vocabulary: vocab }).findings;
92
+ const authored = discoverComponents(root);
93
+ const componentFindings = authored.flatMap((id) => checkComponent(id, root, { vocabulary: vocab }).findings);
94
+
95
+ return {
96
+ project: {
97
+ root,
98
+ tokensCss: vocab.tokensCssPath ? relative(root, vocab.tokensCssPath) : null,
99
+ themeTokens: vocab.themeTokens.size,
100
+ components: components.length,
101
+ pages: pageFiles.length,
102
+ },
103
+ components,
104
+ usage: {
105
+ byPage,
106
+ byComponent,
107
+ unusedShipped: byComponent.filter((c) => c.origin === 'shipped' && c.pages.length === 0).map((c) => c.id),
108
+ customUnregistered: components.filter((c) => c.origin === 'custom' && !c.registered).map((c) => c.id),
109
+ customUnused: byComponent.filter((c) => c.origin === 'custom' && c.pages.length === 0).map((c) => c.id),
110
+ },
111
+ findings: {
112
+ pages: summarise(pageFindings, PAGE_RULES, config),
113
+ components: { checked: authored, ...summarise(componentFindings, COMPONENT_RULES, config) },
114
+ },
115
+ };
116
+ }
117
+
118
+ const list = (items, max = 20) =>
119
+ items.length <= max ? items.join(', ') : `${items.slice(0, max).join(', ')}, +${items.length - max} more`;
120
+
121
+ export function formatReport(r) {
122
+ const out = [];
123
+ out.push(`Project: ${r.project.pages} page file(s), ${r.project.components} component(s), ${r.project.themeTokens} theme tokens from ${r.project.tokensCss ?? '(no tokens.css)'}`);
124
+ if (r.migrations) {
125
+ out.push('');
126
+ out.push(`Migrations: ${r.migrations.status}${r.migrations.pending?.length ? ` (${list(r.migrations.pending)})` : ''}`);
127
+ }
128
+
129
+ out.push('');
130
+ out.push('Components');
131
+ const unread = r.components.filter((c) => c.unread.length);
132
+ out.push(` tokens declared and read by their own CSS: ${r.components.reduce((n, c) => n + c.tokens - c.unread.length, 0)} of ${r.components.reduce((n, c) => n + c.tokens, 0)}`);
133
+ for (const c of unread) out.push(` ${c.id}: ${c.unread.length} unread (${list(c.unread, 6)})`);
134
+ const custom = r.components.filter((c) => c.origin === 'custom');
135
+ out.push(` custom: ${custom.length}${custom.length ? ` (${list(custom.map((c) => c.id))})` : ''}`);
136
+ if (r.usage.customUnregistered.length) out.push(` not registered: ${list(r.usage.customUnregistered)}`);
137
+ const undescribed = custom.filter((c) => !c.described).map((c) => c.id);
138
+ if (undescribed.length) out.push(` no description comment: ${list(undescribed)}`);
139
+
140
+ out.push('');
141
+ out.push('Usage');
142
+ for (const p of r.usage.byPage) {
143
+ if (p.components.length === 0) continue;
144
+ out.push(` ${p.file}: ${p.components.map((c) => `${c.id}×${c.rendered}`).join(', ')}`);
145
+ }
146
+ out.push(` pages rendering no catalogue component: ${r.usage.byPage.filter((p) => p.components.length === 0).length}`);
147
+ out.push(` shipped components used nowhere: ${r.usage.unusedShipped.length}${r.usage.unusedShipped.length ? ` (${list(r.usage.unusedShipped)})` : ''}`);
148
+ if (r.usage.customUnused.length) out.push(` custom components used nowhere: ${list(r.usage.customUnused)}`);
149
+
150
+ const section = (label, s) => {
151
+ out.push('');
152
+ out.push(`${label}: ${s.errors} error(s), ${s.warnings} warning(s); ${s.strictErrors} under --strict`);
153
+ for (const [rule, n] of Object.entries(s.byRule).sort((a, b) => b[1] - a[1])) out.push(` ${rule}: ${n}`);
154
+ };
155
+ section('check-page', r.findings.pages);
156
+ section(`check-component (${r.findings.components.checked.length} authored)`, r.findings.components);
157
+ return out.join('\n');
158
+ }
@@ -26,7 +26,7 @@ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
26
26
  const SHIPPED_COMPONENTS_DIR = 'src/system/components';
27
27
 
28
28
  /** Families whose names are governed by the token contract (see TOKENS.md). */
29
- const CONTRACT_FAMILIES = [
29
+ export const CONTRACT_FAMILIES = [
30
30
  'surface', 'text', 'border', 'color', 'space', 'radius', 'font', 'line-height',
31
31
  'letter-spacing', 'shadow', 'blur', 'icon-size', 'scrim', 'tint', 'columns',
32
32
  'heading', 'body', 'editorial', 'eyebrow', 'code', 'easing', 'duration', 'zoom',
@@ -111,13 +111,26 @@ export function componentProps(source) {
111
111
 
112
112
  const props = new Set();
113
113
  const enums = new Map();
114
+ const types = new Map();
114
115
  const body = iface[1].replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
115
116
  for (const m of body.matchAll(/^\s*(?:readonly\s+)?(\w+)\??\s*:\s*([^;\n]+)/gm)) {
116
117
  props.add(m[1]);
118
+ types.set(m[1], m[2].trim());
117
119
  const values = resolveEnum(m[2]);
118
120
  if (values) enums.set(m[1], new Set(values));
119
121
  }
120
- return { props, enums };
122
+ return { props, enums, types };
123
+ }
124
+
125
+ /** `live-tokens.config.json` at the project root, or nothing. */
126
+ export function readProjectConfig(root) {
127
+ const path = join(root, 'live-tokens.config.json');
128
+ if (!existsSync(path)) return {};
129
+ try {
130
+ return JSON.parse(readFileSync(path, 'utf8'));
131
+ } catch {
132
+ return {};
133
+ }
121
134
  }
122
135
 
123
136
  function walk(dir, exts, out = []) {
@@ -186,14 +199,28 @@ export function loadVocabulary({ root = process.cwd(), pkgRoot = PKG_ROOT } = {}
186
199
 
187
200
  const componentTokens = new Set();
188
201
  const components = new Map();
189
- const dirs = [join(pkgRoot, SHIPPED_COMPONENTS_DIR), join(root, SHIPPED_COMPONENTS_DIR)];
202
+ // A project's own components sit beside the shipped ones, plus any directory
203
+ // `componentDirs` in live-tokens.config.json names.
204
+ const own = [SHIPPED_COMPONENTS_DIR, ...(readProjectConfig(root).componentDirs ?? [])].map((d) => join(root, d));
205
+ const shippedDir = join(pkgRoot, SHIPPED_COMPONENTS_DIR);
206
+ const dirs = [shippedDir, ...own];
190
207
  for (const file of componentFiles(dirs)) {
191
208
  const Id = file.slice(file.lastIndexOf('/') + 1).replace('.svelte', '');
192
209
  const src = readFileSync(file, 'utf8');
193
- components.set(Id.toLowerCase(), { id: Id.toLowerCase(), name: Id, file, props: componentProps(src) });
210
+ const tokens = new Map();
194
211
  for (const block of extractGlobalRootBlocks(src)) {
195
- for (const n of declaredCustomProperties(block)) componentTokens.add(n);
212
+ const clean = block.replace(/\/\*[\s\S]*?\*\//g, ' ');
213
+ for (const n of declaredCustomProperties(clean)) componentTokens.add(n);
214
+ for (const m of clean.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)) if (!tokens.has(m[1])) tokens.set(m[1], m[2].trim());
196
215
  }
216
+ components.set(Id.toLowerCase(), {
217
+ id: Id.toLowerCase(),
218
+ name: Id,
219
+ file,
220
+ origin: file.startsWith(shippedDir) ? 'shipped' : 'custom',
221
+ props: componentProps(src),
222
+ tokens,
223
+ });
197
224
  }
198
225
  const registered = registeredIds(root);
199
226
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -82,5 +82,5 @@ export type { Oklch } from './core/palettes/oklch';
82
82
 
83
83
  export { initializeTheme } from './core/themes/themeInit';
84
84
 
85
- export { registerComponent } from './component-editor/registry';
85
+ export { registerComponent, getComponentRegistryEntries } from './component-editor/registry';
86
86
  export type { RegisterComponentEntry, RegistryEntry, ComponentId } from './component-editor/registry';