@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.
- package/.claude/skills/live-tokens-build-page/SKILL.md +1 -1
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +72 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +13 -12
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +10 -5
- package/.claude/skills/live-tokens-pick-component/SKILL.md +8 -0
- package/CHANGELOG.md +52 -0
- package/README.md +10 -1
- package/bin/check-page.mjs +1 -1
- package/bin/cli.mjs +77 -1
- package/bin/lib/catalogue.mjs +123 -0
- package/bin/lib/report.mjs +158 -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.
|
|
@@ -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
|
|
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
|
|
|
@@ -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,
|
|
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.
|
|
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>` |
|
|
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.
|
|
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
|
|
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?".
|
package/bin/check-page.mjs
CHANGED
|
@@ -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
|
-
|
|
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';
|