@motion-proto/live-tokens 0.69.0 → 0.70.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.
@@ -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
 
@@ -64,7 +64,9 @@ Three things the loop never does:
64
64
  `color-literal` is the finding that takes judgement. The replacement is the
65
65
  token for what the colour *does*, not the token that happens to be closest in
66
66
  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.
67
+ with it. `npx live-tokens tokens --family surface` prints a family's names and
68
+ values (`text`, `border`, `scrim`, `tint` likewise; `--json` for data); the
69
+ families are fixed.
68
70
 
69
71
  | The literal is | Token family | Notes |
70
72
  | --- | --- | --- |
@@ -89,7 +91,7 @@ and is never reported, so leave it.
89
91
 
90
92
  | The literal is | Token | Notes |
91
93
  | --- | --- | --- |
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. |
94
+ | 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
95
  | A stroke width | `--border-width-1`, `-2`, `-4` | Also for `outline`. |
94
96
  | A corner | `--radius-sm` through `-4xl`, `--radius-full` | |
95
97
  | A shadow | `--shadow-sm` through `-xl` | Replace the whole value, never one offset. |
@@ -105,7 +107,7 @@ though no rule reports them, and a `blur()` takes `--blur-*`.
105
107
  | `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
108
  | `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
109
  | `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. |
110
+ | `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
111
  | `unknown-prop-value` | Pick a value from the union the message lists. |
110
112
  | `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
113
  | `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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.70.0 — The registry is a query
4
+
5
+ ### Added
6
+
7
+ - **The registry is a query.** `npx live-tokens components` lists every
8
+ component a project has, shipped and its own, with the variants and props
9
+ read from each `interface Props` and the purpose its header comment states;
10
+ `components <id>` prints one component's props, unions, tokens, and
11
+ defaults. `npx live-tokens tokens` lists every theme token the project's
12
+ `tokens.css` declares by family with its value, `--family <name>` for one
13
+ scale. Both take `--json`. A project's components in a directory other than
14
+ `src/system/components` are found through `"componentDirs"` in
15
+ `live-tokens.config.json`. The same vocabulary the checkers read answers the
16
+ query, so a skill sees exactly what the checkers will hold it to.
17
+
18
+ - **`getComponentRegistryEntries` is exported from the package**, so a
19
+ project's own test suite can run the registry contract over every
20
+ registration, shipped and custom. The create-component skill pointed at it
21
+ before it was public.
22
+
23
+ ### Changed
24
+
25
+ - **The skills read the registry instead of carrying it.** The picker's
26
+ catalogue line is the shipped set only; a project's own component is found
27
+ by `live-tokens components`, weighed by the description its header comment
28
+ states, and never written into a skill file, so `setup-claude --force` no
29
+ longer loses anything. build-page and fix-findings read a component's props
30
+ from the same query, and fix-findings reads a token scale from `tokens`.
31
+
3
32
  ## 0.69.0 — Every value reads a token, and the build says so
4
33
 
5
34
  ### Changed
package/README.md CHANGED
@@ -323,6 +323,8 @@ 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. |
326
328
  | `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
329
  | `check-page [paths...]` | Validate pages against the build-page contract: catalogue components and their props, theme tokens over literals, route wiring. |
328
330
  | `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. |
package/bin/cli.mjs CHANGED
@@ -3,6 +3,8 @@
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.
6
8
  // check-component [id] Validate a component (or every authored one) against the create-component skill contract.
7
9
  // check-page [paths...] Validate pages against the build-page skill contract.
8
10
  // generate-theme <brief> Build a theme from a 10-seed OKLCH brief and open it.
@@ -10,12 +12,14 @@
10
12
  // set-fonts <brief.json> Bind Google Fonts families to the theme's font stacks.
11
13
  // migrate [...] Reconcile tokens.css, the data tree, and route references.
12
14
 
13
- import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
15
+ import { cpSync, existsSync, mkdirSync, readdirSync, statSync, writeSync } from 'node:fs';
14
16
  import { dirname, join, resolve } from 'node:path';
15
17
  import { fileURLToPath } from 'node:url';
16
18
  import process from 'node:process';
17
19
  import { COMPONENT_RULES, checkComponent, discoverComponents, formatReport } from './check-component.mjs';
18
20
  import { PAGE_RULES, checkPages, discoverPages } from './check-page.mjs';
21
+ import { describeComponents, describeTokens, formatComponents, formatTokens } from './lib/catalogue.mjs';
22
+ import { loadVocabulary } from './lib/tokenVocabulary.mjs';
19
23
  import {
20
24
  applySeverity,
21
25
  countBySeverity,
@@ -42,6 +46,14 @@ Commands:
42
46
  create <dir> [--force] Scaffold a new Svelte + Vite app wired up with
43
47
  live-tokens (editor, components, theme tokens)
44
48
  setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
49
+ components [id] [--json] List every component the project has, shipped and
50
+ its own (src/system/components plus any
51
+ "componentDirs" in live-tokens.config.json), with
52
+ the props each takes; with an id, that component's
53
+ props, variants, tokens, and defaults
54
+ tokens [--family <name>] [--json]
55
+ List every theme token the project's tokens.css
56
+ declares, by family, with its value
45
57
  check-component [id] Validate <id>'s runtime, editor, and registration
46
58
  against the live-tokens-create-component contract
47
59
  check-page [paths...] Validate pages against the live-tokens-build-page
@@ -101,6 +113,20 @@ Both check commands accept:
101
113
  either is pending; route findings are advisory).
102
114
  `;
103
115
 
116
+ // A large body written through console.log is cut at the pipe buffer when the
117
+ // process exits before stdout drains, so a query writes synchronously.
118
+ function writeOut(text) {
119
+ const buf = Buffer.from(`${text}\n`);
120
+ let offset = 0;
121
+ while (offset < buf.length) {
122
+ try {
123
+ offset += writeSync(1, buf, offset, buf.length - offset);
124
+ } catch (error) {
125
+ if (error.code !== 'EAGAIN') throw error;
126
+ }
127
+ }
128
+ }
129
+
104
130
  function fail(message, code = 1) {
105
131
  console.error(message);
106
132
  process.exit(code);
@@ -141,6 +167,29 @@ function reportChecks(label, findings, checked, rules, opts) {
141
167
  process.exit(countBySeverity(resolved).errors === 0 ? 0 : 1);
142
168
  }
143
169
 
170
+ if (command === 'components') {
171
+ const opts = parseCheckFlags(rest);
172
+ const list = describeComponents(loadVocabulary());
173
+ const id = opts.rest[0];
174
+ if (id && !list.some((c) => c.id === id)) fail(formatComponents(list, { id }));
175
+ writeOut(opts.json ? JSON.stringify(id ? list.find((c) => c.id === id) : list, null, 2) : formatComponents(list, { id }));
176
+ process.exit(0);
177
+ }
178
+
179
+ if (command === 'tokens') {
180
+ const opts = parseCheckFlags(rest);
181
+ const at = opts.rest.indexOf('--family');
182
+ const family = at >= 0 ? opts.rest[at + 1] : undefined;
183
+ const desc = describeTokens(loadVocabulary());
184
+ if (family && !desc.families.some((f) => f.family === family)) fail(formatTokens(desc, { family }));
185
+ writeOut(
186
+ opts.json
187
+ ? JSON.stringify(family ? desc.families.find((f) => f.family === family) : desc, null, 2)
188
+ : formatTokens(desc, { family }),
189
+ );
190
+ process.exit(0);
191
+ }
192
+
144
193
  if (command === 'check-component') {
145
194
  const opts = parseCheckFlags(rest);
146
195
  const ids = opts.rest.length > 0 ? [opts.rest[0]] : discoverComponents();
@@ -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
+ }
@@ -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.70.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';