@motion-proto/live-tokens 0.76.0 → 0.78.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.
Files changed (50) hide show
  1. package/.claude/skills/live-tokens-check-compliance/SKILL.md +2 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +3 -3
  3. package/.claude/skills/live-tokens-create-component/references/contract-tests.md +99 -3
  4. package/.claude/skills/live-tokens-create-page/SKILL.md +5 -4
  5. package/.claude/skills/live-tokens-fix-findings/SKILL.md +7 -0
  6. package/CHANGELOG.md +90 -0
  7. package/bin/check-component.mjs +2 -0
  8. package/bin/check-page.mjs +131 -9
  9. package/bin/cli.mjs +25 -3
  10. package/bin/contractRunner.mjs +349 -30
  11. package/bin/lib/pageRoutes.mjs +309 -0
  12. package/package.json +5 -3
  13. package/src/editor/overlay/ColumnsOverlay.svelte +1 -1
  14. package/src/editor/overlay/LiveEditorOverlay.svelte +1 -0
  15. package/src/editor/overlay/LiveTokensRouter.svelte +1 -0
  16. package/src/editor/skill-atlas/skillSources.generated.ts +5 -5
  17. package/src/editor/skill-atlas/trees/check-compliance.ts +18 -18
  18. package/src/editor/skill-atlas/trees/create-component.ts +8 -3
  19. package/src/editor/skill-atlas/trees/create-page.ts +26 -26
  20. package/src/editor/skill-atlas/trees/fix-findings.ts +46 -11
  21. package/src/testing-js/{chunk-LXR3MN6N.js → chunk-4JQX6WWL.js} +1 -1
  22. package/src/testing-js/chunk-4JQX6WWL.js.map +1 -0
  23. package/src/testing-js/{chunk-AO7EZHYV.js → chunk-GNIUPIU2.js} +3 -3
  24. package/src/testing-js/{chunk-ZMSX6CXR.js → chunk-NB3NZRBM.js} +13 -2
  25. package/src/testing-js/chunk-NB3NZRBM.js.map +1 -0
  26. package/src/testing-js/{chunk-L73N4NSO.js → chunk-WIZ6W7UT.js} +2 -2
  27. package/src/testing-js/component-alias.contract.js +2 -2
  28. package/src/testing-js/component-editor.contract.js +21 -5
  29. package/src/testing-js/component-editor.contract.js.map +1 -1
  30. package/src/testing-js/component-render.contract.js +4 -4
  31. package/src/testing-js/index.d.ts +9 -3
  32. package/src/testing-js/index.js +20 -12
  33. package/src/testing-js/index.js.map +1 -1
  34. package/src/testing-js/page-compliance.contract.js +829 -0
  35. package/src/testing-js/page-compliance.contract.js.map +1 -0
  36. package/src/testing-js/{vitest-BE6uGF31.d.ts → vitest-C-wNWcoA.d.ts} +21 -1
  37. package/src/testing-js/vitest.d.ts +1 -1
  38. package/src/testing-js/vitest.js +1 -1
  39. package/template/README.md +17 -5
  40. package/template/live-tokens.testing.ts +6 -0
  41. package/template/package.json +5 -2
  42. package/template/src/main.ts +1 -0
  43. package/template/src/pages/Home.svelte +13 -0
  44. package/template/src/registerComponents.ts +4 -0
  45. package/template/tests/contracts.ts +3 -0
  46. package/template/tsconfig.json +1 -1
  47. package/src/testing-js/chunk-LXR3MN6N.js.map +0 -1
  48. package/src/testing-js/chunk-ZMSX6CXR.js.map +0 -1
  49. /package/src/testing-js/{chunk-AO7EZHYV.js.map → chunk-GNIUPIU2.js.map} +0 -0
  50. /package/src/testing-js/{chunk-L73N4NSO.js.map → chunk-WIZ6W7UT.js.map} +0 -0
@@ -9,6 +9,8 @@ Run `npx live-tokens report`. The CLI prints a report with the sections in the R
9
9
 
10
10
  `report` never runs the component contract suites. The `check-component` test run does, and it is what live-tokens-create-component calls to validate a component's runtime behavior.
11
11
 
12
+ `report` stays static for pages too. The `check-page` test run is what live-tokens-create-page calls for a rendered page, in a browser against the project's own route, and it reports `page-component-paint`, `page-text-style`, `page-contrast`, `page-grid`, and `page-overflow` findings the same way `report`'s static findings read: by rule, with a fix.
13
+
12
14
  ## Workflow
13
15
 
14
16
  When `report` is an unknown command, route the dependency upgrade to **live-tokens-fix-findings**. Resume the audit after the upgrade.
@@ -13,7 +13,7 @@ Create a component whose structure and behavior serve the user's purpose. Give e
13
13
  2. Design the properties: separate the component's parts, variants, and states, then write one row per editable role with its token and the CSS it controls, named the way the shipped components name the same role.
14
14
  3. Write the runtime file: the usage comment and the `:global(:root)` block. A structural choice is an intrinsic. Every component joins the sketch layer, and a fixed overlay portals to `<body>`.
15
15
  4. Write the editor file: the schema, the preview props, and the markup. Variants that share a value are linked.
16
- 5. Register the component in the module `src/main.ts` and `live-tokens.testing.ts` both name.
16
+ 5. Register the component in the module `src/main.ts` and `live-tokens.testing.ts` both name, and write its contract in the module `contractsModule` names.
17
17
  6. Run **live-tokens-check-compliance**, then `npx live-tokens check-component <id> --tests --strict --json` until exit 0 with complete applicable coverage, then the Svelte check and the build.
18
18
  7. Reply with the files, the id, the props, and each check's result. Then place the component on a page with **live-tokens-create-page**.
19
19
 
@@ -204,7 +204,7 @@ registerComponent({
204
204
  });
205
205
  ```
206
206
 
207
- Import the module from `src/main.ts`, before `bootLiveTokens` or `mount`, and name it as `registrySetup` in `live-tokens.testing.ts`. One module then serves the running app and `check-component --tests`, which imports it to see the registration without mounting the app: read `references/contract-tests.md`. A component that declares intrinsics adds `intrinsics` to the entry. `check-component` finds the registration by the id literal inside the call.
207
+ Import the module from `src/main.ts`, before `bootLiveTokens` or `mount`, and name it as `registrySetup` in `live-tokens.testing.ts`. One module then serves the running app and `check-component --tests`, which imports it to see the registration without mounting the app. The component's contract, one `ComponentContract` in the module `contractsModule` names, is what the eight component suites run; without it `--tests` reports `contract-missing`. Read `references/contract-tests.md` for both. A component that declares intrinsics adds `intrinsics` to the entry. `check-component` finds the registration by the id literal inside the call.
208
208
 
209
209
  At boot the plugin reads the `:global(:root)` block and writes `component-configs/<id>/default.json`, one token per property. An edit in the editor writes `_working.json`; Save As writes a named config. The assignments stay token references through that flow.
210
210
 
@@ -218,7 +218,7 @@ A fixed overlay portals to `<body>`: read `references/fixed-overlays.md`. A cont
218
218
 
219
219
  ## Verification
220
220
 
221
- 1. Run **live-tokens-check-compliance** and address its findings with **live-tokens-fix-findings**. Then run `npx live-tokens check-component <id> --tests --strict --json`. Inside the live-tokens repository, run `node bin/cli.mjs check-component <id> --tests --strict --json`. Each finding carries a rule id and a line; `--off=<rule>` silences a rule for one run. `--tests` runs the registry contract and the component contract suites, and its JSON reports coverage by rule. A `tests-not-installed` finding names the missing package; install `@playwright/test`, `vitest`, and `happy-dom` as devDependencies, then `npx playwright install chromium`. Fix every finding and rerun until exit 0 with complete applicable coverage and no disabled checks.
221
+ 1. Run **live-tokens-check-compliance** and address its findings with **live-tokens-fix-findings**. Then run `npx live-tokens check-component <id> --tests --strict --json`. Inside the live-tokens repository, run `node bin/cli.mjs check-component <id> --tests --strict --json`. Each finding carries a rule id and a line; `--off=<rule>` silences a rule for one run. `--tests` runs the registry contract and, for a component with a contract in the module `contractsModule` names, the component contract suites; its JSON reports coverage by rule. A `tests-not-installed` finding names the missing package; install `@playwright/test`, `vitest`, and `happy-dom` as devDependencies, then `npx playwright install chromium`. Fix every finding and rerun until exit 0 with complete applicable coverage and no disabled checks.
222
222
  2. Run the project's Svelte check and its build.
223
223
  3. Reply with the files, the component id, the props, and the results of steps 1 and 2, naming any check the environment prevented.
224
224
 
@@ -17,9 +17,11 @@ It resolves a shipped component's `sourceFile` against the package and yours
17
17
  against your project, and reports a component that exists as files and never
18
18
  reached a registration.
19
19
 
20
- Add `@playwright/test`, `vitest`, and `happy-dom` as devDependencies, then
21
- `npx playwright install chromium`. A missing one is a `tests-not-installed`
22
- finding naming the install command.
20
+ A project scaffolded by `create` has `@playwright/test`, `vitest`, and
21
+ `happy-dom` installed, with `live-tokens.testing.ts`, `src/registerComponents.ts`,
22
+ and `tests/contracts.ts` in place. Any other project adds the three as
23
+ devDependencies. Both run `npx playwright install chromium` once. A missing
24
+ one is a `tests-not-installed` finding naming the install command.
23
25
 
24
26
  Name the module that registers your components, in `live-tokens.testing.ts` at
25
27
  the project root, as a plain quoted string:
@@ -60,6 +62,100 @@ of their own.
60
62
  you; it narrows the run to one component and fails when no component is
61
63
  registered under that id.
62
64
 
65
+ ## The component contract
66
+
67
+ The eight Playwright suites drive one `ComponentContract` per component. The
68
+ package ships one for each of its own components and none for yours. A run
69
+ with `LIVE_TOKENS_COMPONENT=<id>` and no contract for `<id>` reports one
70
+ `contract-missing` finding, which names the setting that unlocks the suites.
71
+
72
+ Name the module that exports your contracts as `contractsModule`, next to
73
+ `registrySetup`:
74
+
75
+ ```ts
76
+ // live-tokens.testing.ts
77
+ export default defineTestingConfig({
78
+ registrySetup: 'src/registerComponents.ts',
79
+ contractsModule: 'tests/contracts.ts',
80
+ });
81
+ ```
82
+
83
+ The module exports a `ComponentContract[]`, as the default export or a named
84
+ `contracts` export. The type ships from `@motion-proto/live-tokens/testing`,
85
+ and the doc comment on each field is the reference for it. A contract
86
+ declares the component's parts as selectors inside the preview, then one
87
+ expectation per obligation:
88
+
89
+ ```ts
90
+ // tests/contracts.ts
91
+ import type { ComponentContract } from '@motion-proto/live-tokens/testing';
92
+
93
+ export const contracts: ComponentContract[] = [
94
+ {
95
+ id: 'statcard',
96
+ origin: 'custom',
97
+ root: 'root',
98
+ parts: { root: '.statcard', value: '.statcard-value' },
99
+ properties: [
100
+ {
101
+ paints: {
102
+ root: {
103
+ backgroundColor: '--statcard-surface',
104
+ borderTopColor: '--statcard-border',
105
+ borderTopWidth: '--statcard-border-width',
106
+ borderRadius: '--statcard-radius',
107
+ paddingTop: '--statcard-padding',
108
+ },
109
+ value: { color: '--statcard-value-text', fontSize: '--statcard-value-font-size' },
110
+ },
111
+ },
112
+ ],
113
+ states: { applicable: false, reason: 'a stat card renders one state' },
114
+ persistence: {
115
+ cases: [
116
+ { shape: 'token', variable: '--statcard-radius', observe: { part: 'root', css: 'borderRadius' } },
117
+ ],
118
+ resetVariable: '--statcard-radius',
119
+ },
120
+ theme: {
121
+ theme: 'brand',
122
+ changed: ['--statcard-radius'],
123
+ unchanged: ['--statcard-padding'],
124
+ aliasedTo: { '--statcard-radius': '--radius-none' },
125
+ observe: { part: 'root', css: 'borderRadius', variable: '--statcard-radius' },
126
+ },
127
+ interaction: { applicable: false, reason: 'a stat card carries no interactive role' },
128
+ sketch: {
129
+ style: 'pencil',
130
+ parts: [{ part: 'root', fill: '--statcard-surface', stroke: '--statcard-border' }],
131
+ },
132
+ },
133
+ ];
134
+ ```
135
+
136
+ Each obligation and what it needs from the project:
137
+
138
+ - `properties`: part key, then CSS property, then the semantic property that
139
+ drives it. One entry per variant tab and state tab the editor renders;
140
+ omit `variant` and `state` when the editor renders one view. The paint maps
141
+ and `uncovered` together name every property in `default.json`. A property
142
+ no computed style shows verbatim, one consumed inside `calc()` or a gradient
143
+ function, goes in `uncovered` with its reason.
144
+ - `states`: one entry per state tab, with the class or attributes the preview
145
+ forces. `applicable: false` when the editor renders no state strip.
146
+ - `persistence`: an edit the run makes through the editor, saves, reloads, and
147
+ reads back. `resetVariable` is a property one of the cases moves.
148
+ - `theme`: a theme in the project's data tree whose block for the component
149
+ reassigns a property. Save one with the editor before the first run.
150
+ `changed` names the properties the theme moves, `unchanged` the ones it
151
+ leaves, and `aliasedTo` the design token each reassigned property resolves
152
+ to under it.
153
+ - `interaction`: the interactive part's role and its cases. `applicable:
154
+ false` for a component with no interactive role.
155
+ - `sketch`: a sketch style id and the fill and stroke each reserved-class part
156
+ resolves to under it. `applicable: false` only for a component
157
+ `references/sketch-mode.md` exempts.
158
+
63
159
  ## Running vitest yourself
64
160
 
65
161
  `check-component --tests` covers the shipped path. Run vitest directly only
@@ -17,7 +17,7 @@ Assemble the page from the shipped components at their defaults and the theme's
17
17
  6. Write the page CSS in design tokens.
18
18
  7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.
19
19
  8. Add the route, with a lazy import and the source path.
20
- 9. Run **live-tokens-check-compliance**, then check the rendered page.
20
+ 9. Run **live-tokens-check-compliance** and `npx live-tokens check-page <file> --tests --strict`, then check the rendered page.
21
21
  10. Reply with the sections and the layout each took, the components placed, the route, and the compliance result.
22
22
 
23
23
  ## Layout
@@ -34,7 +34,7 @@ Decide the sections before the columns. Read the page top to bottom and name eac
34
34
  | Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |
35
35
  | Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |
36
36
 
37
- The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align. Below the scaffold's 768px breakpoint, a section's columns stack in reading order.
37
+ The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.
38
38
 
39
39
  ### Grid
40
40
 
@@ -49,6 +49,8 @@ To place a section's children at page-column positions:
49
49
 
50
50
  A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.
51
51
 
52
+ No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.
53
+
52
54
  ### Separation
53
55
 
54
56
  The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.
@@ -163,7 +165,7 @@ const pages = {
163
165
 
164
166
  ## Verify
165
167
 
166
- Run **live-tokens-check-compliance**. Its report carries both checkers' findings by rule, and **live-tokens-fix-findings** takes the fix list. Repeat until the page is clean.
168
+ Run **live-tokens-check-compliance**, then `npx live-tokens check-page <file> --tests --strict`. The Playwright suite runs against the page's own route and proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. Each finding carries a rule id and a line; `--off=<rule>` silences a rule for one run. The two reports carry every finding by rule, and **live-tokens-fix-findings** takes the fix list. Repeat until the page is clean.
167
169
 
168
170
  The checkers cannot see a layout. Open the page at the width it is built for and check each line below.
169
171
 
@@ -172,7 +174,6 @@ The checkers cannot see a layout. Open the page at the width it is built for and
172
174
  - No label is larger than the page's body copy.
173
175
  - A line of copy runs 45 to 90 characters.
174
176
  - The containers in a section align at the bottom.
175
- - Every control stays inside its wrapper. A `width: 100%` field takes `box-sizing: border-box`.
176
177
  - The actions sit where the eye goes last, with the one primary at the end.
177
178
  - Every row of actions holds an action that leaves without committing.
178
179
  - An action that destroys saved work confirms in a `Dialog`.
@@ -80,6 +80,13 @@ When `package.json` has no `check:design` script, add `"check:design": "live-tok
80
80
  | `control-size` | Delete the `size` prop. The shipped default is the page's size. When that default is wrong for the project, retune the component in `/live-tokens/components`. |
81
81
  | `multiple-primary` | Keep the action that completes the main task `primary`. A Button with no `variant` counts as `primary`. Use `secondary` for supporting or related actions and `outline` for unrelated or informational actions. |
82
82
  | `danger-without-dialog` | Open a `Dialog` from the danger Button or IconButton and run the action from the Dialog's confirm. The rule fires once per page, when the page imports no Dialog. For other actions, assign emphasis by the action's relationship to the main task. |
83
+ | `native-control` | Replace the native element with the shipped component the message names: Button or IconButton, Input, MenuSelect. |
84
+ | `property-override` | Delete the declaration from the page. Retune the component's token for the whole project at `/live-tokens/components`. |
85
+ | `page-component-paint` | The finding names the page file and the line of the instance whose part painted a value its semantic property never resolves to. Remove the global rule reaching past the component, from `site.css` or the page's own CSS, and retune the component's token for the whole project at `/live-tokens/components`. |
86
+ | `page-text-style` | The finding names the page file and the line of the text element and the nearest bundle it missed. Set the container's text style directly on the text element, one shipped bundle from `npx live-tokens tokens --scale heading` (or `body`, `editorial`, `code`), rather than leaving it to inherit from an ancestor typed for a different role. |
87
+ | `page-contrast` | The finding names the page file and the line of the text element, the surface ancestor, and both computed colors. Pick the text token the surface pairs with, from the Color by role table above. |
88
+ | `page-grid` | The finding names the page file and the line of the section whose edge sits off a column line. Move the edge to the line: place it by page-column numbers per the Grid section of **live-tokens-create-page**, and give a centered section symmetric insets rather than a margin, a width, or a transform. |
89
+ | `page-overflow` | The finding names the page file and the line of the element or the instance that overflows. Give the control its shipped width, remove a fixed width that does not fit its column at the viewport, and set `overflow-x: auto` only on an element deliberately meant to scroll, such as a code block. When the overflow is the page grid itself at a phone width, collapse it: `grid-template-columns: 1fr`, `column-gap: 0`, per the Grid section of **live-tokens-create-page**. |
83
90
  | `hardcoded-columns` | `repeat(var(--columns-count), 1fr)` for the page grid. `calc(var(--columns-count) - 2)` for a sub-grid spanning fewer columns. |
84
91
  | `site-css-in-main` | Delete the import from `main.ts`. Add it to each page's `<script>`. Page CSS then stays off the editor routes. |
85
92
  | `missing-source` | Add `source: 'src/...'` to the route entry. |
package/CHANGELOG.md CHANGED
@@ -1,5 +1,95 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.78.0 — A page proves compliance as rendered
4
+
5
+ ### Added
6
+
7
+ - **A page proves compliance as rendered, not just in source.** `check-page
8
+ --tests` opens each page's own route in a Playwright browser, against the
9
+ consumer's own app, and reports five rule ids no static read of the source
10
+ could: `page-component-paint` (every shipped component instance paints its
11
+ contracted parts from its semantic properties, never from a page-wide or
12
+ `site.css` rule that reaches past them), `page-text-style` (every run of
13
+ text sits in one shipped text style, never inherited from a container typed
14
+ for a different role), `page-contrast` (every text and surface pair the
15
+ page composes meets AA), `page-grid` (every section's edges land on the
16
+ page's own column lines), and `page-overflow` (nothing overflows its
17
+ container, at either viewport). Coverage is reported by page, rule, and
18
+ viewport; a rule that observed nothing on a page reports `inapplicable`
19
+ with a reason, never a silent pass. `live-tokens.testing.ts` gains two
20
+ settings: `pageRoutes`, for a route the app's own route table cannot
21
+ express, and `pageViewports`, to replace the two sizes every rule runs at.
22
+ - **Two static rules close source-level gaps `check-page` left open.**
23
+ `native-control` flags a raw `<button>`, `<input>`, `<select>`, or
24
+ `<textarea>` where a shipped component belongs. `property-override` flags a
25
+ page that declares or sets a component token's name directly, in a style
26
+ block, an inline `style`, a `style:` directive, or `setProperty`, rather
27
+ than retuning the component for the whole project at
28
+ `/live-tokens/components`.
29
+ - **The consumer acceptance gate now covers a page.** `check:smoke-page-tests`
30
+ packs the built package into a fresh project outside the repository,
31
+ scaffolds it with `create`, and proves the documented command against the
32
+ template's own clean page and against the same page under a deliberate
33
+ `site.css` override: the clean page passes, the override fails
34
+ `page-component-paint` at the page file, and source-tree hashes prove the
35
+ isolated run never touched the project's own data. Wired into
36
+ `prepublishOnly` beside the existing component gate. Both gates now carry a
37
+ twenty-minute deadline of their own, naming the step that did not finish.
38
+
39
+ ### Changed
40
+
41
+ - **The `create` template's `test:design` runs both suites:**
42
+ `live-tokens check-component --tests && live-tokens check-page --tests`.
43
+ - **The live-tokens-create-page skill's Verify step runs the rendered check.**
44
+ After live-tokens-check-compliance, it runs `check-page --tests --strict`
45
+ on the page it built. The manual line "every control stays inside its
46
+ wrapper" moves to the automated `page-overflow` rule; every other
47
+ editorial line stays.
48
+ - **The `create` template's own Home page collapses to one column below
49
+ 768px.** Its `.stub` held a fixed twelve-column span with no phone
50
+ breakpoint, so `page-overflow` failed at 390x844, the defect
51
+ `check:smoke-page-tests` now catches on any page written to that shape.
52
+ The template ships no exclusion mechanism, so the fix is the page's own
53
+ `@media (max-width: 767px)` rule, the same shape every page-defects
54
+ fixture already takes.
55
+
56
+ ## 0.77.0 — A consumer component runs the same suites
57
+
58
+ ### Added
59
+
60
+ - **A consumer component's contract reaches the Playwright suites.**
61
+ `live-tokens.testing.ts` has carried a `contractsModule` setting since
62
+ 0.75.0, naming a module that exports the `ComponentContract[]` a project
63
+ adds to the shipped list, and nothing documented it. A consumer following
64
+ live-tokens-create-component reached the Verification step with the eight
65
+ component suites reported as incomplete and the skill giving no way to
66
+ complete them. The skill now names the contract as a deliverable beside the
67
+ registration, and `references/contract-tests.md` shows the setting, a
68
+ complete contract for the skill's own example, and what each obligation
69
+ needs from the project. `src/testing/contracts/contracts.test.ts` covers the
70
+ loader.
71
+ - **`contract-missing` is its own finding.** A run narrowed to a component
72
+ with no contract used to throw from the Playwright config, so the runner
73
+ saw no report and surfaced a `tests-setup` finding holding a tail of
74
+ stderr. The editor suite now emits one failing test tagged
75
+ `contract-missing`, the JSON report carries it, and the runner maps it like
76
+ any other violation: one finding at `live-tokens.testing.ts` that names the
77
+ setting and lists the declared ids, with the `coverage` fix slug. The render
78
+ and alias suites still run for that component, and the failed rule explains
79
+ its seven unrun obligations, so reconciliation adds nothing on top.
80
+
81
+ ### Changed
82
+
83
+ - **The `create` template installs the test tooling.** `@playwright/test`,
84
+ `vitest`, and `happy-dom` are devDependencies of a scaffolded project, and
85
+ the scaffold ships `live-tokens.testing.ts` naming `src/registerComponents.ts`
86
+ and `tests/contracts.ts`, both present and empty, with `main.ts` importing
87
+ the registration module before boot. `npm run test:design` therefore works
88
+ once `npx playwright install chromium` has run, and the create-component
89
+ skill lands its Registration and contract in files that already exist. The
90
+ three stay optional peers of the package. The create smoke test asserts the
91
+ tooling resolves in a scaffold from the packed tarball.
92
+
3
93
  ## 0.76.0 — The skills directory matches the release
4
94
 
5
95
  ### Changed (breaking)
@@ -55,6 +55,7 @@ export const COMPONENT_RULES = {
55
55
  'contract-interaction': 'error',
56
56
  'contract-listed': 'error',
57
57
  'contract-sketch': 'error',
58
+ 'contract-missing': 'error',
58
59
  'tests-not-installed': 'error',
59
60
  'tests-setup': 'error',
60
61
  'tests-incomplete': 'error',
@@ -94,6 +95,7 @@ export const COMPONENT_RULE_FIX = {
94
95
  'contract-registry': 'registration',
95
96
  'contract-listed': 'registration',
96
97
  'contract-sketch': 'sketch',
98
+ 'contract-missing': 'coverage',
97
99
  'tests-not-installed': 'tooling',
98
100
  'tests-setup': 'tooling',
99
101
  'tests-incomplete': 'coverage',
@@ -33,10 +33,24 @@ export const PAGE_RULES = {
33
33
  'control-size': 'warn',
34
34
  'multiple-primary': 'warn',
35
35
  'danger-without-dialog': 'warn',
36
+ 'native-control': 'warn',
37
+ 'property-override': 'warn',
38
+ // `--tests` (bin/contractRunner.mjs's `runPageTests`). Fixed by design
39
+ // decision 9, same reasoning as `check-component`'s own `contract-*` rules:
40
+ // every one is an error, including the setup rules, which `--tests` treats
41
+ // as never-silenceable (see cli.mjs).
42
+ 'page-component-paint': 'error',
43
+ 'page-text-style': 'error',
44
+ 'page-contrast': 'error',
45
+ 'page-grid': 'error',
46
+ 'page-overflow': 'error',
47
+ 'tests-not-installed': 'error',
48
+ 'tests-setup': 'error',
49
+ 'tests-incomplete': 'error',
36
50
  };
37
51
 
38
52
  // Directories that hold the system, not pages built on it.
39
- const NOT_PAGES = ['src/system', 'src/editor', 'src/lib', 'src/live-tokens'];
53
+ export const NOT_PAGES = ['src/system', 'src/editor', 'src/lib', 'src/live-tokens'];
40
54
 
41
55
  export const COMPONENT_IMPORT =
42
56
  /(?:@motion-proto\/live-tokens\/components|[./][^'"]*\/system\/components)\/([A-Za-z0-9]+)\.svelte$/;
@@ -194,6 +208,61 @@ function tagAttributes(code, start) {
194
208
  return { attrs, end: tagEnd };
195
209
  }
196
210
 
211
+ // The shipped component that owns each native control's paint.
212
+ const NATIVE_CONTROLS = {
213
+ button: 'Button or IconButton',
214
+ input: 'Input',
215
+ select: 'MenuSelect',
216
+ textarea: 'Input',
217
+ };
218
+
219
+ /**
220
+ * `{@html ...}` blanked, brace-balanced, so a tag textually present inside a
221
+ * raw HTML string is not mistaken for markup the page authored.
222
+ */
223
+ function blankHtmlExpressions(code) {
224
+ let out = code;
225
+ const re = /\{@html\b/g;
226
+ let m;
227
+ while ((m = re.exec(out)) !== null) {
228
+ const start = m.index;
229
+ let depth = 0;
230
+ let i = start;
231
+ for (; i < out.length; i++) {
232
+ if (out[i] === '{') depth++;
233
+ else if (out[i] === '}' && --depth === 0) {
234
+ i++;
235
+ break;
236
+ }
237
+ }
238
+ out = out.slice(0, start) + ' '.repeat(i - start) + out.slice(i);
239
+ re.lastIndex = start;
240
+ }
241
+ return out;
242
+ }
243
+
244
+ /** A raw `<button>`, `<input>`, `<select>`, or `<textarea>` where a shipped
245
+ component belongs. `<input type="hidden">` paints nothing and is exempt.
246
+ A tag inside `<script>` is markup the page assembles into a string, not
247
+ markup it renders, so the script block is blanked the same way
248
+ `codeRegion` blanks `<style>`, keeping offsets aligned with `code`. */
249
+ function checkNativeControls(code, add) {
250
+ const scan = blankHtmlExpressions(code).replace(/<script[^>]*>[\s\S]*?<\/script>/g, (m) => ' '.repeat(m.length));
251
+ for (const tag of Object.keys(NATIVE_CONTROLS)) {
252
+ for (const m of scan.matchAll(new RegExp(`<${tag}(?=[\\s/>])`, 'g'))) {
253
+ const parsed = tagAttributes(scan, m.index);
254
+ if (tag === 'input' && parsed?.attrs.some((a) => a.name.toLowerCase() === 'type' && a.value?.toLowerCase() === 'hidden')) {
255
+ continue;
256
+ }
257
+ add(
258
+ 'native-control',
259
+ m.index,
260
+ `<${tag}> is a native control; use ${NATIVE_CONTROLS[tag]} so it paints from the theme.`,
261
+ );
262
+ }
263
+ }
264
+ }
265
+
197
266
  /**
198
267
  * Props a page passes that the component does not declare, values outside a
199
268
  * prop's union, and a shipped component the page sizes itself.
@@ -274,6 +343,22 @@ function checkDestructiveActions(code, imports, add) {
274
343
  }
275
344
  }
276
345
 
346
+ // Which component's :global(:root) block declared each component token,
347
+ // cached per vocabulary since checkFile runs once per page.
348
+ const tokenOwnersCache = new WeakMap();
349
+ function componentTokenOwners(vocab) {
350
+ let owners = tokenOwnersCache.get(vocab);
351
+ if (owners) return owners;
352
+ owners = new Map();
353
+ for (const comp of vocab.components.values()) {
354
+ for (const name of comp.tokens.keys()) {
355
+ if (!owners.has(name)) owners.set(name, comp.name);
356
+ }
357
+ }
358
+ tokenOwnersCache.set(vocab, owners);
359
+ return owners;
360
+ }
361
+
277
362
  /** The object literal enclosing `index`, found by balancing braces outward. */
278
363
  function enclosingObject(text, index) {
279
364
  let depth = 0;
@@ -335,6 +420,7 @@ function checkFile(file, text, vocab, root) {
335
420
  checkComponentUsage(code, imports, add);
336
421
  checkPrimaryActions(code, imports, add);
337
422
  checkDestructiveActions(code, imports, add);
423
+ checkNativeControls(code, add);
338
424
 
339
425
  for (const m of code.matchAll(/['"](\/live-tokens[^'"]*)['"]\s*:/g)) {
340
426
  add('reserved-route', m.index, `route '${m[1]}' is inside the reserved /live-tokens/* namespace`);
@@ -360,16 +446,52 @@ function checkFile(file, text, vocab, root) {
360
446
 
361
447
  // A page may also mint a custom property outside its <style> block — a
362
448
  // `style:--x={...}` directive or an el.style.setProperty call — and those are
363
- // just as declared as one written in CSS.
364
- const declaredHere = new Set();
449
+ // just as declared as one written in CSS. Each name keeps the earliest site
450
+ // it was declared at, so property-override reports one finding per name.
451
+ const declaredSites = new Map();
452
+ const declareAt = (name, index) => {
453
+ if (!declaredSites.has(name) || index < declaredSites.get(name)) declaredSites.set(name, index);
454
+ };
365
455
  const regions = styleRegions(text, file);
366
- for (const region of regions) {
367
- for (const m of neutralise(region.text).matchAll(/(?:^|[;{])\s*(--[a-z0-9-]+)\s*:/gim)) {
368
- declaredHere.add(m[1]);
456
+ const styleBlockDeclarations = (regionList, at) => {
457
+ for (const region of regionList) {
458
+ for (const m of neutralise(region.text).matchAll(/(?:^|[;{])\s*(--[a-z0-9-]+)\s*:/gim)) {
459
+ at(m[1], region.offset + m.index);
460
+ }
369
461
  }
370
- }
462
+ };
463
+ styleBlockDeclarations(regions, declareAt);
371
464
  for (const m of text.matchAll(/(?:style:|setProperty\(\s*['"`]|['"`])(--[a-z0-9-]+)/g)) {
372
- declaredHere.add(m[1]);
465
+ declareAt(m[1], m.index);
466
+ }
467
+
468
+ // property-override needs a narrower set than declaredSites: the bare-quote
469
+ // alternative above exists only to suppress unknown-token on a name any
470
+ // quoted string mentions, so it also matches a read like
471
+ // getPropertyValue("--x"). A real declaration is a style-block rule, an
472
+ // inline `style="--x: ..."` attribute, a `style:--x=` directive, or a
473
+ // setProperty('--x', ...) call.
474
+ const overrideSites = new Map();
475
+ const overrideAt = (name, index) => {
476
+ if (!overrideSites.has(name) || index < overrideSites.get(name)) overrideSites.set(name, index);
477
+ };
478
+ styleBlockDeclarations(regions, overrideAt);
479
+ if (code !== null) styleBlockDeclarations(inlineStyleRegions(code), overrideAt);
480
+ for (const m of text.matchAll(/(?:style:|setProperty\(\s*['"`])(--[a-z0-9-]+)/g)) {
481
+ overrideAt(m[1], m.index);
482
+ }
483
+
484
+ // A name the vocabulary already ties to a component is that component's
485
+ // token, so declaring it here is one instance overriding the whole
486
+ // project's retuning surface at /live-tokens/components.
487
+ for (const [name, index] of overrideSites) {
488
+ if (!vocab.componentTokens.has(name)) continue;
489
+ const owner = componentTokenOwners(vocab).get(name) ?? 'a shipped component';
490
+ add(
491
+ 'property-override',
492
+ index,
493
+ `${name} overrides ${owner}'s token here instead of the whole project; retune it at /live-tokens/components.`,
494
+ );
373
495
  }
374
496
 
375
497
  for (const region of [...regions, ...(code === null ? [] : inlineStyleRegions(code))]) {
@@ -378,7 +500,7 @@ function checkFile(file, text, vocab, root) {
378
500
 
379
501
  for (const m of css.matchAll(/var\(\s*(--[a-z0-9-]+)/g)) {
380
502
  const name = m[1];
381
- if (declaredHere.has(name) || vocab.knows(name)) continue;
503
+ if (declaredSites.has(name) || vocab.knows(name)) continue;
382
504
  add(
383
505
  'unknown-token',
384
506
  at(m.index),
package/bin/cli.mjs CHANGED
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'node:url';
20
20
  import process from 'node:process';
21
21
  import { COMPONENT_RULES, COMPONENT_RULE_FIX, checkComponent, discoverComponents, formatReport } from './check-component.mjs';
22
22
  import { PAGE_RULES, checkPages, discoverPages } from './check-page.mjs';
23
+ import { resolvePageTestTargets } from './lib/pageRoutes.mjs';
23
24
  import { describeComponents, describeTokens, formatComponents, formatTokens } from './lib/catalogue.mjs';
24
25
  import { buildReport, formatReport as formatProjectReport } from './lib/report.mjs';
25
26
  import { loadVocabulary } from './lib/tokenVocabulary.mjs';
@@ -74,10 +75,20 @@ Commands:
74
75
  @playwright/test, vitest, and happy-dom; a
75
76
  missing one is a tests-not-installed finding
76
77
  naming the install command
77
- check-page [paths...] Validate pages against the live-tokens-create-page
78
+ check-page [paths...] [--tests]
79
+ Validate pages against the live-tokens-create-page
78
80
  contract: catalogue components only, and every CSS
79
81
  value a design token. Checks every page under src/
80
- when given no paths.
82
+ when given no paths. --tests also opens each
83
+ page's own route in the consumer's own app and
84
+ proves, per shipped rule id, that the cascade
85
+ painted every component from its semantic
86
+ properties, every run of text sits in one shipped
87
+ text style, every text/surface pair meets AA,
88
+ sections sit on the page grid, and nothing
89
+ overflows. Needs @playwright/test, vitest, and
90
+ happy-dom; a missing one is a tests-not-installed
91
+ finding naming the install command
81
92
 
82
93
  check-component and check-page also accept:
83
94
  --json Machine-readable findings, for a skill to iterate
@@ -270,7 +281,18 @@ if (command === 'check-page') {
270
281
  const opts = parseCheckFlags(rest);
271
282
  const targets = opts.rest.length > 0 ? opts.rest : discoverPages(process.cwd());
272
283
  const { findings, checked } = checkPages(targets, { root: process.cwd() });
273
- reportChecks('check-page', findings, checked, PAGE_RULES, opts);
284
+ if (!opts.tests) {
285
+ reportChecks('check-page', findings, checked, PAGE_RULES, opts);
286
+ }
287
+ const { hasHardFailure, runPageTests } = await import('./contractRunner.mjs');
288
+ const pageTargets = resolvePageTestTargets(opts.rest, process.cwd());
289
+ const testOutcome = await runPageTests(pageTargets, { root: process.cwd() });
290
+ const label = 'check-page --tests';
291
+ const allFindings = [...findings, ...testOutcome.findings];
292
+ reportChecks(label, allFindings, Math.max(checked, pageTargets.length), PAGE_RULES, opts, {
293
+ coverage: testOutcome.coverage,
294
+ hardFailure: hasHardFailure(testOutcome.findings),
295
+ });
274
296
  }
275
297
 
276
298
  if (command === 'set-colors') {