@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.
- package/.claude/skills/live-tokens-build-page/SKILL.md +1 -1
- package/.claude/skills/live-tokens-create-component/SKILL.md +13 -12
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +5 -3
- package/.claude/skills/live-tokens-pick-component/SKILL.md +8 -0
- package/CHANGELOG.md +29 -0
- package/README.md +2 -0
- package/bin/cli.mjs +50 -1
- package/bin/lib/catalogue.mjs +123 -0
- package/bin/lib/tokenVocabulary.mjs +32 -5
- package/package.json +1 -1
- package/src/editor/index.ts +1 -1
|
@@ -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
|
|
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
|
|
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. **
|
|
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
|
-
|
|
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.
|
|
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>` |
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
210
|
+
const tokens = new Map();
|
|
194
211
|
for (const block of extractGlobalRootBlocks(src)) {
|
|
195
|
-
|
|
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
package/src/editor/index.ts
CHANGED
|
@@ -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';
|