@motion-proto/live-tokens 0.97.0 → 0.98.2
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 +5 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
- package/CHANGELOG.md +272 -0
- package/README.md +2 -2
- package/bin/check-component.mjs +8 -0
- package/bin/check-page.mjs +6 -0
- package/bin/cli.mjs +36 -6
- package/bin/contractRunner.mjs +142 -17
- package/bin/lib/findings.mjs +20 -4
- package/bin/rules/a11y.mjs +53 -0
- package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
- package/dist-plugin/index.cjs +148 -3
- package/dist-plugin/index.js +250 -105
- package/dist-plugin/setColors/index.js +1 -1
- package/package.json +47 -2
- package/src/demo/LabeledSelect.svelte +1 -1
- package/src/editor/bootstrap.ts +3 -1
- package/src/editor/component-editor/BadgeEditor.svelte +13 -0
- package/src/editor/component-editor/ButtonEditor.svelte +33 -2
- package/src/editor/component-editor/CalloutEditor.svelte +13 -0
- package/src/editor/component-editor/CardEditor.svelte +12 -2
- package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
- package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
- package/src/editor/component-editor/DialogEditor.svelte +15 -0
- package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
- package/src/editor/component-editor/ImageEditor.svelte +20 -9
- package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
- package/src/editor/component-editor/InputEditor.svelte +23 -0
- package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
- package/src/editor/component-editor/NotificationEditor.svelte +15 -0
- package/src/editor/component-editor/PanelEditor.svelte +1 -1
- package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
- package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
- package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
- package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
- package/src/editor/component-editor/SliderEditor.svelte +33 -0
- package/src/editor/component-editor/TabBarEditor.svelte +23 -2
- package/src/editor/component-editor/TableEditor.svelte +14 -0
- package/src/editor/component-editor/ToggleEditor.svelte +17 -0
- package/src/editor/component-editor/TooltipEditor.svelte +11 -2
- package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
- package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
- package/src/editor/component-editor/registration.ts +2 -1
- package/src/editor/component-editor/registry.ts +69 -27
- package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -3
- package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
- package/src/editor/core/a11y/a11yReport.ts +101 -0
- package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
- package/src/editor/core/a11y/contrastFailures.ts +22 -0
- package/src/editor/core/components/contrastPairRegistry.ts +18 -0
- package/src/editor/core/palettes/contrast.ts +44 -0
- package/src/editor/core/palettes/contrastPairs.ts +79 -0
- package/src/editor/core/palettes/resolveColor.ts +77 -0
- package/src/editor/core/store/editorStore.ts +4 -0
- package/src/editor/core/store/editorViewStore.ts +2 -2
- package/src/editor/core/store/unsavedEdits.ts +10 -0
- package/src/editor/core/themes/liveStateStream.ts +7 -1
- package/src/editor/core/themes/parsers/gradient.ts +5 -2
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/accessibility.md +362 -0
- package/src/editor/docs/content/editing-tokens.md +28 -1
- package/src/editor/docs/content.generated.ts +2 -1
- package/src/editor/pages/ComponentEditorPage.svelte +10 -1
- package/src/editor/pages/EditorShell.svelte +96 -10
- package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
- package/src/editor/skill-atlas/trees/create-component.ts +12 -12
- package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
- package/src/editor/ui/GradientEditor.svelte +9 -1
- package/src/editor/ui/ThemePanel.svelte +4 -4
- package/src/editor/ui/UIContrastReadout.svelte +186 -0
- package/src/editor/ui/UIInfoPopover.svelte +16 -2
- package/src/editor/ui/UIPaletteSelector.svelte +9 -0
- package/src/editor/ui/UITokenSelector.svelte +26 -2
- package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
- package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
- package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
- package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
- package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
- package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
- package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
- package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
- package/src/editor/ui/a11y/a11yView.ts +218 -0
- package/src/editor/ui/contrastReadout.ts +45 -0
- package/src/live-tokens/data/themes/autumn.json +20 -20
- package/src/live-tokens/data/themes/halloween.json +20 -20
- package/src/live-tokens/data/themes/midnight-study.json +20 -20
- package/src/live-tokens/data/themes/ocean.json +20 -20
- package/src/live-tokens/data/themes/royal-velvet.json +20 -20
- package/src/live-tokens/data/themes/sketchy.json +20 -20
- package/src/live-tokens/data/themes/spring-meadow.json +20 -20
- package/src/live-tokens/data/themes/sunset.json +20 -20
- package/src/system/components/Badge.svelte +3 -3
- package/src/system/components/Button.svelte +9 -3
- package/src/system/components/CodeSnippet.svelte +8 -1
- package/src/system/components/CornerBadge.svelte +3 -3
- package/src/system/components/Dialog.svelte +43 -14
- package/src/system/components/IconButton.svelte +1 -1
- package/src/system/components/Image.svelte +7 -0
- package/src/system/components/ImageLightbox.svelte +18 -25
- package/src/system/components/Input.svelte +8 -1
- package/src/system/components/MenuSelect.svelte +30 -2
- package/src/system/components/Notification.svelte +1 -1
- package/src/system/components/ProgressBar.svelte +6 -0
- package/src/system/components/RadioButton.svelte +36 -3
- package/src/system/components/SegmentedControl.svelte +36 -2
- package/src/system/components/SlidePager.svelte +2 -2
- package/src/system/components/Slider.svelte +13 -9
- package/src/system/components/TabBar.svelte +33 -3
- package/src/system/components/Toggle.svelte +2 -2
- package/src/system/components/VideoFrame.svelte +1 -1
- package/src/system/components/VideoLightbox.svelte +2 -2
- package/src/system/internal/focusTrap.ts +35 -0
- package/src/testing-js/__aria__/badge.yml +40 -0
- package/src/testing-js/__aria__/button.yml +88 -0
- package/src/testing-js/__aria__/callout.yml +12 -0
- package/src/testing-js/__aria__/card.yml +8 -0
- package/src/testing-js/__aria__/codesnippet.yml +21 -0
- package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
- package/src/testing-js/__aria__/cornerbadge.yml +24 -0
- package/src/testing-js/__aria__/dialog.yml +42 -0
- package/src/testing-js/__aria__/iconbutton.yml +57 -0
- package/src/testing-js/__aria__/image.yml +10 -0
- package/src/testing-js/__aria__/imagelightbox.yml +50 -0
- package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
- package/src/testing-js/__aria__/input.yml +61 -0
- package/src/testing-js/__aria__/menuselect.yml +30 -0
- package/src/testing-js/__aria__/notification.yml +30 -0
- package/src/testing-js/__aria__/panel.yml +2 -0
- package/src/testing-js/__aria__/progressbar.yml +2 -0
- package/src/testing-js/__aria__/radiobutton.yml +20 -0
- package/src/testing-js/__aria__/sectiondivider.yml +9 -0
- package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
- package/src/testing-js/__aria__/sidenavigation.yml +304 -0
- package/src/testing-js/__aria__/slidepager.yml +72 -0
- package/src/testing-js/__aria__/slider.yml +28 -0
- package/src/testing-js/__aria__/tabbar.yml +35 -0
- package/src/testing-js/__aria__/table.yml +144 -0
- package/src/testing-js/__aria__/toggle.yml +12 -0
- package/src/testing-js/__aria__/tooltip.yml +2 -0
- package/src/testing-js/__aria__/videoframe.yml +6 -0
- package/src/testing-js/__aria__/videolightbox.yml +12 -0
- package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
- package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
- package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
- package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
- package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
- package/src/testing-js/chunk-EWSGE27V.js +168 -0
- package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
- package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
- package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
- package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
- package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
- package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
- package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
- package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
- package/src/testing-js/component-a11y.contract.js +283 -0
- package/src/testing-js/component-a11y.contract.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-behavior.contract.js +2 -2
- package/src/testing-js/component-editor.contract.js +6 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +10 -6
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +44 -16
- package/src/testing-js/index.js +9 -9
- package/src/testing-js/page-compliance.contract.js +132 -13
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/testing-js/vitest.js +3 -3
- package/template/package.json +9 -0
- package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
- package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
- package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
- package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
- /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
- /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { run } from 'svelte/legacy';
|
|
3
3
|
|
|
4
|
-
import { onMount } from 'svelte';
|
|
4
|
+
import { onMount, tick } from 'svelte';
|
|
5
5
|
import { MediaQuery } from 'svelte/reactivity';
|
|
6
6
|
import VariablesTab from '../ui/VariablesTab.svelte';
|
|
7
7
|
import ColorsTab from '../ui/colors/ColorsTab.svelte';
|
|
8
8
|
import EditorViewSwitcher from '../ui/EditorViewSwitcher.svelte';
|
|
9
9
|
import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
|
|
10
10
|
import SketchTab from '../ui/sketch/SketchTab.svelte';
|
|
11
|
+
import AccessibilityTab from '../ui/a11y/AccessibilityTab.svelte';
|
|
12
|
+
import A11yRailMark from '../ui/a11y/A11yRailMark.svelte';
|
|
13
|
+
import {
|
|
14
|
+
A11Y_REVIEW_SECTION,
|
|
15
|
+
A11Y_RUN_SECTION,
|
|
16
|
+
a11ySectionId,
|
|
17
|
+
componentsWithIssues,
|
|
18
|
+
issuesLabel,
|
|
19
|
+
setA11yViewLink,
|
|
20
|
+
} from '../ui/a11y/a11yView';
|
|
11
21
|
import ThemePanel from '../ui/ThemePanel.svelte';
|
|
12
22
|
import { scrollSectionIntoView } from '../ui/scrollSection';
|
|
13
23
|
import { editorView, sidebarCondensed, selectedComponent, type EditorView } from '../core/store/editorViewStore';
|
|
14
|
-
import { componentDirty } from '../core/store/editorStore';
|
|
24
|
+
import { a11yIssues, componentDirty } from '../core/store/editorStore';
|
|
15
25
|
import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
|
|
16
26
|
import { listComponents } from '../core/components/componentConfigService';
|
|
17
27
|
|
|
@@ -43,6 +53,8 @@
|
|
|
43
53
|
{ id: 'sketch-preview', label: 'Preview', icon: 'fas fa-eye' }
|
|
44
54
|
];
|
|
45
55
|
|
|
56
|
+
type NavItem = { id: string; label: string; icon: string; count?: number };
|
|
57
|
+
|
|
46
58
|
let { onclose }: { onclose?: () => void } = $props();
|
|
47
59
|
|
|
48
60
|
const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
|
|
@@ -99,6 +111,13 @@
|
|
|
99
111
|
sectionJump = { id: sectionId };
|
|
100
112
|
}
|
|
101
113
|
|
|
114
|
+
setA11yViewLink(async (sectionId) => {
|
|
115
|
+
editorView.set('accessibility');
|
|
116
|
+
await tick();
|
|
117
|
+
document.querySelector<HTMLDetailsElement>(`#${sectionId} details`)?.setAttribute('open', '');
|
|
118
|
+
scrollToSection(sectionId);
|
|
119
|
+
});
|
|
120
|
+
|
|
102
121
|
function selectComponent(id: string) {
|
|
103
122
|
selectedComponent.set(id);
|
|
104
123
|
hideHint();
|
|
@@ -115,11 +134,26 @@
|
|
|
115
134
|
let menuOpen = $state(false);
|
|
116
135
|
let screenBarHeight = $state(0);
|
|
117
136
|
|
|
118
|
-
|
|
137
|
+
let a11yComponentNavItems: NavItem[] = $derived(
|
|
138
|
+
componentsWithIssues($a11yIssues).map(({ component, label, icon, count }) => ({
|
|
139
|
+
id: a11ySectionId(component), label, icon, count,
|
|
140
|
+
})),
|
|
141
|
+
);
|
|
142
|
+
let a11yMarked = $derived(new Set(componentsWithIssues($a11yIssues).map((row) => row.component)));
|
|
143
|
+
let a11yHasReview = $derived(Object.values($a11yIssues).some((issues) => issues.review.length > 0));
|
|
144
|
+
|
|
145
|
+
// The narrow layout shows one section at a time, so the run section and the
|
|
146
|
+
// check-by-eye line join the component sections the rail lists.
|
|
147
|
+
let sectionNav: Partial<Record<EditorView, NavItem[]>> = $derived({
|
|
119
148
|
tokens: tokenNavItems,
|
|
120
149
|
sketch: sketchNavItems,
|
|
121
150
|
colors: colorsNavItems,
|
|
122
|
-
|
|
151
|
+
accessibility: [
|
|
152
|
+
{ id: A11Y_RUN_SECTION, label: 'Test run', icon: 'fas fa-universal-access' },
|
|
153
|
+
...a11yComponentNavItems,
|
|
154
|
+
...(a11yHasReview ? [{ id: A11Y_REVIEW_SECTION, label: 'Check by eye', icon: 'fas fa-eye' }] : []),
|
|
155
|
+
],
|
|
156
|
+
});
|
|
123
157
|
|
|
124
158
|
function shownSection(view: EditorView): string | null {
|
|
125
159
|
const items = sectionNav[view];
|
|
@@ -127,8 +161,8 @@
|
|
|
127
161
|
return items.some((i) => i.id === selectedTokenSection) ? selectedTokenSection : items[0].id;
|
|
128
162
|
}
|
|
129
163
|
|
|
130
|
-
let menuItems = $derived(
|
|
131
|
-
$editorView === 'components' ? allComponentNavItems :
|
|
164
|
+
let menuItems: NavItem[] = $derived(
|
|
165
|
+
$editorView === 'components' ? allComponentNavItems : sectionNav[$editorView]!,
|
|
132
166
|
);
|
|
133
167
|
let activeItemId = $derived(
|
|
134
168
|
themeScreen ? null : $editorView === 'components' ? $selectedComponent : shownSection($editorView),
|
|
@@ -214,12 +248,17 @@
|
|
|
214
248
|
<button
|
|
215
249
|
type="button"
|
|
216
250
|
class="nav-item"
|
|
251
|
+
class:counted={item.count !== undefined}
|
|
217
252
|
class:active={activeItemId === item.id}
|
|
253
|
+
aria-label={item.count !== undefined ? `${item.label}, ${issuesLabel(item.count)}` : undefined}
|
|
218
254
|
class:dirty={$editorView === 'components' && $componentDirty[item.id]}
|
|
219
255
|
onclick={() => pickSection(item.id)}
|
|
220
256
|
>
|
|
221
257
|
<i class={item.icon}></i>
|
|
222
|
-
<span class="nav-label">{item.label}</span>
|
|
258
|
+
<span class="nav-label">{#if $editorView === 'components' && a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
|
|
259
|
+
{#if item.count !== undefined}
|
|
260
|
+
<span class="nav-count">{item.count}</span>
|
|
261
|
+
{/if}
|
|
223
262
|
{#if $editorView === 'components' && $componentDirty[item.id]}
|
|
224
263
|
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
225
264
|
{/if}
|
|
@@ -309,6 +348,28 @@
|
|
|
309
348
|
<ThemePanel />
|
|
310
349
|
</div>
|
|
311
350
|
{/if}
|
|
351
|
+
{:else if $editorView === 'accessibility'}
|
|
352
|
+
<div class="nav-items">
|
|
353
|
+
{#each a11yComponentNavItems as item (item.id)}
|
|
354
|
+
<button
|
|
355
|
+
class="nav-item counted"
|
|
356
|
+
class:active={selectedTokenSection === item.id}
|
|
357
|
+
aria-label={`${item.label}, ${issuesLabel(item.count ?? 0)}`}
|
|
358
|
+
onmouseenter={(e) => showHint(item.label, e.currentTarget)}
|
|
359
|
+
onmouseleave={hideHint}
|
|
360
|
+
onclick={() => scrollToSection(item.id)}
|
|
361
|
+
>
|
|
362
|
+
<i class={item.icon}></i>
|
|
363
|
+
<span class="nav-label">{item.label}</span>
|
|
364
|
+
<span class="nav-count">{item.count}</span>
|
|
365
|
+
</button>
|
|
366
|
+
{/each}
|
|
367
|
+
</div>
|
|
368
|
+
{#if !condensed}
|
|
369
|
+
<div class="sidebar-footer">
|
|
370
|
+
<ThemePanel />
|
|
371
|
+
</div>
|
|
372
|
+
{/if}
|
|
312
373
|
{:else}
|
|
313
374
|
<div class="nav-items">
|
|
314
375
|
{#each systemNavItems as item (item.id)}
|
|
@@ -321,10 +382,13 @@
|
|
|
321
382
|
onclick={() => selectComponent(item.id)}
|
|
322
383
|
>
|
|
323
384
|
<i class={item.icon}></i>
|
|
324
|
-
<span class="nav-label">{item.label}</span>
|
|
385
|
+
<span class="nav-label">{#if a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
|
|
325
386
|
{#if $componentDirty[item.id]}
|
|
326
387
|
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
327
388
|
{/if}
|
|
389
|
+
{#if condensed && a11yMarked.has(item.id)}
|
|
390
|
+
<A11yRailMark placement="corner" />
|
|
391
|
+
{/if}
|
|
328
392
|
</button>
|
|
329
393
|
{/each}
|
|
330
394
|
{#if customNavItems.length > 0}
|
|
@@ -341,10 +405,13 @@
|
|
|
341
405
|
onclick={() => selectComponent(item.id)}
|
|
342
406
|
>
|
|
343
407
|
<i class={item.icon}></i>
|
|
344
|
-
<span class="nav-label">{item.label}</span>
|
|
408
|
+
<span class="nav-label">{#if a11yMarked.has(item.id)}<A11yRailMark />{/if}{item.label}</span>
|
|
345
409
|
{#if $componentDirty[item.id]}
|
|
346
410
|
<span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
|
|
347
411
|
{/if}
|
|
412
|
+
{#if condensed && a11yMarked.has(item.id)}
|
|
413
|
+
<A11yRailMark placement="corner" />
|
|
414
|
+
{/if}
|
|
348
415
|
</button>
|
|
349
416
|
{/each}
|
|
350
417
|
{/if}
|
|
@@ -367,6 +434,8 @@
|
|
|
367
434
|
<ColorsTab />
|
|
368
435
|
{:else if $editorView === 'sketch'}
|
|
369
436
|
<SketchTab {sectionJump} />
|
|
437
|
+
{:else if $editorView === 'accessibility'}
|
|
438
|
+
<AccessibilityTab />
|
|
370
439
|
{:else}
|
|
371
440
|
<ComponentsTab selectedComponent={$selectedComponent} />
|
|
372
441
|
{/if}
|
|
@@ -499,6 +568,11 @@
|
|
|
499
568
|
min-width: 0;
|
|
500
569
|
}
|
|
501
570
|
|
|
571
|
+
/* A classic scrollbar would cover a third of the 48px rail. It still scrolls by wheel, trackpad and keyboard. */
|
|
572
|
+
.sidebar.condensed {
|
|
573
|
+
scrollbar-width: none;
|
|
574
|
+
}
|
|
575
|
+
|
|
502
576
|
.rail-toggle-row {
|
|
503
577
|
display: flex;
|
|
504
578
|
justify-content: flex-end;
|
|
@@ -607,10 +681,22 @@
|
|
|
607
681
|
transition: opacity 180ms ease;
|
|
608
682
|
}
|
|
609
683
|
|
|
610
|
-
.layout.condensed .nav-label
|
|
684
|
+
.layout.condensed .nav-label,
|
|
685
|
+
.layout.condensed .nav-count {
|
|
611
686
|
opacity: 0;
|
|
612
687
|
}
|
|
613
688
|
|
|
689
|
+
.nav-item.counted {
|
|
690
|
+
grid-template-columns: 48px 1fr auto;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.nav-count {
|
|
694
|
+
padding-right: var(--ui-space-12);
|
|
695
|
+
font-size: var(--ui-font-size-sm);
|
|
696
|
+
color: var(--ui-text-tertiary);
|
|
697
|
+
transition: opacity 180ms ease;
|
|
698
|
+
}
|
|
699
|
+
|
|
614
700
|
.nav-item:hover {
|
|
615
701
|
color: var(--ui-text-secondary);
|
|
616
702
|
background: var(--ui-hover);
|
|
@@ -5,11 +5,11 @@ export const SKILL_DOC = 'SKILL.md' as const;
|
|
|
5
5
|
|
|
6
6
|
export const skillDocs: Record<string, Record<string, string[]>> = {
|
|
7
7
|
"check-compliance": {
|
|
8
|
-
"SKILL.md": ["---","name: live-tokens-check-compliance","description: Check an existing @motion-proto/live-tokens project against the design system and fix it until check-page and check-component both exit 0. Checks for correct use of components, properties, and tokens. Runs both checkers, which bring tokens.css up to the installed package, apply every auto repair, and return each remaining finding with its own guidance, repair level, and details. Use when the user asks to check, audit, or review the project. Use when the user asks to fix the project or to make the build's design checks pass. Edits the files the checkers name. Changes tokens.css only through its migrations.","---","","# Checking and fixing a project's adherence to live-tokens","","Check the project, then fix every finding of `check-page` and `check-component` until both exit 0. `check-page` checks pages. Every component comes from the catalogue, every prop is declared, and every value in page CSS is a design token. `check-component` checks the project's own components. Every token names a semantic property, and its default is the design token that property reads. Every finding carries its own `guidance`, and the repair follows it.","","Both checkers are static. They read the project's source, never open a browser, and never mount a runtime. `npx live-tokens check-page <file> --tests` proves a page's rendered paint, and `npx live-tokens check-component <id> --tests` proves a component's declared behavior. **live-tokens-create-page** and **live-tokens-create-component** run those, one file or one id at a time, and their findings carry guidance the same way.","","## Workflow","","1. Run both checkers with `--json`. Each first brings `tokens.css` up to the installed package by applying every additive migration, then applies every finding whose `repair` is `auto`, checks again, and returns the fixes it applied in `fix.applied` beside the findings that remain. A pending breaking migration returns as a `tokens-breaking-migration` finding, since it renames design tokens the project may read. A fix in `fix.skipped` found its text moved, and the next run applies it. `--no-fix` reports without editing, for a build or CI."," ```sh"," npx live-tokens check-page --json"," npx live-tokens check-component --json"," ```","2. Read what remains. Each finding carries the fields under Finding fields, with a `repair` of `choice` or `authored`.","3. Group the findings by rule. Take the largest error group first, then the remaining errors. Take warnings only when the repair scope includes warnings.","4. Make every repair in the group from its `guidance`, within Scope below.","5. Run both checkers again. When repairable findings remain in scope, return to step 3. When no token fits a remaining finding, leave it and continue to the reply with its reason.","6. When the errors are clear, run both checkers with `--strict`. Report what `--strict` adds. Clear warnings within the existing request. Otherwise ask whether to clear the warnings now.","7. When the repair scope includes warnings, return to step 3 with `--strict`. When strict checks pass or the user defers warnings, continue to the reply.","8. Reply with:"," - the fixes the checkers applied, each with its count and any visible shift"," - the remaining changes by rule, each with its count and any visible shift"," - the findings left, each with its reason and any config entry the user chose"," - both checker commands with their exit codes","","`check-page <path>` scopes a run to one page. `check-component <id>` scopes a run to one component: its runtime, its editor, and its registration. The checkers read tokens.css from its default location.","","## Finding fields","","Every finding from both checkers, under each checker's `--json`:","","| Field | Value |","| --- | --- |","| `rule`, `severity`, `file`, `line`, `message` | What the finding is and where. |","| `guidance` | How to make the repair: the token for the role or the scale step, the command that prints the candidates, or the section of a create skill that owns the fix. |","| `repair` | `auto`, `choice`, or `authored`. See Repair levels. |","| `exception` | The narrower config entry that records a decision to leave the finding as it is: `{ \"checks\": { \"exclude\": [\"<file>\"] } }` for a page or CSS file, otherwise `{ \"checks\": { \"rules\": { \"<rule>\": \"warn\" } } }`. Applying it steps the rule down one level, error to warn and warn to off. |","| `details` | Per-rule data the message already states in prose, such as the accepted values behind `unknown-prop-value` or the nearest design-token candidates behind `dimension-literal`. Absent when a rule has nothing to add. |","","## Repair levels","","Every finding's `repair` says what moving it costs.","","- **`auto`.** The value determines the fix, such as a spacing literal with one nearest design-token step. The checkers apply it and list it in `fix.applied`.","- **`choice`.** A role or an ambiguous value determines the fix, such as a color literal and the role it plays, or a spacing literal tied between two steps. `details` lists the candidates, and `guidance` says how to pick one.","- **`authored`.** New code is the fix: a runtime that has to start behaving, an editor schema that has to name a token, a route that has to move. `guidance` names what to write. No candidate list applies.","","## Scope","","- Add no token to `tokens.css`. Map a literal with no matching token to the nearest existing token by role. When no token fits, leave the finding and say so.","- When the nearest token differs from the literal, use the token and name the shift in the reply, such as `14px` to `--space-16`.","- Apply a `tokens-breaking-migration` finding with `npx live-tokens migrate`, after `npx live-tokens migrate --check` prints the plan. `--tokens <path>` names a tokens.css in an unusual place, and `--write` also rewrites the route references the plan lists. Name each renamed design token in the reply.","- Any finding, at any repair level, can stay as a deliberate exception when the user chooses to keep it. Record that decision in the config entry its `exception` field names, and prefer the narrower entry. When the user has chosen to lower a rule's severity, record it in `live-tokens.config.json` under `\"checks\": { \"rules\": { \"<rule>\": \"warn\" } }`. `--off=<rule>` silences a rule for one run only."],
|
|
8
|
+
"SKILL.md": ["---","name: live-tokens-check-compliance","description: Check an existing @motion-proto/live-tokens project against the design system and fix it until check-page and check-component both exit 0. Checks for correct use of components, properties, and tokens. Runs both checkers, which bring tokens.css up to the installed package, apply every auto repair, and return each remaining finding with its own guidance, repair level, and details. Use when the user asks to check, audit, or review the project. Use when the user asks to fix the project or to make the build's design checks pass. Edits the files the checkers name. Changes tokens.css only through its migrations.","---","","# Checking and fixing a project's adherence to live-tokens","","Check the project, then fix every finding of `check-page` and `check-component` until both exit 0. `check-page` checks pages. Every component comes from the catalogue, every prop is declared, and every value in page CSS is a design token. `check-component` checks the project's own components. Every token names a semantic property, and its default is the design token that property reads. Every finding carries its own `guidance`, and the repair follows it.","","Both checkers are static. They read the project's source, never open a browser, and never mount a runtime. `npx live-tokens check-page <file> --tests` proves a page's rendered paint, and `npx live-tokens check-component <id> --tests` proves a component's declared behavior. **live-tokens-create-page** and **live-tokens-create-component** run those, one file or one id at a time, and their findings carry guidance the same way.","","## Workflow","","1. Run both checkers with `--json`. Each first brings `tokens.css` up to the installed package by applying every additive migration, then applies every finding whose `repair` is `auto`, checks again, and returns the fixes it applied in `fix.applied` beside the findings that remain. A pending breaking migration returns as a `tokens-breaking-migration` finding, since it renames design tokens the project may read. A fix in `fix.skipped` found its text moved, and the next run applies it. `--no-fix` reports without editing, for a build or CI."," ```sh"," npx live-tokens check-page --json"," npx live-tokens check-component --json"," ```","2. Read what remains. Each finding carries the fields under Finding fields, with a `repair` of `choice` or `authored`.","3. Group the findings by rule. Take the largest error group first, then the remaining errors. Take warnings only when the repair scope includes warnings.","4. Make every repair in the group from its `guidance`, within Scope below.","5. Run both checkers again. When repairable findings remain in scope, return to step 3. When no token fits a remaining finding, leave it and continue to the reply with its reason.","6. When the errors are clear, run both checkers with `--strict`. Report what `--strict` adds. Clear warnings within the existing request. Otherwise ask whether to clear the warnings now.","7. When the repair scope includes warnings, return to step 3 with `--strict`. When strict checks pass or the user defers warnings, continue to the reply.","8. Reply with:"," - the fixes the checkers applied, each with its count and any visible shift"," - the remaining changes by rule, each with its count and any visible shift"," - the findings left, each with its reason and any config entry the user chose"," - the `a11y-*` alerts, each with its element and guidance, for the user to review"," - both checker commands with their exit codes","","`check-page <path>` scopes a run to one page. `check-component <id>` scopes a run to one component: its runtime, its editor, and its registration. The checkers read tokens.css from its default location.","","## Finding fields","","Every finding from both checkers, under each checker's `--json`:","","| Field | Value |","| --- | --- |","| `rule`, `severity`, `file`, `line`, `message` | What the finding is and where. |","| `guidance` | How to make the repair: the token for the role or the scale step, the command that prints the candidates, or the section of a create skill that owns the fix. |","| `repair` | `auto`, `choice`, or `authored`. See Repair levels. |","| `exception` | The narrower config entry that records a decision to leave the finding as it is: `{ \"checks\": { \"exclude\": [\"<file>\"] } }` for a page or CSS file, otherwise `{ \"checks\": { \"rules\": { \"<rule>\": \"warn\" } } }`. Applying it steps the rule down one level, error to warn and warn to off. |","| `details` | Per-rule data the message already states in prose, such as the accepted values behind `unknown-prop-value` or the nearest design-token candidates behind `dimension-literal`. Absent when a rule has nothing to add. |","","## Repair levels","","Every finding's `repair` says what moving it costs.","","- **`auto`.** The value determines the fix, such as a spacing literal with one nearest design-token step. The checkers apply it and list it in `fix.applied`.","- **`choice`.** A role or an ambiguous value determines the fix, such as a color literal and the role it plays, or a spacing literal tied between two steps. `details` lists the candidates, and `guidance` says how to pick one.","- **`authored`.** New code is the fix: a runtime that has to start behaving, an editor schema that has to name a token, a route that has to move. `guidance` names what to write. No candidate list applies.","","## Accessibility alerts","","A `--tests` run adds the `a11y-*` rules, which test the rendered page or component against WCAG 2 A and AA with axe, Siteimprove Alfa, the contract's keyboard cases, and the suite's own focus indicator, control boundary, text spacing, and reduced motion checks. Each reports at `warn` and blocks only under `--strict`. Every `a11y-*` finding is an alert the user reviews: list it in the reply, and repair it from its guidance when the repair scope includes warnings. An `a11y-review` finding marks a check axe could not decide, such as text over a gradient, and the user judges it by eye. Inside the live-tokens repository, an `a11y-component` finding with engine rule `aria-snapshot` marks an accessibility tree that changed since it was recorded; after a deliberate markup change, `check-component <id> --tests --update-aria` records it anew.","","## Scope","","- Add no token to `tokens.css`. Map a literal with no matching token to the nearest existing token by role. When no token fits, leave the finding and say so.","- When the nearest token differs from the literal, use the token and name the shift in the reply, such as `14px` to `--space-16`.","- Apply a `tokens-breaking-migration` finding with `npx live-tokens migrate`, after `npx live-tokens migrate --check` prints the plan. `--tokens <path>` names a tokens.css in an unusual place, and `--write` also rewrites the route references the plan lists. Name each renamed design token in the reply.","- Any finding, at any repair level, can stay as a deliberate exception when the user chooses to keep it. Record that decision in the config entry its `exception` field names, and prefer the narrower entry. When the user has chosen to lower a rule's severity, record it in `live-tokens.config.json` under `\"checks\": { \"rules\": { \"<rule>\": \"warn\" } }`. `--off=<rule>` silences a rule for one run only."],
|
|
9
9
|
},
|
|
10
10
|
"create-component": {
|
|
11
|
-
"SKILL.md": ["---","name: live-tokens-create-component","description: Create an editable component for a @motion-proto/live-tokens project. A component is a runtime Svelte file, an editor Svelte file, and one registration. The runtime file declares one semantic property per editable CSS property and assigns each an existing design token. The property names are semantic, based on function, and reuse the names of the existing components. Use when live-tokens-pick-component finds no suitable component, or the user asks for a new component. Use when the user asks to make an existing Svelte component editable in the live-tokens editor. For page integration, read live-tokens-create-page.","---","","# Creating a component for a live-tokens project","","Create a component whose structure and behavior serve the user's purpose. Give each editable visual property a semantic name. Assign its default from the existing design tokens. Deliver the runtime file, the editor file, and the registration together.","","## Workflow","","1. Read the project: `package.json`, `live-tokens.config.json`, `src/main.ts`, the catalogue, the token scales the component will use, a shipped runtime and editor pair, and the property suffixes.","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.","3. Write the runtime file: the catalogue export and the `:global(:root)` block. A structural choice is an intrinsic. Every component joins the sketch layer, and a fixed overlay portals to `<body>`.","4. Write the editor file: the schema, the preview props, and the markup. Variants that share a value are linked.","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.","6. Run `npx live-tokens check-component <id> --tests --strict --json` until it exits 0 with complete applicable coverage, then the Svelte check and the build.","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**.","","## Design model","","A live-tokens project has two layers.","","| Layer | Responsibility | Example |","|---|---|---|","| Design tokens | Name the available colors, typography, geometry, and motion values. A theme sets the values. | `--space-16`, `--radius-md`, `--surface-neutral` |","| Semantic properties | Name the visual roles within a component and reference tokens. A component config records these assignments. | `--statcard-padding: var(--space-16)` |","","A token is assigned to a property, and a CSS declaration reads the property. The editor changes the assignment; the runtime reads the property. Keep the assignment a token reference, so a theme change reaches the component.","","A property describes its purpose: `--statcard-value`, `--statcard-radius`, `--statcard-label-font-size`. Its name stays stable when its assigned color or size changes. Use role names such as `surface` and `text`. Use full words for component ids and parts.","","A component is distinct in its anatomy, its proportions, its content hierarchy, and its behavior. Its appearance comes from the existing tokens. Create only the variants and states the task requires. The tokens stay as they are; a new token is a separate change to the design system.","","Props carry content and behavior: a value, a label, a callback. Properties carry the editable appearance. When a variant is a choice the page makes, expose it as a prop.","","Prop names follow the shipped components. `label` names a control, `title` heads content, `text` is body copy. `open` is the one prop for an open state. `value` holds a selection's id. A size prop's values are `default` and `small`. A callback prop is `on` followed by the event name, all lowercase, as in `onchange`, `onclose`, and `onsave`.","","## Source inspection","","Before writing a file:","","1. Read the project's `package.json`, `live-tokens.config.json`, and `src/main.ts`.","2. Run `npx live-tokens components`. The list holds every component the project has, with its variants and catalogue entry. `npx live-tokens components <id>` prints one component's props.","3. Run `npx live-tokens tokens --scale <name>` for each token scale the component will use. Those names are the tokens a property can reference.","4. Read a shipped runtime and editor pair: `Toggle` for interaction states, `Badge` for variants and linked values, `Card` for text and container parts.","5. Read `references/token-naming.md` for the suffixes that select editor controls.","","The shipped sources are in `node_modules/@motion-proto/live-tokens/src/`: the runtime at `system/components/<Name>.svelte`, the editor at `editor/component-editor/<Name>Editor.svelte`. Inside the live-tokens repository, read them from the repository root. The source is the contract.","","## Variants and states","","A component has three kinds of division. Keep them apart in the props, the names, and the editor.","","| Kind | Meaning | Example |","|---|---|---|","| Part | Regions present at once | Dialog's overlay, header, body, footer |","| Variant | Alternative presentations the page chooses | Badge's brand, danger |","| State | A runtime condition | Toggle's on, hover, disabled |","","States have two axes. A component state is one of a set that excludes the others: default, selected (or on), disabled. An interaction state layers on a component state: default, hover, and later focus or active. `disabled` is terminal: no other state layers on it, in the names or in the editor.","","The default state carries the shared geometry and typography. A state adds properties only for the values that change: `--toggle-on-track-surface`, `--toggle-on-hover-track-surface`.","","The preview renders the state being edited. Pair each `:hover` selector with a `.force-hover` selector and expose a `class` prop, so the editor shows hover without a pointer. Keep native disabled behavior, keyboard operation, and visible focus on an interactive control.","","A component supplies its variants. The page chooses the one primary action.","","## Property design","","Before writing a file, identify the component's parts, text roles, variants, and states. Then write a property map: one row per editable role, with the token it is assigned and the CSS property it controls. Keep separate roles independent even when they start with the same value.","","| Property | Assigned token | CSS use |","|---|---|---|","| `--statcard-surface` | `--surface-neutral` | `background` |","| `--statcard-border` | `--border-neutral` | `border-color` |","| `--statcard-border-width` | `--border-width-1` | `border-width` |","| `--statcard-radius` | `--radius-md` | `border-radius` |","| `--statcard-padding` | `--space-16` | `padding` |","| `--statcard-value` | `--text-primary` | `color` of the value |","| `--statcard-value-font-size` | `--font-size-2xl` | `font-size` of the value |","| `--statcard-label` | `--text-secondary` | `color` of the label |","","Assign from the tokens the project has. Match the token scale to the role: `--surface-*` for a fill, `--border-*` for an outline, `--text-*` for text, and the space, radius, border-width, and icon-size scales for geometry. Give each text role five properties: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`.","","A property name starts with the component id and ends with the property suffix. Use this shape for part-specific states:","","```text","--<componentId>[-<variant>][-<part>][-<state>]-<property>","```","","- `componentId` is the runtime file name in lowercase with no dashes: `StatCard.svelte` is `statcard`.","- `variant` is present when the component has more than one: `--card-default-surface`, `--card-bare-surface`. A component with one variant has no variant segment: `--toggle-track-surface`.","- `part` names a region inside the component: `header`, `body`, `track`, `thumb`. The editor's `element` tag groups rows in the panel and is never a name segment.","- `state` comes before the property: `--card-hover-border`. `disabled` is terminal, so no name pairs `disabled` with another state.","- `property` is the suffix, and the suffix selects the editor control: `-surface` for a fill, `-border` for a border color, `-border-width` for a stroke, `-radius` for corners, `-padding` and `-gap` for spacing, and the five typography suffixes. `references/token-naming.md` lists every suffix.","","For a state that affects several parts, follow Toggle: `--toggle-on-hover-track-surface`. State segments precede the affected part.","","Name a role as the shipped component that paints the same thing names it. A fill is `-surface` in every shipped component. A knob is `-thumb`. A text role's color sits on the role's own name, `-title`, `-body`, `-label`, `-value`, and its typography hangs off that name: `--card-default-title-font-size`. A component with one text role uses `-text`: `--badge-brand-text`.","","## Runtime component","","Create `src/system/components/StatCard.svelte`. A component in another directory is listed by `components` and `report`, and checked by `check-component <id>`, when that directory is named in `\"componentDirs\"` in `live-tokens.config.json`. Use Svelte 5 props and snippets, semantic HTML, and the behavior the task requires.","","Open the file with a `<script module lang=\"ts\">` block that exports a `catalogue` entry in the shape every shipped component carries. `npx live-tokens components` prints it beside the id, and `components <id>` prints it with the props. Every value is a literal: a string in single, double, or backtick quotes, an object of strings, or an array. No `${}` interpolation, no concatenation, no identifier reference. Each `whenNotToUse` row is `{ when, use? }`: `when` states the condition that rules this component out, and `use`, where a sibling covers it, names that sibling by its component id (`table`, never `Table`); `check-component` reports `missing-description` when no component has that id. An optional `constraints` array states each rule of use as one sentence. An optional `props` map adds one line per prop whose values carry a choice, such as `variant`; each key names a prop the file declares, and the text says what the values mean.","","```svelte","<script module lang=\"ts\">"," import type { CatalogueEntry } from '@motion-proto/live-tokens';",""," export const catalogue = {"," description: 'A figure with its label.',"," whenToUse: 'one number the reader takes in at a glance.',"," whenNotToUse: ["," { when: 'the reader scans and compares many records.', use: 'table' },"," { when: 'the figure belongs to a titled block of content.', use: 'card' },"," ],"," constraints: ['The label names the figure in the words the page uses for it.'],"," } satisfies CatalogueEntry;","</script>","```","","An agent chooses by these sentences, so word them the way the shipped entries are worded:","","- `description` is one sentence that says what the component is.","- `whenToUse` states the one condition that makes this component right.","- Each `when` states one condition that rules this component out. It reads correctly after \"do not use <Name> when\".","- A row takes a `use` when another component fits that condition, and no `use` when none does.","- When the nearest sibling is one of the project's own components, give that sibling a row that names the new component. An agent reaches a component by following `use`.","","Declare every editable property in a literal `:global(:root)` block, each assigned a token. The plugin parses the Svelte source to seed `component-configs/<id>/default.json`, so the block holds plain declarations with no SCSS loop or interpolation.","","```svelte","<style>"," :global(:root) {"," --statcard-surface: var(--surface-neutral);"," --statcard-border: var(--border-neutral);"," --statcard-border-width: var(--border-width-1);"," --statcard-radius: var(--radius-md);"," --statcard-padding: var(--space-16);"," --statcard-value: var(--text-primary);"," --statcard-value-font-size: var(--font-size-2xl);"," --statcard-label: var(--text-secondary);"," }",""," .statcard {"," display: grid;"," background: var(--statcard-surface);"," border: var(--statcard-border-width) solid var(--statcard-border);"," border-radius: var(--statcard-radius);"," padding: var(--statcard-padding);"," }",""," .value { color: var(--statcard-value); font-size: var(--statcard-value-font-size); }"," .label { color: var(--statcard-label); }","</style>","```","","The excerpt shows the chain for part of the property map. Every editable value reads a property. Structural CSS (`display: grid`, `width: 100%`, `align-items: center`) stays in the layout rules. A value beyond a scale is a token expression: `calc(var(--space-64) * 4)`. A property that carries a structural choice, an alignment or a visibility, is an intrinsic: read `references/intrinsics.md`.","","## Component editor","","Create `src/system/components/StatCardEditor.svelte` beside the runtime file. The editor has three parts.","","1. A `<script module>` block exports `component`, the id, and `allTokens`, one row per property in the map. A row is `{ label, variable, element? }`; `element` groups rows in the panel by part, and `label` names the property in the row.","2. The instance script imports the runtime component and the editor primitives from the package's public paths, and maps the state being edited to preview props.","3. The markup mounts `ComponentEditorBase` with one `VariantGroup` per variant, each rendering a preview.","","```svelte","<script module lang=\"ts\">"," import type { Token } from '@motion-proto/live-tokens/component-editor';",""," export const component = 'statcard';"," const states: Record<string, Token[]> = {"," default: ["," { label: 'surface', element: 'frame', variable: '--statcard-surface' },"," { label: 'border', element: 'frame', variable: '--statcard-border' },"," { label: 'border width', element: 'frame', variable: '--statcard-border-width' },"," { label: 'radius', element: 'frame', variable: '--statcard-radius' },"," { label: 'padding', element: 'frame', variable: '--statcard-padding' },"," { label: 'text', element: 'value', variable: '--statcard-value' },"," { label: 'font size', element: 'value', variable: '--statcard-value-font-size' },"," { label: 'text', element: 'label', variable: '--statcard-label' },"," ],"," };"," export const allTokens: Token[] = Object.values(states).flat();","</script>","","<script lang=\"ts\">"," import { ComponentEditorBase, VariantGroup } from '@motion-proto/live-tokens/component-editor';"," import StatCard from './StatCard.svelte';","</script>","","<ComponentEditorBase {component} title=\"Stat Card\" tokens={allTokens}>"," <VariantGroup name=\"statcard\" title=\"Stat Card\" {states} {component}>"," <StatCard value=\"1,204\" label=\"Sessions\" />"," </VariantGroup>","</ComponentEditorBase>","```","","The shipped editor for the closest component gives the preview snippet for a component with states. Custom chrome in an editor takes `--ui-*` tokens and no accent color; its copy uses periods and commas, never em-dashes.","","When variants share a value, read `references/linked-siblings.md`. A `groupKey` is scoped to the text role: `value-font-size` and `label-font-size` stay separate keys. A `buildTypeGroup*` helper takes `{ component, variants }` so it derives one key per role.","","## Registration","","Register the component in `src/registerComponents.ts`, a registration-only module, beside any registration already there. The id is unique; a registration that repeats a shipped id replaces that component.","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import { catalogue } from './system/components/StatCard.svelte';","import StatCardEditor, { allTokens as statCardTokens } from './system/components/StatCardEditor.svelte';","","registerComponent({"," id: 'statcard',"," label: 'Stat Card',"," icon: 'fas fa-chart-simple',"," sourceFile: 'src/system/components/StatCard.svelte',"," editorComponent: StatCardEditor,"," schema: statCardTokens,"," catalogue,","});","```","","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.","","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.","","Inside the live-tokens repository, a first-party component keeps its editor in `src/editor/component-editor/` and takes an entry in `builtInRegistry` in `src/editor/component-editor/registry.ts`.","","## Sketch mode and overlays","","Every component joins the sketch layer: read `references/sketch-mode.md`. A suitable root or inner wrapper carries a reserved class and names five `--sketch-*` values from its own properties. Preserve positioning, clipping, and pseudo-elements as the reference specifies. A first-party component adds a `PartSpec` row instead.","","A fixed overlay portals to `<body>`: read `references/fixed-overlays.md`. A container that owns the typography of its content follows `Card` and its `prose` prop.","","## Verification","","1. 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`. It applies every `auto` repair, runs the registry contract and, for a component with a contract in the module `contractsModule` names, the component contract suites, and returns the fixes it applied, the findings that remain, and coverage by rule. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","2. Each remaining finding carries a rule id, a line, and its `guidance`. Make each repair from its guidance, and run the command again until it exits 0 with complete applicable coverage and no disabled checks.","3. Run the project's Svelte check and its build.","4. Reply with the files, the component id, the props, and the result of each check, naming any check the environment prevented.","","`--tests` covers every line a reviewer once checked by eye: the component's listing, its controls and preview, persistence and reset, theme projection, linked properties, and Sketch mode.","","Then place the component on a page with **live-tokens-create-page**."],
|
|
12
|
-
"references/contract-tests.md": ["# The registry contract as a test in a consumer project","","The package ships the contract as a test file. `checkRegistryEntry` is the","assertion behind it, exported so a project can write its own file instead. The","contract takes one registry entry and returns a violation line per failure; an","empty array is the pass.","","## The shipped path","","`npx live-tokens check-component <id> --tests` runs the shipped file for you,","under vitest, alongside the Playwright component contract suites, and maps","every failure to a finding with a rule id and a line. The compiled file is","`src/testing-js/registry.contract.js`; the `.ts` source it compiles from is not","in the tarball.","","It resolves a shipped component's `sourceFile` against the package and yours","against your project, and reports a component that exists as files and never","reached a registration.","","A project scaffolded by `create` has `@playwright/test`, `vitest`, and","`happy-dom` installed, with `live-tokens.testing.ts`, `src/registerComponents.ts`,","and `tests/contracts.ts` in place. Any other project adds the three as","devDependencies. Both run `npx playwright install chromium` once. A missing","one is a `tests-not-installed` finding naming the install command.","","Name the module that registers your components, in `live-tokens.testing.ts` at","the project root, as a plain quoted string:","","```ts","// live-tokens.testing.ts","import { defineTestingConfig } from '@motion-proto/live-tokens/testing/vitest';","","export default defineTestingConfig({"," registrySetup: 'src/registerComponents.ts',","});","```","","The setup module registers and stops there, exactly as the Registration","section of live-tokens-create-component wires it up:","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import MyWidgetEditor, { allTokens } from './system/components/MyWidgetEditor.svelte';","","registerComponent({"," id: 'mywidget',"," label: 'My Widget',"," icon: 'fas fa-magic',"," sourceFile: 'src/system/components/MyWidget.svelte',"," editorComponent: MyWidgetEditor,"," schema: allTokens,","});","```","","Import the same module from `src/main.ts`, so one list of registrations serves","the app and the tests. Importing an editor registers nothing, and importing","`main.ts` would mount the app, which is why the registrations live in a module","of their own.","","`LIVE_TOKENS_COMPONENT=<id>` is what `check-component <id> --tests` sets for","you; it narrows the run to one component and fails when no component is","registered under that id.","","## The component contract","","The eight Playwright suites drive one `ComponentContract` per component. The","package ships one for each of its own components and none for yours. A run","with `LIVE_TOKENS_COMPONENT=<id>` and no contract for `<id>` reports one","`contract-missing` finding, which names the setting that unlocks the suites.","","Name the module that exports your contracts as `contractsModule`, next to","`registrySetup`:","","```ts","// live-tokens.testing.ts","export default defineTestingConfig({"," registrySetup: 'src/registerComponents.ts',"," contractsModule: 'tests/contracts.ts',","});","```","","The module exports a `ComponentContract[]`, as the default export or a named","`contracts` export. The type ships from `@motion-proto/live-tokens/testing`,","and the doc comment on each field is the reference for it. A contract","declares the component's parts as selectors inside the preview, then one","expectation per obligation:","","```ts","// tests/contracts.ts","import type { ComponentContract } from '@motion-proto/live-tokens/testing';","","export const contracts: ComponentContract[] = ["," {"," id: 'statcard',"," origin: 'custom',"," root: 'root',"," parts: { root: '.statcard', value: '.statcard-value' },"," properties: ["," {"," paints: {"," root: {"," backgroundColor: '--statcard-surface',"," borderTopColor: '--statcard-border',"," borderTopWidth: '--statcard-border-width',"," borderRadius: '--statcard-radius',"," paddingTop: '--statcard-padding',"," },"," value: { color: '--statcard-value-text', fontSize: '--statcard-value-font-size' },"," },"," },"," ],"," states: { applicable: false, reason: 'a stat card renders one state' },"," persistence: {"," cases: ["," { shape: 'token', variable: '--statcard-radius', observe: { part: 'root', css: 'borderRadius' } },"," ],"," resetVariable: '--statcard-radius',"," },"," theme: {"," theme: 'brand',"," changed: ['--statcard-radius'],"," unchanged: ['--statcard-padding'],"," aliasedTo: { '--statcard-radius': '--radius-none' },"," observe: { part: 'root', css: 'borderRadius', variable: '--statcard-radius' },"," },"," interaction: { applicable: false, reason: 'a stat card carries no interactive role' },"," sketch: {"," style: 'pencil',"," parts: [{ part: 'root', fill: '--statcard-surface', stroke: '--statcard-border' }],"," },"," },","];","```","","Each obligation and what it needs from the project:","","- `properties`: part key, then CSS property, then the semantic property that"," drives it. One entry per variant tab and state tab the editor renders;"," omit `variant` and `state` when the editor renders one view. The paint maps"," and `uncovered` together name every property in `default.json`. A property"," no computed style shows verbatim, one consumed inside `calc()` or a gradient"," function, goes in `uncovered` with its reason.","- `states`: one entry per state tab, with the class or attributes the preview"," forces. `applicable: false` when the editor renders no state strip.","- `persistence`: an edit the run makes through the editor, saves, reloads, and"," reads back. `resetVariable` is a property one of the cases moves.","- `theme`: a theme in the project's data tree whose block for the component"," reassigns a property. Save one with the editor before the first run."," `changed` names the properties the theme moves, `unchanged` the ones it"," leaves, and `aliasedTo` the design token each reassigned property resolves"," to under it.","- `interaction`: the interactive part's role and its cases. `applicable:"," false` for a component with no interactive role.","- `behavior`: what a callback prop does. See Behavior below.","- `sketch`: a sketch style id and the fill and stroke each reserved-class part"," resolves to under it. `applicable: false` only for a component"," `references/sketch-mode.md` exempts.","","## Behavior","","`behavior` runs under Vitest with `mount` from Svelte and happy-dom, not in","the editor preview, because a controlled component's callback prop has","nothing driving it there: the preview edits the token, not the prop. One","case names the props the runtime mounts with, the action a user takes, and","what that should do.","","```ts","behavior: {"," cases: ["," {"," name: 'clicking an off toggle asks for on',"," props: { checked: false },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'callback', prop: 'onchange', args: [true] },"," },"," {"," name: 'clicking leaves the switch where the prop put it',"," props: { checked: false },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'attribute', part: 'root', name: 'aria-checked', value: 'false' },"," },"," {"," name: 'checked drives aria-checked',"," props: { checked: true },"," expect: { kind: 'attribute', part: 'root', name: 'aria-checked', value: 'true' },"," },"," {"," name: 'a disabled toggle stays silent',"," props: { checked: false, disabled: true },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'no-callback', prop: 'onchange' },"," },"," ],","},","```","","Toggle is controlled: it never flips `checked` itself, so the click case","that asks for `on` and the mount case that reads `checked` are two separate","cases, not one. The disabled case pins that a disabled toggle calls nothing","back: the prop reaches the DOM as a real `disabled` attribute on the part,","and happy-dom withholds a dispatched click from a disabled `<button>`. A","component that reports through `input` rather than `click`, such as Slider,","gets no disabled case, because happy-dom delivers a dispatched `input` event","to a disabled control and the case would assert the environment rather than","the component.","","A component with no callback prop declares the obligation inapplicable, in","one line:","","```ts","behavior: { applicable: false, reason: 'a table wrapper declares no callback prop; its rows arrive as a snippet' },","```","","The fault this suite catches is a callback that fires with the wrong","argument, fires when a case says it should stay silent, or never fires at","all. `check-component <id> --tests` reports it as `contract-behavior`,","naming the case.","","## Running vitest yourself","","`check-component --tests` covers the shipped path. Run vitest directly only","when you need to drive it outside the CLI. `createVitestConfig` lives at","`@motion-proto/live-tokens/testing/vitest`, which never imports","`@playwright/test`, so a project holding only `vitest` and `happy-dom` can","still build this config:","","```ts","// vitest.contract.config.ts","import { createVitestConfig } from '@motion-proto/live-tokens/testing/vitest';","import viteConfig from './vite.config';","import settings from './live-tokens.testing';","","export default createVitestConfig(viteConfig, { registrySetup: settings.registrySetup });","```","","```bash","npx vitest run --config vitest.contract.config.ts","```","","## Writing your own file","","`checkRegistryEntry` is exported at","`@motion-proto/live-tokens/component-editor/contract`, so a project that wants","its own suite writes two lines against its own registrations:","","```ts","// tests/registryContract.test.ts","// @vitest-environment happy-dom","import { describe, it, expect } from 'vitest';","import { getComponentRegistryEntries } from '@motion-proto/live-tokens';","import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';","import '../src/registerComponents';","","const mine = getComponentRegistryEntries().filter((e) => e.origin === 'custom');","","describe.each(mine.map((e) => [e.id, e] as const))('%s', (_id, entry) => {"," it('meets the registry contract', () => {"," expect(checkRegistryEntry(entry)).toEqual([]);"," });","});","```","","Filter on `origin`. The registry always carries the shipped components too, and","their `sourceFile` paths are relative to the package root. Without the filter","every built-in fails on a path that does not exist in your project.","","The helper reads the runtime file and `default.json` off disk, which is why it","is node-only and has its own subpath.","","## Paths","","Paths resolve against `process.cwd()` and `src/live-tokens/data/component-configs`.","A project that moved either passes them:","","```ts","checkRegistryEntry(entry, { projectRoot, componentConfigsDir });","```","","`componentConfigsDir` is the same directory `live-tokens.config.json` names.","","## What it holds","","1. **Registration**: `sourceFile` resolves to a real file, the schema is non-empty.","2. **Uniqueness**: no schema variable is declared twice.","3. **Editor to runtime**: every editable token's CSS var is declared in the"," runtime's `<style>` block, so an edit has something to repaint.","4. **Editor to default config**: every editable token has a seed alias in"," `component-configs/<id>/default.json`, so the component adopts with full"," defaults. A component with no `default.json` is editor-only; this check and"," the next one skip it.","5. **Opacity floors**: a token declaring `minOpacity` ships a default at or"," above it, so a floating panel starts out legible over page content.","6. **Round-trip**: `setComponentAlias` persists into the slice under the same key.","","Checks 3 and 4 exclude `hidden: true` tokens, `kind: 'gradient'` tokens (stored","as gradient objects, not vars), and `-padding-(top|right|bottom|left)` suffixes","(written on demand by the split-padding UI and read through the `themed-padding`","mixin's fallback chain, so they exist as neither `:root` declarations nor seeds)."],
|
|
11
|
+
"SKILL.md": ["---","name: live-tokens-create-component","description: Create an editable component for a @motion-proto/live-tokens project. A component is a runtime Svelte file, an editor Svelte file, and one registration. The runtime file declares one semantic property per editable CSS property and assigns each an existing design token. The property names are semantic, based on function, and reuse the names of the existing components. Use when live-tokens-pick-component finds no suitable component, or the user asks for a new component. Use when the user asks to make an existing Svelte component editable in the live-tokens editor. For page integration, read live-tokens-create-page.","---","","# Creating a component for a live-tokens project","","Create a component whose structure and behavior serve the user's purpose. Give each editable visual property a semantic name. Assign its default from the existing design tokens. Deliver the runtime file, the editor file, and the registration together.","","## Workflow","","1. Read the project: `package.json`, `live-tokens.config.json`, `src/main.ts`, the catalogue, the token scales the component will use, a shipped runtime and editor pair, and the property suffixes.","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.","3. Write the runtime file: the catalogue export and the `:global(:root)` block. A structural choice is an intrinsic. Every component joins the sketch layer, and a fixed overlay portals to `<body>`.","4. Write the editor file: the schema, the preview props, and the markup. Variants that share a value are linked.","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.","6. Run `npx live-tokens check-component <id> --tests --strict --json` until it exits 0 with complete applicable coverage, then the Svelte check and the build.","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**.","","## Design model","","A live-tokens project has two layers.","","| Layer | Responsibility | Example |","|---|---|---|","| Design tokens | Name the available colors, typography, geometry, and motion values. A theme sets the values. | `--space-16`, `--radius-md`, `--surface-neutral` |","| Semantic properties | Name the visual roles within a component and reference tokens. A component config records these assignments. | `--statcard-padding: var(--space-16)` |","","A token is assigned to a property, and a CSS declaration reads the property. The editor changes the assignment; the runtime reads the property. Keep the assignment a token reference, so a theme change reaches the component.","","A property describes its purpose: `--statcard-value`, `--statcard-radius`, `--statcard-label-font-size`. Its name stays stable when its assigned color or size changes. Use role names such as `surface` and `text`. Use full words for component ids and parts.","","A component is distinct in its anatomy, its proportions, its content hierarchy, and its behavior. Its appearance comes from the existing tokens. Create only the variants and states the task requires. The tokens stay as they are; a new token is a separate change to the design system.","","Props carry content and behavior: a value, a label, a callback. Properties carry the editable appearance. When a variant is a choice the page makes, expose it as a prop.","","Prop names follow the shipped components. `label` names a control, `title` heads content, `text` is body copy. `open` is the one prop for an open state. `value` holds a selection's id. A size prop's values are `default` and `small`. A callback prop is `on` followed by the event name, all lowercase, as in `onchange`, `onclose`, and `onsave`.","","## Source inspection","","Before writing a file:","","1. Read the project's `package.json`, `live-tokens.config.json`, and `src/main.ts`.","2. Run `npx live-tokens components`. The list holds every component the project has, with its variants and catalogue entry. `npx live-tokens components <id>` prints one component's props.","3. Run `npx live-tokens tokens --scale <name>` for each token scale the component will use. Those names are the tokens a property can reference.","4. Read a shipped runtime and editor pair: `Toggle` for interaction states, `Badge` for variants and linked values, `Card` for text and container parts.","5. Read `references/token-naming.md` for the suffixes that select editor controls.","","The shipped sources are in `node_modules/@motion-proto/live-tokens/src/`: the runtime at `system/components/<Name>.svelte`, the editor at `editor/component-editor/<Name>Editor.svelte`. Inside the live-tokens repository, read them from the repository root. The source is the contract.","","## Variants and states","","A component has three kinds of division. Keep them apart in the props, the names, and the editor.","","| Kind | Meaning | Example |","|---|---|---|","| Part | Regions present at once | Dialog's overlay, header, body, footer |","| Variant | Alternative presentations the page chooses | Badge's brand, danger |","| State | A runtime condition | Toggle's on, hover, disabled |","","States have two axes. A component state is one of a set that excludes the others: default, selected (or on), disabled. An interaction state layers on a component state: default, hover, and later focus or active. `disabled` is terminal: no other state layers on it, in the names or in the editor.","","The default state carries the shared geometry and typography. A state adds properties only for the values that change: `--toggle-on-track-surface`, `--toggle-on-hover-track-surface`.","","The preview renders the state being edited. Pair each `:hover` selector with a `.force-hover` selector and expose a `class` prop, so the editor shows hover without a pointer. Keep native disabled behavior, keyboard operation, and visible focus on an interactive control.","","A component supplies its variants. The page chooses the one primary action.","","## Property design","","Before writing a file, identify the component's parts, text roles, variants, and states. Then write a property map: one row per editable role, with the token it is assigned and the CSS property it controls. Keep separate roles independent even when they start with the same value.","","| Property | Assigned token | CSS use |","|---|---|---|","| `--statcard-surface` | `--surface-neutral` | `background` |","| `--statcard-border` | `--border-neutral` | `border-color` |","| `--statcard-border-width` | `--border-width-1` | `border-width` |","| `--statcard-radius` | `--radius-md` | `border-radius` |","| `--statcard-padding` | `--space-16` | `padding` |","| `--statcard-value` | `--text-primary` | `color` of the value |","| `--statcard-value-font-size` | `--font-size-2xl` | `font-size` of the value |","| `--statcard-label` | `--text-secondary` | `color` of the label |","","Assign from the tokens the project has. Match the token scale to the role: `--surface-*` for a fill, `--border-*` for an outline, `--text-*` for text, and the space, radius, border-width, and icon-size scales for geometry. Give each text role five properties: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`.","","A property name starts with the component id and ends with the property suffix. Use this shape for part-specific states:","","```text","--<componentId>[-<variant>][-<part>][-<state>]-<property>","```","","- `componentId` is the runtime file name in lowercase with no dashes: `StatCard.svelte` is `statcard`.","- `variant` is present when the component has more than one: `--card-default-surface`, `--card-bare-surface`. A component with one variant has no variant segment: `--toggle-track-surface`.","- `part` names a region inside the component: `header`, `body`, `track`, `thumb`. The editor's `element` tag groups rows in the panel and is never a name segment.","- `state` comes before the property: `--card-hover-border`. `disabled` is terminal, so no name pairs `disabled` with another state.","- `property` is the suffix, and the suffix selects the editor control: `-surface` for a fill, `-border` for a border color, `-border-width` for a stroke, `-radius` for corners, `-padding` and `-gap` for spacing, and the five typography suffixes. `references/token-naming.md` lists every suffix.","","For a state that affects several parts, follow Toggle: `--toggle-on-hover-track-surface`. State segments precede the affected part.","","Name a role as the shipped component that paints the same thing names it. A fill is `-surface` in every shipped component. A knob is `-thumb`. A text role's color sits on the role's own name, `-title`, `-body`, `-label`, `-value`, and its typography hangs off that name: `--card-default-title-font-size`. A component with one text role uses `-text`: `--badge-brand-text`.","","## Runtime component","","Create `src/system/components/StatCard.svelte`. A component in another directory is listed by `components` and `report`, and checked by `check-component <id>`, when that directory is named in `\"componentDirs\"` in `live-tokens.config.json`. Use Svelte 5 props and snippets, semantic HTML, and the behavior the task requires.","","Open the file with a `<script module lang=\"ts\">` block that exports a `catalogue` entry in the shape every shipped component carries. `npx live-tokens components` prints it beside the id, and `components <id>` prints it with the props. Every value is a literal: a string in single, double, or backtick quotes, an object of strings, or an array. No `${}` interpolation, no concatenation, no identifier reference. Each `whenNotToUse` row is `{ when, use? }`: `when` states the condition that rules this component out, and `use`, where a sibling covers it, names that sibling by its component id (`table`, never `Table`); `check-component` reports `missing-description` when no component has that id. An optional `constraints` array states each rule of use as one sentence. An optional `props` map adds one line per prop whose values carry a choice, such as `variant`; each key names a prop the file declares, and the text says what the values mean.","","```svelte","<script module lang=\"ts\">"," import type { CatalogueEntry } from '@motion-proto/live-tokens';",""," export const catalogue = {"," description: 'A figure with its label.',"," whenToUse: 'one number the reader takes in at a glance.',"," whenNotToUse: ["," { when: 'the reader scans and compares many records.', use: 'table' },"," { when: 'the figure belongs to a titled block of content.', use: 'card' },"," ],"," constraints: ['The label names the figure in the words the page uses for it.'],"," } satisfies CatalogueEntry;","</script>","```","","An agent chooses by these sentences, so word them the way the shipped entries are worded:","","- `description` is one sentence that says what the component is.","- `whenToUse` states the one condition that makes this component right.","- Each `when` states one condition that rules this component out. It reads correctly after \"do not use <Name> when\".","- A row takes a `use` when another component fits that condition, and no `use` when none does.","- When the nearest sibling is one of the project's own components, give that sibling a row that names the new component. An agent reaches a component by following `use`.","","Declare every editable property in a literal `:global(:root)` block, each assigned a token. The plugin parses the Svelte source to seed `component-configs/<id>/default.json`, so the block holds plain declarations with no SCSS loop or interpolation.","","```svelte","<style>"," :global(:root) {"," --statcard-surface: var(--surface-neutral);"," --statcard-border: var(--border-neutral);"," --statcard-border-width: var(--border-width-1);"," --statcard-radius: var(--radius-md);"," --statcard-padding: var(--space-16);"," --statcard-value: var(--text-primary);"," --statcard-value-font-size: var(--font-size-2xl);"," --statcard-label: var(--text-secondary);"," }",""," .statcard {"," display: grid;"," background: var(--statcard-surface);"," border: var(--statcard-border-width) solid var(--statcard-border);"," border-radius: var(--statcard-radius);"," padding: var(--statcard-padding);"," }",""," .value { color: var(--statcard-value); font-size: var(--statcard-value-font-size); }"," .label { color: var(--statcard-label); }","</style>","```","","The excerpt shows the chain for part of the property map. Every editable value reads a property. Structural CSS (`display: grid`, `width: 100%`, `align-items: center`) stays in the layout rules. A value beyond a scale is a token expression: `calc(var(--space-64) * 4)`. A property that carries a structural choice, an alignment or a visibility, is an intrinsic: read `references/intrinsics.md`.","","## Component editor","","Create `src/system/components/StatCardEditor.svelte` beside the runtime file. The editor has four parts.","","1. A `<script module>` block exports `component`, the id, and `allTokens`, one row per property in the map. A row is `{ label, variable, element? }`; `element` groups rows in the panel by part, and `label` names the property in the row.","2. The instance script imports the runtime component and the editor primitives from the package's public paths, and maps the state being edited to preview props.","3. The markup mounts `ComponentEditorBase` with one `VariantGroup` per variant, each rendering a preview.","4. The same `<script module>` block exports `contrastPairs`, one pair for each foreground color and each backdrop it sits on, and the registration passes it as `contrastPairs`. Every `allTokens` color with a text or icon suffix (`-text`, `-label`, `-icon`, `-title`, `-body`, `-value`, and the other text roles in `references/token-naming.md`) is the foreground of at least one pair. A pair is `{ foreground, backdrop, role, fontSize?, fontWeight?, inactive?, context? }`. `backdrop` lists the properties the runtime paints behind the foreground, top layer first, and leaves out `--page-bg`, which the editor puts under the last layer. `role` is `'text'`, or `'non-text'` for an icon or another mark. A text pair names its `-font-size` property as `fontSize`, the smaller one when the text renders at two sizes, and its `-font-weight` property as `fontWeight`. A disabled state's pair sets `inactive: true`, because WCAG exempts disabled controls. A foreground over several backdrops takes one pair per backdrop, each with its own `context`. A pair names only properties in `allTokens` and intrinsic variables, and no two pairs share a foreground, a backdrop, and a context.","","```svelte","<script module lang=\"ts\">"," import type { Token } from '@motion-proto/live-tokens/component-editor';",""," export const component = 'statcard';"," const states: Record<string, Token[]> = {"," default: ["," { label: 'surface', element: 'frame', variable: '--statcard-surface' },"," { label: 'border', element: 'frame', variable: '--statcard-border' },"," { label: 'border width', element: 'frame', variable: '--statcard-border-width' },"," { label: 'radius', element: 'frame', variable: '--statcard-radius' },"," { label: 'padding', element: 'frame', variable: '--statcard-padding' },"," { label: 'text', element: 'value', variable: '--statcard-value' },"," { label: 'font size', element: 'value', variable: '--statcard-value-font-size' },"," { label: 'text', element: 'label', variable: '--statcard-label' },"," ],"," };"," export const allTokens: Token[] = Object.values(states).flat();","</script>","","<script lang=\"ts\">"," import { ComponentEditorBase, VariantGroup } from '@motion-proto/live-tokens/component-editor';"," import StatCard from './StatCard.svelte';","</script>","","<ComponentEditorBase {component} title=\"Stat Card\" tokens={allTokens}>"," <VariantGroup name=\"statcard\" title=\"Stat Card\" {states} {component}>"," <StatCard value=\"1,204\" label=\"Sessions\" />"," </VariantGroup>","</ComponentEditorBase>","```","","The shipped editor for the closest component gives the preview snippet for a component with states. Custom chrome in an editor takes `--ui-*` tokens and no accent color; its copy uses periods and commas, never em-dashes.","","When variants share a value, read `references/linked-siblings.md`. A `groupKey` is scoped to the text role: `value-font-size` and `label-font-size` stay separate keys. A `buildTypeGroup*` helper takes `{ component, variants }` so it derives one key per role.","","## Registration","","Register the component in `src/registerComponents.ts`, a registration-only module, beside any registration already there. The id is unique; a registration that repeats a shipped id replaces that component.","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import { catalogue } from './system/components/StatCard.svelte';","import StatCardEditor, { allTokens as statCardTokens } from './system/components/StatCardEditor.svelte';","","registerComponent({"," id: 'statcard',"," label: 'Stat Card',"," icon: 'fas fa-chart-simple',"," sourceFile: 'src/system/components/StatCard.svelte',"," editorComponent: StatCardEditor,"," schema: statCardTokens,"," catalogue,","});","```","","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. An interactive component's contract declares a keyboard case for each key its WAI-ARIA pattern handles: Enter or Space activates, an arrow key moves where the pattern moves, and Escape closes an overlay. A component that declares intrinsics adds `intrinsics` to the entry. `check-component` finds the registration by the id literal inside the call.","","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.","","Inside the live-tokens repository, a first-party component keeps its editor in `src/editor/component-editor/` and takes an entry in `builtInRegistry` in `src/editor/component-editor/registry.ts`.","","## Sketch mode and overlays","","Every component joins the sketch layer: read `references/sketch-mode.md`. A suitable root or inner wrapper carries a reserved class and names five `--sketch-*` values from its own properties. Preserve positioning, clipping, and pseudo-elements as the reference specifies. A first-party component adds a `PartSpec` row instead.","","A fixed overlay portals to `<body>`: read `references/fixed-overlays.md`. A container that owns the typography of its content follows `Card` and its `prose` prop.","","## Verification","","1. 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`. It applies every `auto` repair, runs the registry contract and, for a component with a contract in the module `contractsModule` names, the component contract suites, and returns the fixes it applied, the findings that remain, and coverage by rule. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","2. Each remaining finding carries a rule id, a line, and its `guidance`. Make each repair from its guidance, and run the command again until it exits 0 with complete applicable coverage and no disabled checks. The `a11y-*` findings report accessibility at `warn`, and `--strict` makes them block with the rest. An `a11y-review` finding marks a check axe could not decide, such as text over a gradient: judge it by eye against its help page, and repair it when it fails. Name the ones that pass in the reply, and add `--off=a11y-review` to the final run, the one disabled check this step allows. Inside the live-tokens repository, an `aria-snapshot` finding marks an accessibility tree that changed since it was recorded; after a deliberate markup change, rerun with `--update-aria`.","3. Run the project's Svelte check and its build.","4. Reply with the files, the component id, the props, and the result of each check, naming any check the environment prevented.","","`--tests` covers every line a reviewer once checked by eye: the component's listing, its controls and preview, persistence and reset, theme projection, linked properties, and Sketch mode.","","Then place the component on a page with **live-tokens-create-page**."],
|
|
12
|
+
"references/contract-tests.md": ["# The registry contract as a test in a consumer project","","The package ships the contract as a test file. `checkRegistryEntry` is the","assertion behind it, exported so a project can write its own file instead. The","contract takes one registry entry and returns a violation line per failure; an","empty array is the pass.","","## The shipped path","","`npx live-tokens check-component <id> --tests` runs the shipped file for you,","under vitest, alongside the Playwright component contract suites, and maps","every failure to a finding with a rule id and a line. The compiled file is","`src/testing-js/registry.contract.js`; the `.ts` source it compiles from is not","in the tarball.","","It resolves a shipped component's `sourceFile` against the package and yours","against your project, and reports a component that exists as files and never","reached a registration.","","A project scaffolded by `create` has `@playwright/test`, `vitest`, and","`happy-dom` installed, with `live-tokens.testing.ts`, `src/registerComponents.ts`,","and `tests/contracts.ts` in place. Any other project adds the three as","devDependencies. Both run `npx playwright install chromium` once. A missing","one is a `tests-not-installed` finding naming the install command.","","Name the module that registers your components, in `live-tokens.testing.ts` at","the project root, as a plain quoted string:","","```ts","// live-tokens.testing.ts","import { defineTestingConfig } from '@motion-proto/live-tokens/testing/vitest';","","export default defineTestingConfig({"," registrySetup: 'src/registerComponents.ts',","});","```","","The setup module registers and stops there, exactly as the Registration","section of live-tokens-create-component wires it up:","","```ts","// src/registerComponents.ts","import { registerComponent } from '@motion-proto/live-tokens';","import MyWidgetEditor, { allTokens } from './system/components/MyWidgetEditor.svelte';","","registerComponent({"," id: 'mywidget',"," label: 'My Widget',"," icon: 'fas fa-magic',"," sourceFile: 'src/system/components/MyWidget.svelte',"," editorComponent: MyWidgetEditor,"," schema: allTokens,","});","```","","Import the same module from `src/main.ts`, so one list of registrations serves","the app and the tests. Importing an editor registers nothing, and importing","`main.ts` would mount the app, which is why the registrations live in a module","of their own.","","`LIVE_TOKENS_COMPONENT=<id>` is what `check-component <id> --tests` sets for","you; it narrows the run to one component and fails when no component is","registered under that id.","","## The component contract","","The eight Playwright suites drive one `ComponentContract` per component. The","package ships one for each of its own components and none for yours. A run","with `LIVE_TOKENS_COMPONENT=<id>` and no contract for `<id>` reports one","`contract-missing` finding, which names the setting that unlocks the suites.","","Name the module that exports your contracts as `contractsModule`, next to","`registrySetup`:","","```ts","// live-tokens.testing.ts","export default defineTestingConfig({"," registrySetup: 'src/registerComponents.ts',"," contractsModule: 'tests/contracts.ts',","});","```","","The module exports a `ComponentContract[]`, as the default export or a named","`contracts` export. The type ships from `@motion-proto/live-tokens/testing`,","and the doc comment on each field is the reference for it. A contract","declares the component's parts as selectors inside the preview, then one","expectation per obligation:","","```ts","// tests/contracts.ts","import type { ComponentContract } from '@motion-proto/live-tokens/testing';","","export const contracts: ComponentContract[] = ["," {"," id: 'statcard',"," origin: 'custom',"," root: 'root',"," parts: { root: '.statcard', value: '.statcard-value' },"," properties: ["," {"," paints: {"," root: {"," backgroundColor: '--statcard-surface',"," borderTopColor: '--statcard-border',"," borderTopWidth: '--statcard-border-width',"," borderRadius: '--statcard-radius',"," paddingTop: '--statcard-padding',"," },"," value: { color: '--statcard-value-text', fontSize: '--statcard-value-font-size' },"," },"," },"," ],"," states: { applicable: false, reason: 'a stat card renders one state' },"," persistence: {"," cases: ["," { shape: 'token', variable: '--statcard-radius', observe: { part: 'root', css: 'borderRadius' } },"," ],"," resetVariable: '--statcard-radius',"," },"," theme: {"," theme: 'brand',"," changed: ['--statcard-surface'],"," unchanged: ['--statcard-radius', '--statcard-padding'],"," aliasedTo: { '--statcard-surface': '--surface-neutral' },"," observe: { part: 'root', css: 'backgroundColor', variable: '--statcard-surface' },"," },"," interaction: { applicable: false, reason: 'a stat card carries no interactive role' },"," sketch: {"," style: 'pencil',"," parts: [{ part: 'root', fill: '--statcard-surface', stroke: '--statcard-border' }],"," },"," },","];","```","","Each obligation and what it needs from the project:","","- `properties`: part key, then CSS property, then the semantic property that"," drives it. One entry per variant tab and state tab the editor renders;"," omit `variant` and `state` when the editor renders one view. The paint maps"," and `uncovered` together name every property in `default.json`. A property"," no computed style shows verbatim, one consumed inside `calc()` or a gradient"," function, goes in `uncovered` with its reason.","- `states`: one entry per state tab, with the class or attributes the preview"," forces. `applicable: false` when the editor renders no state strip.","- `persistence`: an edit the run makes through the editor, saves, reloads, and"," reads back. `resetVariable` is a property one of the cases moves.","- `theme`: a saved theme in the project's data tree, previewed as it stands."," The run checks that the component follows the theme's design tokens."," `changed` names the properties the theme moves through its design tokens,"," such as colors, shadows, and fonts, `unchanged` the ones it leaves, and `aliasedTo` the design token each"," changed property resolves to under it. A theme assigns geometry steps only"," to the components it holds a block for, so a new component's radius,"," padding, and border width belong in `unchanged`.","- `interaction`: the interactive part's role and its cases, keyboard cases"," among them. See Keyboard cases below. `applicable: false` for a component"," with no interactive role.","- `boundaries`: optional. The parts that draw a control's visible edge,"," such as a text field or a switch track, each with a label:"," `[{ part: 'field', label: 'Field' }]`. The accessibility suite measures"," each part's border and surface against the backdrop behind it at 3:1, per"," WCAG 2.2 SC 1.4.11. A component with no control edge leaves it out.","- `behavior`: what a callback prop does. See Behavior below.","- `sketch`: a sketch style id and the fill and stroke each reserved-class part"," resolves to under it. `applicable: false` only for a component"," `references/sketch-mode.md` exempts.","","## Keyboard cases","","An interactive component declares one `press` case for each key its WAI-ARIA","pattern handles: Enter or Space activates, an arrow key moves focus or value","where the pattern moves it, and Escape closes an overlay. Each sets","`rule: 'a11y-keyboard'`. A failed keyboard case reports `a11y-keyboard` at","`warn` and lets the test pass; a case without `rule` fails as","`contract-interaction`, an error.","","```ts","interaction: {"," part: 'secondTab',"," role: 'button',"," cases: ["," {"," name: 'Enter activates a tab',"," rule: 'a11y-keyboard',"," action: { kind: 'press', part: 'secondTab', key: 'Enter' },"," expect: { kind: 'activated', part: 'secondTab' },"," },"," {"," name: 'arrow right moves focus to the next tab',"," rule: 'a11y-keyboard',"," action: { kind: 'press', part: 'firstTab', key: 'ArrowRight' },"," expect: { kind: 'focused', part: 'secondTab', value: true },"," },"," ],","},","```","","- `press` focuses `part` and sends `key`. Without `part`, the key goes to the"," element that holds focus, such as an overlay that focused itself as it"," opened.","- `activated` passes when the key fired a click on the part, as a native"," button does for Enter and Space. The preview never feeds a controlled"," component's callback back into its props, so a click is the activation it"," shows.","- `focused` with `value: true` names the part an arrow key moves focus to."," Arrow cases need parts that keep their position as the selection moves,"," such as `firstTab` and `secondTab`.","- `present` with `value: false` passes when the part has left the document,"," as a dialog's root does when Escape closes it:"," `{ kind: 'present', part: 'root', value: false }`.","","The accessibility suite reads the same contract. It scans every view that","`properties` and `states` name with axe and Siteimprove Alfa's rule SIA-R65,","measures focus indicators and the `boundaries` parts at 3:1, checks text","spacing and reduced motion, and reports each failure at `warn` under an","`a11y-*` rule. Mark placeholder","content the editor adds to its preview, such as a demo label, with","`data-preview-demo`, and the scans skip it.","","## Behavior","","`behavior` runs under Vitest with `mount` from Svelte and happy-dom, not in","the editor preview, because a controlled component's callback prop has","nothing driving it there: the preview edits the token, not the prop. One","case names the props the runtime mounts with, the action a user takes, and","what that should do.","","```ts","behavior: {"," cases: ["," {"," name: 'clicking an off toggle asks for on',"," props: { checked: false },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'callback', prop: 'onchange', args: [true] },"," },"," {"," name: 'clicking leaves the switch where the prop put it',"," props: { checked: false },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'attribute', part: 'root', name: 'aria-checked', value: 'false' },"," },"," {"," name: 'checked drives aria-checked',"," props: { checked: true },"," expect: { kind: 'attribute', part: 'root', name: 'aria-checked', value: 'true' },"," },"," {"," name: 'a disabled toggle stays silent',"," props: { checked: false, disabled: true },"," action: { kind: 'click', part: 'root' },"," expect: { kind: 'no-callback', prop: 'onchange' },"," },"," ],","},","```","","Toggle is controlled: it never flips `checked` itself, so the click case","that asks for `on` and the mount case that reads `checked` are two separate","cases, not one. The disabled case pins that a disabled toggle calls nothing","back: the prop reaches the DOM as a real `disabled` attribute on the part,","and happy-dom withholds a dispatched click from a disabled `<button>`. A","component that reports through `input` rather than `click`, such as Slider,","gets no disabled case, because happy-dom delivers a dispatched `input` event","to a disabled control and the case would assert the environment rather than","the component.","","A component with no callback prop declares the obligation inapplicable, in","one line:","","```ts","behavior: { applicable: false, reason: 'a table wrapper declares no callback prop; its rows arrive as a snippet' },","```","","The fault this suite catches is a callback that fires with the wrong","argument, fires when a case says it should stay silent, or never fires at","all. `check-component <id> --tests` reports it as `contract-behavior`,","naming the case.","","## Running vitest yourself","","`check-component --tests` covers the shipped path. Run vitest directly only","when you need to drive it outside the CLI. `createVitestConfig` lives at","`@motion-proto/live-tokens/testing/vitest`, which never imports","`@playwright/test`, so a project holding only `vitest` and `happy-dom` can","still build this config:","","```ts","// vitest.contract.config.ts","import { createVitestConfig } from '@motion-proto/live-tokens/testing/vitest';","import viteConfig from './vite.config';","import settings from './live-tokens.testing';","","export default createVitestConfig(viteConfig, { registrySetup: settings.registrySetup });","```","","```bash","npx vitest run --config vitest.contract.config.ts","```","","## Writing your own file","","`checkRegistryEntry` is exported at","`@motion-proto/live-tokens/component-editor/contract`, so a project that wants","its own suite writes two lines against its own registrations:","","```ts","// tests/registryContract.test.ts","// @vitest-environment happy-dom","import { describe, it, expect } from 'vitest';","import { getComponentRegistryEntries } from '@motion-proto/live-tokens';","import { checkRegistryEntry } from '@motion-proto/live-tokens/component-editor/contract';","import '../src/registerComponents';","","const mine = getComponentRegistryEntries().filter((e) => e.origin === 'custom');","","describe.each(mine.map((e) => [e.id, e] as const))('%s', (_id, entry) => {"," it('meets the registry contract', () => {"," expect(checkRegistryEntry(entry)).toEqual([]);"," });","});","```","","Filter on `origin`. The registry always carries the shipped components too, and","their `sourceFile` paths are relative to the package root. Without the filter","every built-in fails on a path that does not exist in your project.","","The helper reads the runtime file and `default.json` off disk, which is why it","is node-only and has its own subpath.","","## Paths","","Paths resolve against `process.cwd()` and `src/live-tokens/data/component-configs`.","A project that moved either passes them:","","```ts","checkRegistryEntry(entry, { projectRoot, componentConfigsDir });","```","","`componentConfigsDir` is the same directory `live-tokens.config.json` names.","","## What it holds","","1. **Registration**: `sourceFile` resolves to a real file, the schema is non-empty.","2. **Uniqueness**: no schema variable is declared twice.","3. **Editor to runtime**: every editable token's CSS var is declared in the"," runtime's `<style>` block, so an edit has something to repaint.","4. **Editor to default config**: every editable token has a seed alias in"," `component-configs/<id>/default.json`, so the component adopts with full"," defaults. A component with no `default.json` is editor-only; this check and"," the next one skip it.","5. **Opacity floors**: a token declaring `minOpacity` ships a default at or"," above it, so a floating panel starts out legible over page content.","6. **Round-trip**: `setComponentAlias` persists into the slice under the same key.","","Checks 3 and 4 exclude `hidden: true` tokens, `kind: 'gradient'` tokens (stored","as gradient objects, not vars), and `-padding-(top|right|bottom|left)` suffixes","(written on demand by the split-padding UI and read through the `themed-padding`","mixin's fallback chain, so they exist as neither `:root` declarations nor seeds)."],
|
|
13
13
|
"references/fixed-overlays.md": ["# Fixed overlays must portal to body","","Any `position: fixed` layer (modal, lightbox, full-screen backdrop) is trapped, clipped or painted under other chrome, by a transformed / `isolation` / `contain` / `will-change` ancestor. Real consumer pages and the editor's own preview pane both have one, so this is the normal case rather than the edge.","","Render the layer with `use:portal` from `src/system/internal/portal.ts` so it escapes to `<body>`. `use:portal={enabled}` keeps an in-flow preview variant where it is; `Dialog` is the worked example. `check:overlay-portal` fails the build when a component sets `position: fixed` without it. Anchored popovers are exempt: `Tooltip` is `position: absolute` against its trigger and belongs in the flow.","","Moving to `<body>` costs two things:","","- DOM events from the layer no longer bubble to a consumer ancestor, so pass component callbacks the way `Dialog` does.","- A subtree-scoped CSS-variable theme no longer reaches it. This library themes via `:root`, so nothing breaks here.","","A modal also needs `role=\"dialog\"` with `aria-modal`, focus moved in on open and restored on close, and `Tab` trapped inside. `ImageLightbox` is the worked example."],
|
|
14
14
|
"references/intrinsics.md": ["# Extension: intrinsics","","Some components expose **structural or display choices** that are not token values: an alignment (start / center), an element's visibility (show / hide), a layout position. These ride a custom `<select>` or checkbox authored in an editor snippet, outside the generic token grid, so they do not belong in `allTokens`. Toggle and most components have none. SectionDivider is the worked example (alignment, hairline position, eyebrow / description visibility).","","An intrinsic still cascades through a CSS custom property with a default in the runtime `:global(:root)`. The trap: that default now lives in two places, the runtime `:global(:root)` AND the editor's read-back getter. When they disagree the control displays a state the page never renders, and a native `<select>` won't even fire `onchange` to write the \"change\" the user thinks they made. `:global(:root)` is the source of truth.","","Declare intrinsics so the editor and the contract test stay honest:","","1. **Runtime `:global(:root)`** carries the per-variant default like any other variable:",""," ```css"," --mywidget-lg-align: start;"," --mywidget-lg-eyebrow-display: block;"," ```","","2. **Editor `<script module>`** exports `intrinsics: IntrinsicSpec[]`, one entry per structural property, each `default` mirroring `:global(:root)` per variant:",""," ```ts"," import type { IntrinsicSpec } from '@motion-proto/live-tokens/component-editor';",""," export const intrinsics: IntrinsicSpec[] = ["," {"," key: 'align',"," variants: ['lg', 'md', 'sm'],"," variable: (v) => `--mywidget-${v}-align`,"," values: ['start', 'center'],"," default: { lg: 'start', md: 'start', sm: 'start' },"," },"," ];"," ```","","3. **Read-back getters fall back to the spec default**, never a hard-coded constant. This is the rule that keeps the control's displayed default in step with what an unedited instance renders:",""," ```ts"," const byKey = new Map(intrinsics.map((i) => [i.key, i]));"," function readIntrinsic(key: string, v: string): string {"," const spec = byKey.get(key)!;"," const raw = readLiteral(spec.variable(v)) ?? spec.default[v]; // store override, else runtime default"," return spec.normalize ? spec.normalize(raw) : raw;"," }"," function getAlign(v: string) {"," return readIntrinsic('align', v) === 'center' ? 'center' : 'start';"," }"," ```",""," Writes go through `setComponentAlias(component, spec.variable(v), { kind: 'literal', value })` so the choice cascades to `:root` like any token.","","4. **Put `intrinsics` on the registry entry** so the contract test can see it. That is the same entry the recipe passes to `bootLiveTokens`, with one more field:",""," ```ts"," bootLiveTokens(App, '#app', {"," components: [{"," id: 'mywidget',"," // ...label, icon, sourceFile, editorComponent, schema..."," intrinsics: myWidgetIntrinsics,"," }],"," });"," ```","","Use `normalize` only when two raw values render identically and the dropdown lists one (SectionDivider folds `above-description` into `below-label`). Properties that resemble intrinsics but are not: preview-only props with no persistence (a size selector that only changes the demo), `setComponentConfig` editor metadata (Dialog's button variants), and token-valued selects (a control choosing between two tokens). None carry a duplicated runtime default, so none need an `IntrinsicSpec`."],
|
|
15
15
|
"references/linked-siblings.md": ["# Extension: linked siblings","","Read this when the component has more than one variant and those variants share base properties (surface, radius, padding) that should move together. Toggle and SectionDivider have no linked tokens and skip all of it.","","Toggle's tokens are flat per state. Most multi-variant components (Badge, Card, SegmentedControl) share base properties across variants and surface that equality via a *linked block*: one edit propagates to every variant, while per-variant properties stay independent. Five additions to the Toggle pattern; see `BadgeEditor.svelte` in `node_modules` for the full file.","","1. **Mark linkable tokens** with `canBeLinked: true` + a `groupKey`. Peers sharing a `groupKey` form a link set across variants.",""," ```ts"," function variantBaseTokens(v: Variant): Token[] {"," return ["," { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--badge-${v}-padding` },"," { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--badge-${v}-radius` },"," ];"," }"," // Colors omit canBeLinked. Per-variant by design."," function variantColorTokens(v: Variant): Token[] {"," return ["," { label: 'surface color', groupKey: 'surface', variable: `--badge-${v}-surface` },"," { label: 'text color', groupKey: 'text', variable: `--badge-${v}-text` },"," ];"," }"," ```","","2. **Build a `linkableContexts: Map<variable, contextLabel>`** in `<script module>`. The label (e.g. `\"success base\"`) is how the LinkageChart row identifies this variable. Plain literal Map, no helper needed.",""," ```ts"," const linkableContexts = new Map<string, string>("," variants.flatMap((v) =>"," variantBaseTokens(v)"," .filter((t) => t.canBeLinked)"," .map((t) => [t.variable, `${v} base`] as [string, string]),"," ),"," );"," ```","","3. **Compute `linked` and mask currently-linked rows** out of per-state lists, so they render once inside the LinkedBlock instead of twice.",""," ```ts"," import { editorState } from '@motion-proto/live-tokens';"," import { computeLinkedBlock, withLinkedDisabled, buildSiblings }"," from '@motion-proto/live-tokens/component-editor';",""," let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));"," let visibleVariantStates = $derived((v: Variant) => Object.fromEntries("," Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),"," ));"," ```","","4. **Pass `{linked}` to `ComponentEditorBase`** so the LinkedBlock renders above the variant groups.","","5. **Multi-variant editors iterate VariantGroups** with `buildSiblings` so cross-variant link rows resolve to their peers.",""," ```svelte"," <ComponentEditorBase {component} title=\"Badge\" tokens={allTokens} {linked} variants={variantOptions}>"," {#each variants as v}"," <VariantGroup"," name={v}"," title={v}"," states={visibleVariantStates(v)}"," {component}"," siblings={buildSiblings(variants, v, variantStates)}"," >"," ...preview snippet"," </VariantGroup>"," {/each}"," </ComponentEditorBase>"," ```","","Single-variant components with multi-state linked tokens still set `canBeLinked` + `linkableContexts`, but skip `buildSiblings` and the `{#each}` loop. Components with no linked tokens (Toggle, SectionDivider) skip all five steps; `ComponentEditorBase` renders without a `{linked}` prop."],
|
|
@@ -2,7 +2,7 @@ import type { SkillTree } from '../types';
|
|
|
2
2
|
|
|
3
3
|
export const checkCompliance: SkillTree = {
|
|
4
4
|
"id": "live-tokens-check-compliance",
|
|
5
|
-
"digest": "sha256:
|
|
5
|
+
"digest": "sha256:653134a5c594aca6",
|
|
6
6
|
"title": "check-compliance",
|
|
7
7
|
"tagline": "Check and Fix a Project's Use of Live Tokens",
|
|
8
8
|
"nodes": [
|
|
@@ -40,8 +40,8 @@ export const checkCompliance: SkillTree = {
|
|
|
40
40
|
"row": 3,
|
|
41
41
|
"kind": "done",
|
|
42
42
|
"title": "Reply with the results",
|
|
43
|
-
"desc": "The fixes, the judgment calls made, the findings left with their reasons, and both exit codes.",
|
|
44
|
-
"lines": [25,
|
|
43
|
+
"desc": "The fixes, the judgment calls made, the findings left with their reasons, the accessibility alerts, and both exit codes.",
|
|
44
|
+
"lines": [25, 30],
|
|
45
45
|
"anchor": "Reply with:",
|
|
46
46
|
"anchorEnd": "both checker commands with their exit codes"
|
|
47
47
|
}
|
|
@@ -2,7 +2,7 @@ import type { SkillTree } from '../types';
|
|
|
2
2
|
|
|
3
3
|
export const createComponent: SkillTree = {
|
|
4
4
|
"id": "live-tokens-create-component",
|
|
5
|
-
"digest": "sha256:
|
|
5
|
+
"digest": "sha256:a11744345425852c",
|
|
6
6
|
"title": "create-component",
|
|
7
7
|
"tagline": "Create a LiveTokens Component",
|
|
8
8
|
"nodes": [
|
|
@@ -119,12 +119,12 @@ export const createComponent: SkillTree = {
|
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
"label": "Sketch mode",
|
|
122
|
-
"lines": [
|
|
122
|
+
"lines": [236, 236],
|
|
123
123
|
"anchor": "Every component joins the sketch layer: read `references/ske"
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
"label": "Fixed overlays",
|
|
127
|
-
"lines": [
|
|
127
|
+
"lines": [238, 238],
|
|
128
128
|
"anchor": "A fixed overlay portals to `<body>`: read `references/fixed-"
|
|
129
129
|
}
|
|
130
130
|
]
|
|
@@ -155,7 +155,7 @@ export const createComponent: SkillTree = {
|
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
"label": "Linked siblings",
|
|
158
|
-
"lines": [
|
|
158
|
+
"lines": [205, 205],
|
|
159
159
|
"anchor": "When variants share a value, read `references/linked-sibling"
|
|
160
160
|
}
|
|
161
161
|
]
|
|
@@ -171,23 +171,23 @@ export const createComponent: SkillTree = {
|
|
|
171
171
|
"chips": [
|
|
172
172
|
{
|
|
173
173
|
"label": "Shared module",
|
|
174
|
-
"lines": [
|
|
174
|
+
"lines": [209, 225],
|
|
175
175
|
"anchor": "Register the component in `src/registerComponents.ts`, a reg",
|
|
176
176
|
"anchorEnd": "});"
|
|
177
177
|
},
|
|
178
178
|
{
|
|
179
179
|
"label": "Import and name it",
|
|
180
|
-
"lines": [
|
|
180
|
+
"lines": [228, 228],
|
|
181
181
|
"anchor": "Import the module from `src/main.ts`, before `bootLiveTokens"
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
"label": "Contract",
|
|
185
|
-
"lines": [
|
|
185
|
+
"lines": [228, 228],
|
|
186
186
|
"anchor": "Import the module from `src/main.ts`, before `bootLiveTokens"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"label": "First-party",
|
|
190
|
-
"lines": [
|
|
190
|
+
"lines": [232, 232],
|
|
191
191
|
"anchor": "Inside the live-tokens repository, a first-party component k"
|
|
192
192
|
}
|
|
193
193
|
]
|
|
@@ -198,7 +198,7 @@ export const createComponent: SkillTree = {
|
|
|
198
198
|
"kind": "cli",
|
|
199
199
|
"title": "Run check-component",
|
|
200
200
|
"desc": "check-component <id> --tests --strict --json applies every auto repair, runs the contract suites, and returns the fixes beside the remaining findings and coverage by rule.",
|
|
201
|
-
"lines": [
|
|
201
|
+
"lines": [242, 242],
|
|
202
202
|
"anchor": "Run `npx live-tokens check-component <id> --tests --strict -"
|
|
203
203
|
},
|
|
204
204
|
{
|
|
@@ -206,8 +206,8 @@ export const createComponent: SkillTree = {
|
|
|
206
206
|
"row": 7,
|
|
207
207
|
"kind": "gate",
|
|
208
208
|
"title": "Repair from guidance",
|
|
209
|
-
"desc": "Each remaining finding carries its guidance. Make each repair, then run check-component again until coverage is complete.",
|
|
210
|
-
"lines": [
|
|
209
|
+
"desc": "Each remaining finding carries its guidance. Make each repair, judge each a11y-review alert by eye, then run check-component again until coverage is complete.",
|
|
210
|
+
"lines": [243, 243],
|
|
211
211
|
"anchor": "Each remaining finding carries a rule id, a line, and its `g"
|
|
212
212
|
},
|
|
213
213
|
{
|
|
@@ -225,7 +225,7 @@ export const createComponent: SkillTree = {
|
|
|
225
225
|
"kind": "hand",
|
|
226
226
|
"title": "live-tokens-create-page",
|
|
227
227
|
"desc": "The component is done. Placing it on a page is the next skill.",
|
|
228
|
-
"lines": [
|
|
228
|
+
"lines": [249, 249],
|
|
229
229
|
"anchor": "Then place the component on a page with **live-tokens-create"
|
|
230
230
|
}
|
|
231
231
|
],
|
|
@@ -18,21 +18,23 @@
|
|
|
18
18
|
let componentsDisabled = $derived($parentRoute === DEFAULT_COMPONENTS_PATH);
|
|
19
19
|
let colorsDisabled = $derived($parentRoute === DEFAULT_COLORS_PATH);
|
|
20
20
|
|
|
21
|
-
// Editing flow order: Tokens → Components → Sketchstyle → Color Wheel. The condensed
|
|
21
|
+
// Editing flow order: Tokens → Components → Sketchstyle → Color Wheel → Accessibility. The condensed
|
|
22
22
|
// rail cycles through these; a view is skipped while it's disabled (already
|
|
23
23
|
// on that page) so the cycle never lands on a dead view.
|
|
24
|
-
const CYCLE: readonly EditorView[] = ['tokens', 'components', 'sketch', 'colors'];
|
|
24
|
+
const CYCLE: readonly EditorView[] = ['tokens', 'components', 'sketch', 'colors', 'accessibility'];
|
|
25
25
|
const ICONS: Record<EditorView, string> = {
|
|
26
26
|
tokens: 'fa-sliders',
|
|
27
27
|
colors: 'fa-palette',
|
|
28
28
|
components: 'fa-cubes',
|
|
29
29
|
sketch: 'fa-pen-nib',
|
|
30
|
+
accessibility: 'fa-universal-access',
|
|
30
31
|
};
|
|
31
32
|
const LABELS: Record<EditorView, string> = {
|
|
32
33
|
tokens: 'Tokens',
|
|
33
34
|
colors: 'Color Wheel',
|
|
34
35
|
components: 'Components',
|
|
35
36
|
sketch: 'Sketchstyle',
|
|
37
|
+
accessibility: 'Accessibility',
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
function set(v: EditorView) {
|
|
@@ -116,6 +118,17 @@
|
|
|
116
118
|
<span class="radio" aria-hidden="true"></span>
|
|
117
119
|
<span>Color Wheel</span>
|
|
118
120
|
</button>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
role="tab"
|
|
124
|
+
class="seg-btn"
|
|
125
|
+
class:active={$editorView === 'accessibility'}
|
|
126
|
+
aria-selected={$editorView === 'accessibility'}
|
|
127
|
+
onclick={() => set('accessibility')}
|
|
128
|
+
>
|
|
129
|
+
<span class="radio" aria-hidden="true"></span>
|
|
130
|
+
<span>Accessibility</span>
|
|
131
|
+
</button>
|
|
119
132
|
</div>
|
|
120
133
|
</div>
|
|
121
134
|
{/if}
|