@motion-proto/live-tokens 0.71.0 → 0.72.1
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-adjust-geometry/SKILL.md +1 -1
- package/.claude/skills/live-tokens-build-page/SKILL.md +3 -3
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +18 -40
- package/.claude/skills/live-tokens-create-component/SKILL.md +40 -98
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +99 -0
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +9 -3
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -0
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +33 -80
- package/.claude/skills/live-tokens-generate-theme/SKILL.md +2 -2
- package/.claude/skills/live-tokens-pick-component/SKILL.md +3 -3
- package/CHANGELOG.md +109 -0
- package/README.md +1 -1
- package/bin/check-component.mjs +2 -11
- package/bin/check-page.mjs +3 -2
- package/bin/cli.mjs +2 -1
- package/bin/lib/catalogue.mjs +6 -2
- package/bin/lib/findings.mjs +17 -0
- package/bin/lib/report.mjs +1 -1
- package/bin/lib/tokenVocabulary.mjs +20 -0
- package/dist-plugin/adjust/index.cjs +33 -6
- package/dist-plugin/adjust/index.d.cts +2 -2
- package/dist-plugin/adjust/index.d.ts +2 -2
- package/dist-plugin/adjust/index.js +7 -6
- package/dist-plugin/{chunk-2YNERPXY.js → chunk-RIXO2E55.js} +1 -1
- package/dist-plugin/{chunk-RVE3MNKM.js → chunk-W6Y4BWFB.js} +3 -1
- package/dist-plugin/{chunk-GPIBU44G.js → chunk-YLCOIGQC.js} +25 -1
- package/dist-plugin/{dataPaths-bJTCEO4H.d.ts → dataPaths-BhWzd5cL.d.cts} +4 -0
- package/dist-plugin/{dataPaths-bJTCEO4H.d.cts → dataPaths-BhWzd5cL.d.ts} +4 -0
- package/dist-plugin/fontPairing/index.cjs +3 -1
- package/dist-plugin/fontPairing/index.d.cts +1 -1
- package/dist-plugin/fontPairing/index.d.ts +1 -1
- package/dist-plugin/fontPairing/index.js +1 -1
- package/dist-plugin/generateColorsAndType/index.cjs +28 -2
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.js +2 -2
- package/dist-plugin/index.cjs +28 -2
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +3 -3
- package/dist-plugin/migrateData/index.cjs +28 -2
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +3 -3
- package/dist-plugin/tokensCssMigrations/index.cjs +3 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +5 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +1 -11
- package/src/editor/component-editor/contract.ts +175 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +0 -8
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +5 -0
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +0 -21
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +2 -2
- package/src/editor/component-editor/scaffolding/types.ts +1 -7
- package/src/editor/core/components/aliasKinds.ts +6 -2
- package/src/editor/core/themes/migrations/2026-09-02-sectiondivider-drop-title-outline.ts +39 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/live-tokens/data/themes/autumn.json +1 -7
- package/src/live-tokens/data/themes/halloween.json +1 -7
- package/src/live-tokens/data/themes/midnight-study.json +1 -7
- package/src/live-tokens/data/themes/ocean.json +1 -7
- package/src/live-tokens/data/themes/royal-velvet.json +1 -7
- package/src/live-tokens/data/themes/sketchy.json +1 -7
- package/src/live-tokens/data/themes/spring-meadow.json +1 -7
- package/src/live-tokens/data/themes/sunset.json +1 -7
- package/src/system/components/SectionDivider.svelte +3 -143
- package/src/system/components/FloatingTokenTags.css +0 -284
- package/src/system/components/FloatingTokenTags.svelte +0 -570
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: live-tokens-adjust-geometry
|
|
3
|
-
description: Adjust corner radius, padding, gap, and border width across live-tokens components by moving each token alias along the shipped scales. Use when the user asks for pill or capsule buttons; rounded, rounder, sharp, sharper, square, softer, or harder corners; thicker or thinner borders; or density
|
|
3
|
+
description: Adjust corner radius, padding, gap, and border width across live-tokens components by moving each token alias along the shipped scales. Use when the user asks for pill or capsule buttons; rounded, rounder, sharp, sharper, square, softer, or harder corners; thicker or thinner borders; or density: space it out, tighter, denser, more compact, airier, more breathing room. Also invoked by live-tokens-generate-theme for the geometry half of a whole look. Changes shape and space aliases per component, never color, fonts, or tokens.css. Not for editing a single token (use the editor) or for color (see live-tokens-generate-theme).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Adjusting geometry
|
|
@@ -16,7 +16,7 @@ Text inside a `Card` or a `CollapsibleSection` is typed by that container, not b
|
|
|
16
16
|
|
|
17
17
|
## Layout
|
|
18
18
|
|
|
19
|
-
Pages sit inside the column grid via `--columns-count`, `--columns-gutter`, `--columns-max-width`.
|
|
19
|
+
Pages sit inside the column grid via `--columns-count`, `--columns-gutter`, `--columns-max-width`. The columns button in the overlay's header (the vertical-lines icon) draws the grid over the page while you place content.
|
|
20
20
|
|
|
21
21
|
To place children at specific page-column positions, span the parent grid (`grid-column: 1 / -1`), redeclare `repeat(var(--columns-count), 1fr)` with `--columns-gutter`, then refer to children by real page-column numbers. Never fabricate a local `repeat(N, 1fr)` with a hardcoded count: the widths drift from the page grid and the numbers stop matching `ColumnsOverlay`.
|
|
22
22
|
|
|
@@ -61,8 +61,8 @@ npx live-tokens check-page src/pages/YourPage.svelte
|
|
|
61
61
|
# or: npx @motion-proto/live-tokens check-page (every page under src/)
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
It fails on a component outside the catalogue, a prop
|
|
64
|
+
It fails on a component outside the catalogue, a prop or value the component does not declare, a deep import, a `var()` that resolves to nothing, a colour literal in any notation, a route under `/live-tokens/*`, and `site.css` imported from `main.ts`. It warns on a px or rem literal in the geometry the theme owns, a hardcoded page-column count, a raw type axis, and a route entry with no `source`. Inline `style=` attributes and `style:` directives are read the same way as the `<style>` block; a `var()` fallback is never a finding. The recipe for each rule is in **live-tokens-fix-findings**.
|
|
65
65
|
|
|
66
66
|
Warnings do not fail the run. `--strict` makes them fail, which is the setting to use when the page is meant to be fully tokenized. `--json` prints findings with a stable `rule` id, so you can work through one rule at a time and re-run. `--off=<rule>` silences a rule for a run; `"checks": { "rules": { ... } }` in `live-tokens.config.json` sets it for the project. A project scaffolded by `create` runs the checker, with `check-component`, as `npm run check:design` before every `vite build`, so the page has to pass before it can ship.
|
|
67
67
|
|
|
68
|
-
Then in dev: change a colour in `/live-tokens/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the route's `source`).
|
|
68
|
+
Then in dev: change a colour in `/live-tokens/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the route's `source`). The columns overlay shows content sitting inside `--columns-max-width`.
|
|
@@ -5,23 +5,20 @@ description: Check an existing @motion-proto/live-tokens project against its des
|
|
|
5
5
|
|
|
6
6
|
# Checking a project against its design system
|
|
7
7
|
|
|
8
|
-
The answer to "check this project" is a report, and
|
|
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.
|
|
8
|
+
The answer to "check this project" is a report. Every fact in it comes from one command; the reading of those facts, and what fixing them would cost, is yours. This skill edits nothing. When the user wants the changes made, that is **live-tokens-fix-findings**, and this report is what it starts from.
|
|
13
9
|
|
|
14
|
-
##
|
|
10
|
+
## Workflow
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
1. Run `npx live-tokens report --json`. It always exits 0: it is a reading, not a gate. Unknown command means the installed package predates it; upgrade `@motion-proto/live-tokens` first.
|
|
13
|
+
2. Read each section against the table below. For every rule with findings, say in a line what the rule holds and whether the fix is mechanical or a judgement.
|
|
14
|
+
3. Where a finding looks deliberate, name the config entry that would record the decision, and leave the decision to the user.
|
|
15
|
+
4. Report in the order under Summary, each line carrying its count, and end by handing the list to **live-tokens-fix-findings**. Do not start applying fixes here, even one-liners, because the user asked how things stand.
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
installed package predates it; upgrade `@motion-proto/live-tokens` first. The
|
|
22
|
-
sections, in the order the report gives them:
|
|
17
|
+
`npx live-tokens components <id>` and `npx live-tokens tokens --family <name>` (both take `--json`) answer any question the report raises about one component or one scale.
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
## The report's sections
|
|
20
|
+
|
|
21
|
+
| Section | Fact | When it is not clean |
|
|
25
22
|
| --- | --- | --- |
|
|
26
23
|
| `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
24
|
| `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. |
|
|
@@ -32,41 +29,22 @@ sections, in the order the report gives them:
|
|
|
32
29
|
| `usage.customUnregistered`, `usage.customUnused` | The project's own components that are unregistered or unused | Dead or half-wired work. |
|
|
33
30
|
| `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
31
|
|
|
35
|
-
|
|
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:
|
|
32
|
+
## Mechanical or judgement
|
|
43
33
|
|
|
44
|
-
- **Mechanical**: a spacing literal to its nearest `--space-*` step, a stroke
|
|
45
|
-
|
|
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.
|
|
34
|
+
- **Mechanical**: a spacing literal to its nearest `--space-*` step, a stroke to `--border-width-*`, a hardcoded column count to `var(--columns-count)`, `site.css` moved out of `main.ts`, a route given its `source`. Name any visible shift, such as a `14px` margin becoming `16px`.
|
|
35
|
+
- **Judgement**: a colour literal mapped by the role it plays rather than its hue, a raw type axis set from a text style, a prop the component does not declare mapped or dropped. Say what the choice is, not what you would pick.
|
|
51
36
|
|
|
52
|
-
|
|
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.
|
|
37
|
+
## Deliberate findings
|
|
56
38
|
|
|
57
|
-
|
|
39
|
+
A translucent overlay on an app shell, or a layout size the project owns, may be a decision rather than a miss. Say so and name the entry that would record it: `"checks": { "rules": { "<rule>": "warn" } }` in `live-tokens.config.json`. Where a whole file is not a themed surface at all, hand-tuned artwork or vendored CSS, the entry is `"checks": { "exclude": ["src/art/hero.css"] }`: a project-relative path, a directory covering what is under it, and naming the file on the command line still checks it. Prefer the narrower one: an exclusion drops one file, a severity change drops a rule everywhere. Recording either is the user's call, not yours.
|
|
58
40
|
|
|
59
|
-
|
|
41
|
+
## Summary
|
|
60
42
|
|
|
61
43
|
1. Migrations pending, and the one command that clears them.
|
|
62
44
|
2. What fails the build now: errors by rule, with the files.
|
|
63
45
|
3. What `--strict` would add: warnings by rule.
|
|
64
46
|
4. Components: unread tokens, unregistered, undescribed.
|
|
65
47
|
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.
|
|
48
|
+
6. Recommended fixes, in the order **live-tokens-fix-findings** would take them: migrations, then the largest group of errors, then the rest, then warnings. Mark each as mechanical or judgement.
|
|
69
49
|
|
|
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.
|
|
50
|
+
End with the hand-off: "Run live-tokens-fix-findings to apply these", or the subset the user chooses.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: live-tokens-create-component
|
|
3
|
-
description: Author a brand-new editable component for a @motion-proto/live-tokens project when nothing in the shipped catalogue fits
|
|
3
|
+
description: Author a brand-new editable component for a @motion-proto/live-tokens project when nothing in the shipped catalogue fits: runtime and editor Svelte files, registration, naming, state model, and verification. Use when the user asks to author, create, or build a new tokenized component; make an existing Svelte component editable in the live-tokens editor; add a component to the catalogue; register a custom component with the editor; or build a [Thing] component that does not exist in the shipped set. Not for placing an existing shipped component on a page (see live-tokens-build-page); read live-tokens-pick-component first to confirm nothing in the catalogue fits.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Authoring a component for a live-tokens project
|
|
@@ -9,22 +9,23 @@ The end state is a runtime Svelte file, an editor Svelte file, one registration,
|
|
|
9
9
|
|
|
10
10
|
## Worked examples ship inside the package
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Read a shipped component's source from the consumer's `node_modules` rather than from memory, because the files are the contract and this skill is not:
|
|
13
13
|
|
|
14
14
|
- Runtime files: `node_modules/@motion-proto/live-tokens/src/system/components/<Name>.svelte`.
|
|
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
18
|
- Multi-variant with linked siblings (`canBeLinked` + `groupKey`): `SegmentedControl`, `TabBar`. Composes another shipped component: `CodeSnippet`.
|
|
19
|
-
-
|
|
19
|
+
- Every rule below in the fewest lines: `Toggle`. Component states name themselves in the token (`--toggle-on-*`, `--toggle-disabled-*`), interaction states layer on top (`--toggle-hover-*`, `--toggle-on-hover-*`), disabled is terminal (no `--toggle-disabled-hover-*`), and each `:hover` selector has a `.force-hover` sibling so the editor's preview can paint hover tokens without a pointer.
|
|
20
|
+
- Editor files: `node_modules/@motion-proto/live-tokens/src/editor/component-editor/<Name>Editor.svelte`. `ToggleEditor` has no `groupKey` and no `canBeLinked`; for components that share base properties across variants, read `references/linked-siblings.md`.
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
Shipped editors live in `src/editor/component-editor/` because they are library-internal. For *your* component, co-locate both files in `src/system/components/`. Read the shipped files for pattern, ignore their location.
|
|
22
23
|
|
|
23
24
|
## The recipe
|
|
24
25
|
|
|
25
|
-
1. **Runtime file
|
|
26
|
-
2. **Editor file
|
|
27
|
-
3. **Register**
|
|
26
|
+
1. **Runtime file**, `src/system/components/MyWidget.svelte`. Declare every editable slot as a CSS custom property inside `:global(:root)`, defaulting to a theme token (never a raw value). The plugin parses `:global(:root)` to seed `component-configs/<id>/default.json`; variables declared anywhere else cannot be edited.
|
|
27
|
+
2. **Editor file**, `src/system/components/MyWidgetEditor.svelte`. In a `<script module>` block, declare `const component = 'mywidget'`, build a `states: Record<string, Token[]>` for each VariantGroup, and export the flat union as `allTokens: Token[]`. Components with linked siblings also build a `linkableContexts: Map<string, string>` (read `references/linked-siblings.md`). Components with structural or display controls that are not token values (alignment, element visibility, layout position) also export an `intrinsics: IntrinsicSpec[]` (read `references/intrinsics.md`). In the runtime `<script>` block, mount `ComponentEditorBase` with one `VariantGroup` per variant.
|
|
28
|
+
3. **Register** by passing the component to `bootLiveTokens` in `src/main.ts`, the boot the scaffold generates:
|
|
28
29
|
```ts
|
|
29
30
|
import { bootLiveTokens } from '@motion-proto/live-tokens';
|
|
30
31
|
import App from './App.svelte';
|
|
@@ -41,41 +42,16 @@ For pattern reference, read any shipped component's source directly from the con
|
|
|
41
42
|
}],
|
|
42
43
|
});
|
|
43
44
|
```
|
|
44
|
-
|
|
45
|
-
4. **Say what it is for.** The runtime file's leading HTML comment is the
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
54
|
-
5. **Join the sketch layer** — the effect draws a fixed set of parts, so a new
|
|
55
|
-
component stays crisp while the page around it goes hand-drawn until it opts
|
|
56
|
-
in. A consumer component carries one of four reserved classes on its root and
|
|
57
|
-
names the five `--sketch-*` values it is drawn with; a first-party component
|
|
58
|
-
adds a `PartSpec` row instead. The layer also takes `background`,
|
|
59
|
-
`border-color`, `box-shadow`, `overflow`, `position` and both pseudo-elements
|
|
60
|
-
away from the element it draws, which constrains where the class can go. Read
|
|
61
|
-
`references/sketch-mode.md`.
|
|
62
|
-
6. **Gate on the checker.** Run it, fix every error, and run it again. Do not
|
|
63
|
-
call the component done while it reports one:
|
|
45
|
+
`bootLiveTokens` calls `registerComponent` for you after its editor init hooks and before it seeds configs, so a standalone `registerComponent(...)` placed *before* `bootLiveTokens` lands in the wrong window and can leave editor changes disconnected from the live page. Call `registerComponent` directly only when the app mounts manually, and then before `mount(App, ...)`. Registering against a built-in id wins with a console warning; the right call is a unique id.
|
|
46
|
+
4. **Say what it is for.** The runtime file's leading HTML comment is the component's description. `npx live-tokens components` prints it beside the id with the variants and props read from `interface Props` (`--json` for data), which is how **live-tokens-pick-component** weighs a project's own component against the shipped set: no skill file is edited, and nothing is lost when `setup-claude` refreshes the skills. Name the job it does and what it is not for. A directory other than `src/system/components` goes in `"componentDirs"` in `live-tokens.config.json`. A first-party component is also added to the picker's **Catalogue** line, which `check:skills` holds.
|
|
47
|
+
5. **Join the sketch layer.** The effect draws a fixed set of parts, so a new component stays crisp while the page around it goes hand-drawn until it opts in. A consumer component carries one of four reserved classes on its root and names the five `--sketch-*` values it is drawn with; a first-party component adds a `PartSpec` row instead. The layer also takes `background`, `border-color`, `box-shadow`, `overflow`, `position` and both pseudo-elements away from the element it draws, which constrains where the class can go. Read `references/sketch-mode.md`.
|
|
48
|
+
6. **Gate on the checker.** Run it, fix every error, and run it again. Do not call the component done while it reports one:
|
|
64
49
|
```bash
|
|
65
50
|
npx live-tokens check-component <id> --strict --json
|
|
66
51
|
```
|
|
67
|
-
`--json` gives each finding a stable `rule` id and a line number, so work one
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
is a naming or token decision that is cheaper to make now than to migrate
|
|
71
|
-
later. Exit code 0 is the gate. With no id it checks every component under
|
|
72
|
-
`src/system/components`; a project scaffolded by `create` runs that as
|
|
73
|
-
`npm run check:design` before every `vite build`.
|
|
74
|
-
|
|
75
|
-
If it rejects a suffix, do not invent a new name for the role. Find a shipped
|
|
76
|
-
component that paints the same thing and use the name it uses: the catalogue
|
|
77
|
-
is the worked reference, and `bin/check-component.test.ts` holds all 26 of
|
|
78
|
-
them to this same contract.
|
|
52
|
+
`--json` gives each finding a stable `rule` id and a line number, so work one rule at a time. `--strict` fails on warnings too, the right setting for a new component: every warning is a naming or token decision that is cheaper to make now than to migrate later. Exit code 0 is the gate. With no id it checks every component under `src/system/components`; a project scaffolded by `create` runs that as `npm run check:design` before every `vite build`.
|
|
53
|
+
|
|
54
|
+
If it rejects a suffix, do not invent a new name for the role. Find a shipped component that paints the same thing and use the name it uses: every shipped component passes this same check, so the catalogue is the worked reference.
|
|
79
55
|
7. **Verify** with the checklist at the bottom of this file, then place the component on a page with **live-tokens-build-page**.
|
|
80
56
|
|
|
81
57
|
## Token discipline
|
|
@@ -86,11 +62,11 @@ For pattern reference, read any shipped component's source directly from the con
|
|
|
86
62
|
--<componentId>-<part|variant>[-<state>][-<element>]-<property>
|
|
87
63
|
```
|
|
88
64
|
|
|
89
|
-
- `componentId
|
|
90
|
-
- `part` or `variant
|
|
91
|
-
- `state` (optional)
|
|
92
|
-
- `element` (optional)
|
|
93
|
-
- `property
|
|
65
|
+
- `componentId`: the literal id passed to `registerComponent()`. Lowercase, no dashes, no abbreviations (`segmentedcontrol` not `sc`). The file id matches: `MyWidget.svelte` is id `mywidget`.
|
|
66
|
+
- `part` or `variant`: the sub-region (`bar`, `option`, `track`, `header`, `body`, `footer`, `overlay`, `value`, `label`), or, on a component whose variants differ in more than one property, the variant name: `--badge-accent-surface`, `--callout-danger-border`. A component with both stacks them outer to inner, so the bar in its small size is `--segmentedcontrol-bar-small-padding`.
|
|
67
|
+
- `state` (optional): interaction or component state (`hover`, `disabled`, `selected`, `focus`). **Always before the property.**
|
|
68
|
+
- `element` (optional): sub-element inside the part (`dot`, `icon`, `label`, `text`).
|
|
69
|
+
- `property`: theme role or CSS property. Always last.
|
|
94
70
|
|
|
95
71
|
### Suffix vocabulary
|
|
96
72
|
|
|
@@ -121,12 +97,12 @@ control behind it.
|
|
|
121
97
|
### Rules that bite
|
|
122
98
|
|
|
123
99
|
- **Fixed overlays must portal to `<body>`.** Any `position: fixed` layer is trapped by a transformed or `contain`ed ancestor, which real pages and the editor's preview pane both have. `check:overlay-portal` fails the build without it. Read `references/fixed-overlays.md` before authoring a modal, lightbox, or backdrop.
|
|
124
|
-
- **State before property.** `--mywidget-button-hover-surface`
|
|
125
|
-
- **Every default resolves to a theme token.** A component token names a semantic property; its default is the theme token that property reads
|
|
126
|
-
- **No abbreviations.** `bg`
|
|
100
|
+
- **State before property.** `--mywidget-button-hover-surface` passes; `--mywidget-button-surface-hover` breaks sibling matching. Disabled is terminal in the name too: `-disabled-hover-` and `-selected-disabled-` describe states that never paint, and `check-component` rejects both.
|
|
101
|
+
- **Every default resolves to a theme token.** A component token names a semantic property; its default is the theme token that property reads, which is what makes the component repaint when the theme changes. `var(--surface-primary)` passes; `#6a4ce8`, `white`, `var(--surface-imaginary)`, and a bare `16rem` all fail, because `check-component` rejects a colour literal in any notation, a `var()` naming a token that does not exist, and a default with no token behind it. Composing tokens counts and is common: `color-mix(in srgb, var(--surface-neutral-lower) 70%, transparent)`, or `calc(var(--space-64) * 4)` for a width the spacing scale does not reach. The one value allowed without a token is a structural keyword (`contain`, `start`, `none`), and only when the editor declares it in `intrinsics`.
|
|
102
|
+
- **No abbreviations.** `bg` is `surface`; `fg` is `text`; component ids are never abbreviated.
|
|
127
103
|
- **Text aliases.** Neutral scale is `--text-primary` / `--text-secondary` / `--text-tertiary` / `--text-muted` / `--text-disabled`. Family-tinted is `--text-primary-color`, `--text-accent`, `--text-success`. There is no `--text-neutral`.
|
|
128
|
-
- **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'`
|
|
129
|
-
- **Let the type-group helpers derive slot-scoped keys
|
|
104
|
+
- **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'` stay distinct; a bare `groupKey: 'font-family'` silently merges the slots into one link tree. Single-slot components can use a bare typography `groupKey`; add the slot prefix the moment a second slot appears. The same trap applies to type-group colours (two slots ending in `-text` collapsing to one `text` key).
|
|
105
|
+
- **Let the type-group helpers derive slot-scoped keys.** When you build typography tokens with `buildTypeGroupColorTokens` / `buildTypeGroupTokens` / `buildTypeGroupFontTokens`, pass `{ component, variants }` so each slot gets a distinct, structural `groupKey`:
|
|
130
106
|
|
|
131
107
|
```ts
|
|
132
108
|
// variants = the variant/state segment strings as they appear in the variable name
|
|
@@ -135,24 +111,23 @@ control behind it.
|
|
|
135
111
|
...buildTypeGroupFontTokens(typeGroups, { component, variants: [...VARIANTS] }),
|
|
136
112
|
```
|
|
137
113
|
|
|
138
|
-
The helper strips the `--<component>-` prefix and those segments, keeping the rest: `--mywidget-header-default-text`
|
|
139
|
-
|
|
114
|
+
The helper strips the `--<component>-` prefix and those segments, keeping the rest: `--mywidget-header-default-text` becomes `header-text`, and `--mywidget-header-default-text-font-family` becomes `header-text-font-family`. Two parts ending in the same word stay distinct; one slot across variants collapses to one key. To override a single derived key, set `colorGroupKey` on that type-group config; it wins and is never recomputed. There is no name-based fallback: a bare `buildTypeGroupColorTokens` call emits un-grouped (solo) colours rather than guessing, and a bare *font* helper across multiple slots is a `check-component` warning, because its default keys would merge the slots' fonts.
|
|
140
115
|
|
|
141
116
|
## State model
|
|
142
117
|
|
|
143
|
-
Components *can* have two state axes. Many
|
|
118
|
+
Components *can* have two state axes. Many do not: container and messaging components (Card, Badge, Callout, CollapsibleSection) have only variants, no hover or disabled. Skip the rest of this section for those.
|
|
144
119
|
|
|
145
|
-
When a component does have states,
|
|
120
|
+
When a component does have states, keep the two axes apart:
|
|
146
121
|
|
|
147
|
-
- **Component states**
|
|
148
|
-
- **Interaction states**
|
|
122
|
+
- **Component states** are mutually exclusive top-level fieldsets: `default`, `selected`, `disabled` (names vary by component). One fieldset per component state.
|
|
123
|
+
- **Interaction states** are a select *inside* each component-state fieldset: `default`, `hover`. Add `focus` or `active` later if needed.
|
|
149
124
|
|
|
150
125
|
Rules:
|
|
151
126
|
|
|
152
|
-
- **Disabled is terminal.** A disabled component
|
|
153
|
-
- **`selected-disabled` is impossible.**
|
|
154
|
-
- **Parts
|
|
155
|
-
- **
|
|
127
|
+
- **Disabled is terminal.** A disabled component cannot be hovered or focused. The `disabled` fieldset is flat, with no interaction selector.
|
|
128
|
+
- **`selected-disabled` is impossible.** Do not author tokens or fieldsets for it.
|
|
129
|
+
- **Parts are not states.** Dialog's `overlay | header | body | footer` are *parts* (all present at once), not states. The VariantGroup tab strip defaults its label to "Element" (neutral). If you label tabs anywhere, use **part** for structure and **state** for runtime conditions. Never call a footer a state.
|
|
130
|
+
- **Do not call interaction states "option states" or "selected states"** in the UI. `selected` is a *component* state.
|
|
156
131
|
|
|
157
132
|
Token naming consequence:
|
|
158
133
|
|
|
@@ -161,14 +136,14 @@ Token naming consequence:
|
|
|
161
136
|
--mywidget-option-disabled-surface ✗ implies disabled is an interaction state
|
|
162
137
|
--mywidget-option-hover-surface ✓ default-component-state, hover-interaction
|
|
163
138
|
--mywidget-selected-hover-surface ✓ selected-component-state, hover-interaction
|
|
164
|
-
--mywidget-selected-disabled-text ✗ selected-disabled
|
|
139
|
+
--mywidget-selected-disabled-text ✗ selected-disabled does not exist
|
|
165
140
|
```
|
|
166
141
|
|
|
167
142
|
## User-facing copy
|
|
168
143
|
|
|
169
|
-
Strings you author for the editor UI use periods and commas, never em-dashes
|
|
144
|
+
Strings you author for the editor UI use periods and commas, never em-dashes, which read as an AI tell. This applies to `title=` and `description=` on `ComponentEditorBase`, token row labels, info popovers, and any text inside `previewActions` / `canvasToolbarExtras` snippets. Code comments are unaffected.
|
|
170
145
|
|
|
171
|
-
|
|
146
|
+
Custom chrome inside an editor snippet is rare, since `ComponentEditorBase` and `VariantGroup` carry the standard chrome. Where you add some, keep it greyscale (no accent colours) and reference heading sizes via `--ui-font-size-md` / `-lg` / `-2xl` rather than pixel literals.
|
|
172
147
|
|
|
173
148
|
## Public imports only
|
|
174
149
|
|
|
@@ -187,26 +162,6 @@ That covers everything the worked examples use. Additional primitives (`LinkedBl
|
|
|
187
162
|
|
|
188
163
|
**Never deep-import `node_modules/@motion-proto/live-tokens/src/...`.** Reading those files for pattern reference is fine; importing them at runtime is not. If you need something not exported, file an issue rather than reaching in.
|
|
189
164
|
|
|
190
|
-
## Worked example: the shipped Toggle
|
|
191
|
-
|
|
192
|
-
Toggle exercises every rule above in the fewest lines. Read both files from the package rather than from memory, because they are the contract and this skill is not:
|
|
193
|
-
|
|
194
|
-
- `node_modules/@motion-proto/live-tokens/src/system/components/Toggle.svelte`
|
|
195
|
-
- `node_modules/@motion-proto/live-tokens/src/editor/component-editor/ToggleEditor.svelte`
|
|
196
|
-
|
|
197
|
-
What to notice in the runtime file:
|
|
198
|
-
|
|
199
|
-
- Component states (`on`, `disabled`) name themselves in the token: `--toggle-on-*`, `--toggle-disabled-*`.
|
|
200
|
-
- Interaction states layer on top: `--toggle-hover-*` for default+hover, `--toggle-on-hover-*` for on+hover.
|
|
201
|
-
- Disabled is terminal: no `--toggle-disabled-hover-*`, no `--toggle-on-disabled-*`.
|
|
202
|
-
- The `force-hover` class pairs with the editor's preview hook so hover tokens paint without a real pointer. Each `:hover` selector has a matching `.force-hover` sibling.
|
|
203
|
-
|
|
204
|
-
What to notice in the editor file:
|
|
205
|
-
|
|
206
|
-
- No `groupKey`, no `canBeLinked`: Toggle has no linked siblings. For components that share base properties across variants, read `references/linked-siblings.md`.
|
|
207
|
-
|
|
208
|
-
For your own component, copy the pattern and substitute your id. Registering against a built-in id wins with a console warning, but the right call is a unique id.
|
|
209
|
-
|
|
210
165
|
## Extensions
|
|
211
166
|
|
|
212
167
|
Read the sketch reference for every component; the other two only when they apply.
|
|
@@ -217,24 +172,11 @@ Read the sketch reference for every component; the other two only when they appl
|
|
|
217
172
|
|
|
218
173
|
## Verification checklist
|
|
219
174
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
```bash
|
|
223
|
-
npx live-tokens check-component <id>
|
|
224
|
-
# or: npx @motion-proto/live-tokens check-component <id>
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
It enforces the file layout, the `:global(:root)` block, token-suffix vocabulary, state-before-property rule, the terminal disabled state, public-imports rule, that every token an editor row names is declared in the runtime, and that the id is registered, via either `bootLiveTokens({ components: [{ id }] })` or a direct `registerComponent({ id })` call. On the value side it fails on a colour literal in any notation, on a default reading a token that does not exist, and on a default with no theme token behind it that the editor does not declare an intrinsic.
|
|
228
|
-
|
|
229
|
-
It *warns* (non-fatal) when a token-backed default still carries a px or rem term, and when a type-group font helper is called bare across multiple slots, which would merge their fonts into one link tree.
|
|
230
|
-
|
|
231
|
-
Exit code 0 means the static contract is met. Resolve warnings before shipping, or run with `--strict` to make them fail. `--json` prints findings with a stable `rule` id, so you can work through one rule at a time and re-run.
|
|
232
|
-
|
|
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.
|
|
175
|
+
Step 6 of the recipe is the static gate: `npx live-tokens check-component <id>` at exit 0, with `--strict` clean or its warnings resolved. It enforces the file layout, the `:global(:root)` block, the suffix vocabulary, state-before-property, the terminal disabled state, public imports, that every token an editor row names is declared in the runtime, that every default reads a theme token, and that the id is registered through `bootLiveTokens({ components: [{ id }] })` or a direct `registerComponent({ id })` call.
|
|
234
176
|
|
|
235
|
-
|
|
177
|
+
**Then run the registry contract test.** `checkRegistryEntry`, from `@motion-proto/live-tokens/component-editor/contract`, takes one registry entry and returns a violation line per failure, so a suite over your own components is a `describe.each` and one call. It verifies that the registration resolves to a real `sourceFile` and a non-empty schema, that schema variables are unique, that every editable token is declared in the runtime `<style>` block and seeded in `component-configs/<id>/default.json`, that a token declaring `minOpacity` seeds at or above its floor, and that `setComponentAlias` round-trips the alias through the slice. The test file and its path options are in `references/contract-tests.md`. Inside the package, `registryContract.test.ts` runs that same check over `builtInRegistry`, so a first-party component is covered the moment it lands there.
|
|
236
178
|
|
|
237
|
-
**If your component declares `intrinsics`, the intrinsics contract test covers it too.** `
|
|
179
|
+
**If your component declares `intrinsics`, the intrinsics contract test covers it too.** `intrinsicsContract.test.ts` asserts, per (intrinsic, variant), that the runtime `:global(:root)` declares a default, that it is one of the spec's `values`, and that the editor's `default` equals it. This is what would have caught a getter defaulting to `center` while `:global(:root)` says `start`.
|
|
238
180
|
|
|
239
181
|
Finally navigate to `/live-tokens/components` and confirm the runtime behaviours no static check can see:
|
|
240
182
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# The registry contract as a test in your own project
|
|
2
|
+
|
|
3
|
+
`checkRegistryEntry` is the contract the package holds its own 26 components
|
|
4
|
+
to, exported so a project outside the package can run it over its own. It takes
|
|
5
|
+
one registry entry and returns a violation line per failure; an empty array is
|
|
6
|
+
the pass.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// tests/registryContract.test.ts
|
|
10
|
+
// @vitest-environment happy-dom
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { getComponentRegistryEntries, registerComponent } from '@motion-proto/live-tokens';
|
|
13
|
+
import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';
|
|
14
|
+
import MyWidgetEditor, { allTokens } from '../src/system/components/MyWidgetEditor.svelte';
|
|
15
|
+
|
|
16
|
+
registerComponent({
|
|
17
|
+
id: 'mywidget',
|
|
18
|
+
label: 'My Widget',
|
|
19
|
+
icon: 'fas fa-magic',
|
|
20
|
+
sourceFile: 'src/system/components/MyWidget.svelte',
|
|
21
|
+
editorComponent: MyWidgetEditor,
|
|
22
|
+
schema: allTokens,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const mine = getComponentRegistryEntries().filter((e) => e.origin === 'custom');
|
|
26
|
+
|
|
27
|
+
describe.each(mine.map((e) => [e.id, e] as const))('%s', (_id, entry) => {
|
|
28
|
+
it('meets the registry contract', () => {
|
|
29
|
+
expect(checkRegistryEntry(entry)).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Two lines there are load-bearing.
|
|
35
|
+
|
|
36
|
+
- **Register at the top of the test file**, rather than importing `main.ts`.
|
|
37
|
+
The entries have to exist before `describe.each` reads them, and
|
|
38
|
+
`bootLiveTokens` would mount the app.
|
|
39
|
+
- **Filter on `origin`.** The registry always carries the shipped components
|
|
40
|
+
too, and their `sourceFile` paths are relative to the package root, not
|
|
41
|
+
yours. Without the filter every built-in fails on a path that does not exist
|
|
42
|
+
in your project.
|
|
43
|
+
|
|
44
|
+
## Setup
|
|
45
|
+
|
|
46
|
+
`vitest` and `happy-dom` as devDependencies, and the svelte plugin already in
|
|
47
|
+
`vite.config.ts` so the editor `.svelte` import resolves. The helper reads the
|
|
48
|
+
runtime file and `default.json` off disk, which is why it is node-only and has
|
|
49
|
+
its own subpath.
|
|
50
|
+
|
|
51
|
+
The package ships Svelte and TypeScript source, and `bootLiveTokens` imports the
|
|
52
|
+
FontAwesome stylesheet. Left external, Node meets that `.css` and stops with
|
|
53
|
+
`Unknown file extension ".css"`, before a single test runs. Inline both so Vite
|
|
54
|
+
transforms them:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// vitest.config.ts
|
|
58
|
+
import { defineConfig, mergeConfig } from 'vitest/config';
|
|
59
|
+
import viteConfig from './vite.config';
|
|
60
|
+
|
|
61
|
+
export default mergeConfig(
|
|
62
|
+
viteConfig,
|
|
63
|
+
defineConfig({
|
|
64
|
+
test: {
|
|
65
|
+
server: { deps: { inline: [/@motion-proto\/live-tokens/, /@fortawesome/] } },
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Paths
|
|
72
|
+
|
|
73
|
+
Paths resolve against `process.cwd()` and `src/live-tokens/data/component-configs`.
|
|
74
|
+
A project that moved either passes them:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
checkRegistryEntry(entry, { projectRoot, componentConfigsDir });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`componentConfigsDir` is the same directory `live-tokens.config.json` names.
|
|
81
|
+
|
|
82
|
+
## What it holds
|
|
83
|
+
|
|
84
|
+
1. **Registration** — `sourceFile` resolves to a real file, the schema is non-empty.
|
|
85
|
+
2. **Uniqueness** — no schema variable is declared twice.
|
|
86
|
+
3. **Editor to runtime** — every editable token's CSS var is declared in the
|
|
87
|
+
runtime's `<style>` block, so an edit has something to repaint.
|
|
88
|
+
4. **Editor to default config** — every editable token has a seed alias in
|
|
89
|
+
`component-configs/<id>/default.json`, so the component adopts with full
|
|
90
|
+
defaults. A component with no `default.json` is editor-only; this check and
|
|
91
|
+
the next one skip it.
|
|
92
|
+
5. **Opacity floors** — a token declaring `minOpacity` ships a default at or
|
|
93
|
+
above it, so a floating panel starts out legible over page content.
|
|
94
|
+
6. **Round-trip** — `setComponentAlias` persists into the slice under the same key.
|
|
95
|
+
|
|
96
|
+
Checks 3 and 4 exclude `hidden: true` tokens, `kind: 'gradient'` tokens (stored
|
|
97
|
+
as gradient objects, not vars), and `-padding-(top|right|bottom|left)` suffixes
|
|
98
|
+
(written on demand by the split-padding UI and read through the `themed-padding`
|
|
99
|
+
mixin's fallback chain, so they exist as neither `:root` declarations nor seeds).
|
|
@@ -85,10 +85,17 @@ preview of the hover state, and a hover the sketch layer cannot paint reads as
|
|
|
85
85
|
no hover at all once the real background is transparent.
|
|
86
86
|
|
|
87
87
|
An inner part that carries its own surface (a header strip, a footer) takes its
|
|
88
|
-
own class and its own five values.
|
|
88
|
+
own class and its own five values. The class is easy to forget, because the part
|
|
89
|
+
already has its own values and looks finished without it — a part carrying only
|
|
90
|
+
the values is left crisp, and reads as a hard-edged rectangle dropped inside a
|
|
91
|
+
drawn box. No checker sees it. Where such a part draws no outline, bind the
|
|
89
92
|
hatch ink to the ink its **parent** is outlined in, so the component reads as one
|
|
90
93
|
drawing rather than a shaded panel dropped into a box:
|
|
91
94
|
|
|
95
|
+
```svelte
|
|
96
|
+
<span class="mywidget-header sketch-chip">{label}</span>
|
|
97
|
+
```
|
|
98
|
+
|
|
92
99
|
```css
|
|
93
100
|
.mywidget-header {
|
|
94
101
|
--sketch-fill: var(--mywidget-header-surface);
|
|
@@ -176,8 +183,7 @@ inherits, so one declaration covers everything under it:
|
|
|
176
183
|
|
|
177
184
|
Travel is stated in px against a glyph whose size the layer cannot know, so the
|
|
178
185
|
dial that suits a card's worth of artwork tears a 16px icon apart. Reach for the
|
|
179
|
-
soft bank before reaching for `none`.
|
|
180
|
-
example.
|
|
186
|
+
soft bank before reaching for `none`.
|
|
181
187
|
|
|
182
188
|
## First-party components
|
|
183
189
|
|
|
@@ -64,6 +64,12 @@ to that list, so the two cannot drift apart.
|
|
|
64
64
|
| `-padding` | Internal spacing |
|
|
65
65
|
| `-gap` | Spacing between sibling elements |
|
|
66
66
|
|
|
67
|
+
`-width`, `-height` and `-size` are the fall-through: any dimension with no
|
|
68
|
+
more specific name behind it. They read the `--space-*` scale through the same
|
|
69
|
+
picker `-gap` uses, and they match last, so `-border-width`, `-divider-height`,
|
|
70
|
+
`-icon-size` and the rest claim their token first. Reach for the specific name
|
|
71
|
+
when one fits; a stroke is `-border-width` even where the CSS says `outline:`.
|
|
72
|
+
|
|
67
73
|
## Typography
|
|
68
74
|
|
|
69
75
|
| Suffix | Meaning |
|