@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.
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +2 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +3 -3
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +99 -3
- package/.claude/skills/live-tokens-create-page/SKILL.md +5 -4
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +7 -0
- package/CHANGELOG.md +90 -0
- package/bin/check-component.mjs +2 -0
- package/bin/check-page.mjs +131 -9
- package/bin/cli.mjs +25 -3
- package/bin/contractRunner.mjs +349 -30
- package/bin/lib/pageRoutes.mjs +309 -0
- package/package.json +5 -3
- package/src/editor/overlay/ColumnsOverlay.svelte +1 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +1 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +1 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +5 -5
- package/src/editor/skill-atlas/trees/check-compliance.ts +18 -18
- package/src/editor/skill-atlas/trees/create-component.ts +8 -3
- package/src/editor/skill-atlas/trees/create-page.ts +26 -26
- package/src/editor/skill-atlas/trees/fix-findings.ts +46 -11
- package/src/testing-js/{chunk-LXR3MN6N.js → chunk-4JQX6WWL.js} +1 -1
- package/src/testing-js/chunk-4JQX6WWL.js.map +1 -0
- package/src/testing-js/{chunk-AO7EZHYV.js → chunk-GNIUPIU2.js} +3 -3
- package/src/testing-js/{chunk-ZMSX6CXR.js → chunk-NB3NZRBM.js} +13 -2
- package/src/testing-js/chunk-NB3NZRBM.js.map +1 -0
- package/src/testing-js/{chunk-L73N4NSO.js → chunk-WIZ6W7UT.js} +2 -2
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-editor.contract.js +21 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +4 -4
- package/src/testing-js/index.d.ts +9 -3
- package/src/testing-js/index.js +20 -12
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +829 -0
- package/src/testing-js/page-compliance.contract.js.map +1 -0
- package/src/testing-js/{vitest-BE6uGF31.d.ts → vitest-C-wNWcoA.d.ts} +21 -1
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +1 -1
- package/template/README.md +17 -5
- package/template/live-tokens.testing.ts +6 -0
- package/template/package.json +5 -2
- package/template/src/main.ts +1 -0
- package/template/src/pages/Home.svelte +13 -0
- package/template/src/registerComponents.ts +4 -0
- package/template/tests/contracts.ts +3 -0
- package/template/tsconfig.json +1 -1
- package/src/testing-js/chunk-LXR3MN6N.js.map +0 -1
- package/src/testing-js/chunk-ZMSX6CXR.js.map +0 -1
- /package/src/testing-js/{chunk-AO7EZHYV.js.map → chunk-GNIUPIU2.js.map} +0 -0
- /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
|
|
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
|
|
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
|
-
|
|
21
|
-
`
|
|
22
|
-
|
|
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
|
|
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.
|
|
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
|
|
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)
|
package/bin/check-component.mjs
CHANGED
|
@@ -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',
|
package/bin/check-page.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
367
|
-
for (const
|
|
368
|
-
|
|
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
|
-
|
|
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 (
|
|
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...]
|
|
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
|
-
|
|
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') {
|