@motion-proto/live-tokens 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,12 +7,13 @@ A foundational design system for quickly styling and building Svelte + Vite micr
7
7
  ## What you get
8
8
 
9
9
  - **Real-time token editing.** Pick a color, drag a hue slider, retype a font size — the page repaints on every input event via CSS-variable writes. No reload, no save-and-refresh, no build step. Works across colors, typography, spacing, radii, shadows, motion, palettes, and gradients.
10
- - **Real-time component editing.** Each of ~19 shipped Svelte components (Button, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, Tabs, SegmentedControl, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, and more) declares its own design-token aliases in a `:global(:root)` block. Rewire any alias from a per-component picker and see that component update everywhere it's used — live, on your real pages, not in a Storybook sandbox.
10
+ - **Real-time component editing.** Each of ~24 shipped Svelte components (Button, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, CodeSnippet, SideNavigation, and more) declares its own design-token aliases in a `:global(:root)` block. Rewire any alias from a per-component picker and see that component update everywhere it's used — live, on your real pages, not in a Storybook sandbox.
11
11
  - **Theme editor** (`/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a static `tokens.css` for the build.
12
12
  - **Per-component editor** (`/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
13
13
  - **Live editor overlay** — pins to the top-right of every dev page. Opens the editor in a side panel or floating window so you edit *on the page you're styling*, not in a separate tab. Includes a "Page Source" button that opens the current page's `.svelte` file in VS Code.
14
14
  - **Preset bundles** — capture a whole site configuration (active theme + every component's active config) as a single portable artifact. Drop a preset into a new project to restore the full styling in one step.
15
15
  - **Vite plugin** — hosts the `/api/live-tokens/{themes,component-configs,manifests}/*` routes that persist your edits to disk as you make them. The single namespace keeps live-tokens' routes from colliding with anything your app serves under `/api`.
16
+ - **Claude Code skill suite** — three bundled skills so you can drive the package in plain English. `build-page` composes pages from the shipped components. `pick-component` decides between confusing pairs (TabBar vs SegmentedControl, Card vs CollapsibleSection). `create-component` authors a new editable component against the project's naming, state-model, and import rules. One command to install all three: `npx @motion-proto/live-tokens setup-claude`. See [Claude Code skills](#claude-code-skills) below.
16
17
 
17
18
  ## Quick install
18
19
 
@@ -236,16 +237,39 @@ registerComponent({
236
237
 
237
238
  The component appears in the `/components` page under a **CUSTOM** group in the nav rail. Token rows, linked-block sharing, per-component config persistence, and reset-to-default work identically to the built-in set. All imports must come from `@motion-proto/live-tokens` or `@motion-proto/live-tokens/component-editor`; never deep-import from `src/`.
238
239
 
239
- ### Skill (Claude-assisted authoring)
240
+ ## Claude Code skills
240
241
 
241
- The package bundles a Claude Code skill at `node_modules/@motion-proto/live-tokens/.claude/skills/live-tokens-add-component/`. It teaches the token-naming conventions, state model, editor patterns, and the public-imports rule. To make it active in your project, copy or symlink it into your `.claude/skills/` directory:
242
+ The package ships a suite of Claude Code skills that encode the project's conventions so Claude can drive the package in plain English. They cover the three jobs the README itself can't carry well: deciding which shipped component fits a need, composing a page from the catalogue, and (for the long-tail case) authoring a new editable component. Each skill auto-triggers from natural-language requests no slash commands. (Plain `npm install` plus the README handle first-time setup.)
243
+
244
+ | Skill | Triggers on | What it knows |
245
+ |--------------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
246
+ | `live-tokens-build-page` | "build a pricing page using live-tokens components" | shipped-component catalogue, column grid, `pageSources` registration, token-only styling rule |
247
+ | `live-tokens-pick-component` | "what's the difference between TabBar and SegmentedControl?" | decision tables for each confusable family (selection, container, messaging, on/off); when to author a new one instead |
248
+ | `live-tokens-create-component` | "author a new Toggle component for my live-tokens project" | runtime + editor + `registerComponent()` recipe, naming scheme, state model, public-imports rule, verification checklist |
249
+
250
+ ### Install
251
+
252
+ ```bash
253
+ npx @motion-proto/live-tokens setup-claude
254
+ ```
255
+
256
+ Copies every bundled skill into `./.claude/skills/` in the current directory. Re-run after upgrading the package to pick up new or updated skills (pass `--force` to overwrite). macOS/Linux only.
257
+
258
+ If you'd rather avoid the CLI, the equivalent one-liner:
259
+
260
+ ```bash
261
+ mkdir -p .claude/skills && cp -R node_modules/@motion-proto/live-tokens/.claude/skills/. .claude/skills/
262
+ ```
263
+
264
+ ### Validate authored components
265
+
266
+ The same CLI ships a static validator that turns the `create-component` skill's verification checklist into a runnable command:
242
267
 
243
268
  ```bash
244
- mkdir -p .claude/skills
245
- ln -s ../../node_modules/@motion-proto/live-tokens/.claude/skills/live-tokens-add-component .claude/skills/
269
+ npx @motion-proto/live-tokens check-component <id>
246
270
  ```
247
271
 
248
- Once linked, asking Claude to "add a Stat component to my live-tokens project" triggers the skill, which walks the runtime file, editor file, and registration step.
272
+ It enforces the file layout, `:global(:root)` block, token-suffix vocabulary, the state-before-property rule, the no-raw-colour-defaults rule, the public-imports rule, and the `registerComponent({ id })` call. Exit code 0 means the static contract is met. Useful both as a self-check after Claude generates a component and as a pre-commit guard on human-authored ones.
249
273
 
250
274
  ## How the editor ships changes to prod
251
275
 
@@ -0,0 +1,244 @@
1
+ // Static validator for a live-tokens component.
2
+ //
3
+ // Asserts that an authored component (runtime + editor + registration) satisfies
4
+ // the contract described in the live-tokens-create-component skill:
5
+ //
6
+ // - runtime file at src/system/components/<Id>.svelte with :global(:root) block
7
+ // - editor file at src/system/components/<Id>Editor.svelte exporting `component` + `allTokens`
8
+ // - registerComponent({ id: '<id>', ... }) call somewhere in src/
9
+ // - all imports in the three files use public live-tokens paths only
10
+ // - token names match --<id>-<part>[-<state>][-<element>]-<property>
11
+ // with the property being one of the recognised suffixes,
12
+ // and state coming before property (never after)
13
+ // - :global(:root) defaults reference theme tokens (no raw colour literals)
14
+ //
15
+ // Returns { errors: string[], warnings: string[] }.
16
+
17
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
18
+ import { extname, join, relative } from 'node:path';
19
+
20
+ // Property suffixes the editor picker recognises (KIND_PATTERNS in the editor).
21
+ // Keep in sync with the skill's suffix vocabulary.
22
+ const KNOWN_SUFFIXES = [
23
+ 'surface', 'border', 'text', 'icon', 'label', 'fill',
24
+ 'radius', 'border-width', 'font-family', 'font-weight',
25
+ 'font-size', 'line-height', 'letter-spacing', 'padding',
26
+ 'thickness', 'width', 'color', 'size', 'gap', 'opacity',
27
+ 'shadow', 'blur', 'divider',
28
+ ];
29
+
30
+ // State tokens that must come *before* the property, never after.
31
+ const STATE_TOKENS = ['hover', 'disabled', 'selected', 'focus', 'active', 'focused'];
32
+
33
+ // Deep imports into the package internals are not a supported API.
34
+ const DEEP_IMPORT_PATTERNS = [
35
+ /^@motion-proto\/live-tokens\/src\//,
36
+ /node_modules\/@motion-proto\/live-tokens/,
37
+ ];
38
+
39
+ function capitalize(id) {
40
+ return id.charAt(0).toUpperCase() + id.slice(1);
41
+ }
42
+
43
+ function extractImports(source) {
44
+ const out = [];
45
+ const re = /import\s+(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/g;
46
+ let m;
47
+ while ((m = re.exec(source)) !== null) {
48
+ out.push(m[1]);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ function extractGlobalRootBlocks(source) {
54
+ const blocks = [];
55
+ const re = /:global\(:root\)\s*\{([^}]*)\}/g;
56
+ let m;
57
+ while ((m = re.exec(source)) !== null) {
58
+ blocks.push(m[1]);
59
+ }
60
+ return blocks;
61
+ }
62
+
63
+ function extractTokensForId(blocks, id) {
64
+ const tokens = new Set();
65
+ const re = new RegExp(`--${id}-[a-z0-9-]+`, 'g');
66
+ for (const block of blocks) {
67
+ const matches = block.match(re) ?? [];
68
+ for (const t of matches) tokens.add(t);
69
+ }
70
+ return [...tokens];
71
+ }
72
+
73
+ function tokenSuffix(token) {
74
+ for (const suffix of KNOWN_SUFFIXES) {
75
+ if (token.endsWith(`-${suffix}`)) return suffix;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ function detectStateAfterProperty(token) {
81
+ // e.g. --comp-part-surface-hover (wrong) vs --comp-part-hover-surface (right)
82
+ for (const state of STATE_TOKENS) {
83
+ if (token.endsWith(`-${state}`)) {
84
+ const head = token.slice(0, -(state.length + 1));
85
+ if (tokenSuffix(head)) return state;
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+
91
+ function findFilesRecursive(dir, exts) {
92
+ if (!existsSync(dir)) return [];
93
+ const out = [];
94
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
95
+ if (ent.name === 'node_modules' || ent.name.startsWith('.')) continue;
96
+ const full = join(dir, ent.name);
97
+ if (ent.isDirectory()) {
98
+ out.push(...findFilesRecursive(full, exts));
99
+ } else if (exts.includes(extname(ent.name))) {
100
+ out.push(full);
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+
106
+ export function checkComponent(id, root = process.cwd()) {
107
+ const errors = [];
108
+ const warnings = [];
109
+
110
+ if (!/^[a-z][a-z0-9]*$/.test(id)) {
111
+ errors.push(`id "${id}" is invalid; must be lowercase letters/digits, no dashes`);
112
+ return { errors, warnings };
113
+ }
114
+
115
+ const Id = capitalize(id);
116
+ const runtimePath = join(root, 'src/system/components', `${Id}.svelte`);
117
+ const editorPath = join(root, 'src/system/components', `${Id}Editor.svelte`);
118
+
119
+ if (!existsSync(runtimePath)) {
120
+ errors.push(`runtime missing: ${relative(root, runtimePath)}`);
121
+ }
122
+ if (!existsSync(editorPath)) {
123
+ errors.push(`editor missing: ${relative(root, editorPath)}`);
124
+ }
125
+ if (errors.length) return { errors, warnings };
126
+
127
+ const runtime = readFileSync(runtimePath, 'utf8');
128
+ const editor = readFileSync(editorPath, 'utf8');
129
+
130
+ // Runtime: :global(:root) block present.
131
+ const blocks = extractGlobalRootBlocks(runtime);
132
+ if (blocks.length === 0) {
133
+ errors.push(`${relative(root, runtimePath)}: missing :global(:root) declaration block`);
134
+ }
135
+
136
+ // Runtime: at least one --<id>-* token.
137
+ const tokens = extractTokensForId(blocks, id);
138
+ if (blocks.length > 0 && tokens.length === 0) {
139
+ errors.push(`${relative(root, runtimePath)}: no --${id}-* tokens declared in :global(:root)`);
140
+ }
141
+
142
+ // Runtime: state-after-property anti-pattern. Report this first; if it fires
143
+ // for a token, skip the unknown-suffix error for the same token (the state-
144
+ // suffix wouldn't be in the suffix list anyway, so it's the same root cause).
145
+ const stateAfterTokens = new Set();
146
+ for (const token of tokens) {
147
+ const trailingState = detectStateAfterProperty(token);
148
+ if (trailingState) {
149
+ stateAfterTokens.add(token);
150
+ errors.push(
151
+ `${relative(root, runtimePath)}: ${token} has '${trailingState}' after the property; ` +
152
+ `state must come before property (e.g. -${trailingState}-surface, not -surface-${trailingState})`,
153
+ );
154
+ }
155
+ }
156
+
157
+ // Runtime: every token ends in a known suffix.
158
+ for (const token of tokens) {
159
+ if (stateAfterTokens.has(token)) continue;
160
+ if (!tokenSuffix(token)) {
161
+ errors.push(`${relative(root, runtimePath)}: ${token} doesn't end in a known suffix`);
162
+ }
163
+ }
164
+
165
+ // Runtime: defaults inside :global(:root) reference theme tokens, not raw colours.
166
+ for (const block of blocks) {
167
+ const rawColours = block.match(/:\s*#[0-9a-fA-F]{3,8}\b/g) ?? [];
168
+ if (rawColours.length > 0) {
169
+ errors.push(
170
+ `${relative(root, runtimePath)}: :global(:root) contains ${rawColours.length} raw colour literal(s); ` +
171
+ `defaults must reference theme tokens (e.g. var(--surface-primary))`,
172
+ );
173
+ }
174
+ }
175
+
176
+ // Editor: declares `const component = '<id>'` (module block).
177
+ const componentDecl = new RegExp(`\\bconst\\s+component\\s*=\\s*['"]${id}['"]`);
178
+ if (!componentDecl.test(editor)) {
179
+ errors.push(`${relative(root, editorPath)}: missing 'const component = "${id}"' in <script module>`);
180
+ }
181
+
182
+ // Editor: exports allTokens.
183
+ if (!/\bexport\s+const\s+allTokens\b/.test(editor)) {
184
+ errors.push(`${relative(root, editorPath)}: missing 'export const allTokens'`);
185
+ }
186
+
187
+ // Imports across runtime + editor: reject deep imports into the package.
188
+ for (const [path, source] of [[runtimePath, runtime], [editorPath, editor]]) {
189
+ for (const imp of extractImports(source)) {
190
+ for (const pattern of DEEP_IMPORT_PATTERNS) {
191
+ if (pattern.test(imp)) {
192
+ errors.push(`${relative(root, path)}: deep import not supported: ${imp}`);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ // Registration: registerComponent({ id: '<id>', ... }) somewhere under src/.
199
+ const srcFiles = findFilesRecursive(join(root, 'src'), ['.ts', '.js', '.svelte', '.mjs']);
200
+ const regPattern = new RegExp(`registerComponent\\s*\\(\\s*\\{[^}]*id\\s*:\\s*['"]${id}['"]`, 's');
201
+ let registrationFile = null;
202
+ for (const file of srcFiles) {
203
+ try {
204
+ if (regPattern.test(readFileSync(file, 'utf8'))) {
205
+ registrationFile = file;
206
+ break;
207
+ }
208
+ } catch {
209
+ // ignore unreadable files
210
+ }
211
+ }
212
+ if (!registrationFile) {
213
+ errors.push(`registerComponent({ id: '${id}', ... }) not found anywhere under src/`);
214
+ } else {
215
+ // Check the registration file's imports too.
216
+ const regSource = readFileSync(registrationFile, 'utf8');
217
+ for (const imp of extractImports(regSource)) {
218
+ for (const pattern of DEEP_IMPORT_PATTERNS) {
219
+ if (pattern.test(imp)) {
220
+ errors.push(`${relative(root, registrationFile)}: deep import not supported: ${imp}`);
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ return { errors, warnings };
227
+ }
228
+
229
+ export function formatReport(id, result) {
230
+ const lines = [];
231
+ if (result.errors.length === 0 && result.warnings.length === 0) {
232
+ lines.push(`✓ ${id}: passes the live-tokens-create-component contract.`);
233
+ } else {
234
+ if (result.errors.length > 0) {
235
+ lines.push(`✗ ${id}: ${result.errors.length} error(s)`);
236
+ for (const e of result.errors) lines.push(` - ${e}`);
237
+ }
238
+ if (result.warnings.length > 0) {
239
+ lines.push(`! ${id}: ${result.warnings.length} warning(s)`);
240
+ for (const w of result.warnings) lines.push(` - ${w}`);
241
+ }
242
+ }
243
+ return lines.join('\n');
244
+ }
package/bin/cli.mjs ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // CLI for @motion-proto/live-tokens.
3
+ // Subcommands:
4
+ // setup-claude [--force] Copy bundled Claude Code skills into ./.claude/skills/.
5
+ // check-component <id> Validate a component against the add-component skill contract.
6
+
7
+ import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import process from 'node:process';
11
+ import { checkComponent, formatReport } from './check-component.mjs';
12
+
13
+ const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
14
+
15
+ Commands:
16
+ setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
17
+ check-component <id> Validate <id>'s runtime, editor, and registration
18
+ against the live-tokens-create-component contract
19
+ `;
20
+
21
+ function fail(message, code = 1) {
22
+ console.error(message);
23
+ process.exit(code);
24
+ }
25
+
26
+ const [, , command, ...rest] = process.argv;
27
+
28
+ if (!command || command === '--help' || command === '-h') {
29
+ console.log(USAGE);
30
+ process.exit(0);
31
+ }
32
+
33
+ if (command === 'check-component') {
34
+ const id = rest[0];
35
+ if (!id) fail(`Usage: npx @motion-proto/live-tokens check-component <id>`);
36
+ const result = checkComponent(id);
37
+ console.log(formatReport(id, result));
38
+ process.exit(result.errors.length === 0 ? 0 : 1);
39
+ }
40
+
41
+ if (command !== 'setup-claude') {
42
+ fail(`Unknown command: ${command}\n\n${USAGE}`);
43
+ }
44
+
45
+ if (process.platform === 'win32') {
46
+ fail('setup-claude is macOS/Linux only.');
47
+ }
48
+
49
+ const force = rest.includes('--force');
50
+
51
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
52
+ const srcSkills = join(pkgRoot, '.claude', 'skills');
53
+
54
+ if (!existsSync(srcSkills)) {
55
+ fail(`No bundled skills found at ${srcSkills}. Is the package installed correctly?`);
56
+ }
57
+
58
+ const skills = readdirSync(srcSkills).filter((name) =>
59
+ statSync(join(srcSkills, name)).isDirectory(),
60
+ );
61
+
62
+ if (skills.length === 0) {
63
+ fail('No bundled skills to install.');
64
+ }
65
+
66
+ const destSkills = join(process.cwd(), '.claude', 'skills');
67
+ mkdirSync(destSkills, { recursive: true });
68
+
69
+ let installed = 0;
70
+ let skipped = 0;
71
+ for (const skill of skills) {
72
+ const src = join(srcSkills, skill);
73
+ const dest = join(destSkills, skill);
74
+ if (existsSync(dest) && !force) {
75
+ console.log(` skip ${skill} (already exists; pass --force to overwrite)`);
76
+ skipped++;
77
+ continue;
78
+ }
79
+ cpSync(src, dest, { recursive: true });
80
+ console.log(` ok ${skill}`);
81
+ installed++;
82
+ }
83
+
84
+ console.log(`\n${installed} installed, ${skipped} skipped → ${destSkills}`);
85
+
86
+ const SAMPLE_PROMPTS = {
87
+ 'live-tokens-build-page': 'build a pricing page using live-tokens components',
88
+ 'live-tokens-pick-component': "what's the difference between TabBar and SegmentedControl?",
89
+ 'live-tokens-create-component': 'author a new Toggle component for my live-tokens project',
90
+ };
91
+
92
+ const installedSamples = skills
93
+ .map((s) => SAMPLE_PROMPTS[s] && [s, SAMPLE_PROMPTS[s]])
94
+ .filter(Boolean);
95
+
96
+ if (installedSamples.length > 0) {
97
+ console.log(`\nIn Claude Code, prompts like these auto-trigger the matching skill:`);
98
+ for (const [skill, prompt] of installedSamples) {
99
+ console.log(` • "${prompt}"\n → ${skill}`);
100
+ }
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 6/7.",
6
6
  "keywords": [
@@ -24,12 +24,16 @@
24
24
  "src/app/site.css",
25
25
  "dist-plugin",
26
26
  ".claude/skills",
27
+ "bin",
27
28
  "!**/*.test.ts",
28
29
  "!**/*.spec.ts",
29
30
  "!**/__tests__/**",
30
31
  "!src/system/components/Stat.svelte",
31
32
  "!src/system/components/StatEditor.svelte"
32
33
  ],
34
+ "bin": {
35
+ "live-tokens": "./bin/cli.mjs"
36
+ },
33
37
  "exports": {
34
38
  ".": {
35
39
  "svelte": "./src/editor/index.ts",
@@ -0,0 +1,61 @@
1
+ <script module lang="ts">
2
+ import type { Token } from './scaffolding/types';
3
+
4
+ export const component = 'codesnippet';
5
+
6
+ // Single variant. Default carries every container, code, and icon token;
7
+ // hover layers a single icon-color override on top.
8
+ const states: Record<string, Token[]> = {
9
+ default: [
10
+ { label: 'surface', variable: '--codesnippet-surface' },
11
+ { label: 'border', variable: '--codesnippet-border' },
12
+ { label: 'border width', variable: '--codesnippet-border-width' },
13
+ { label: 'corner radius', variable: '--codesnippet-radius' },
14
+ { label: 'padding', variable: '--codesnippet-padding' },
15
+ { label: 'gap', variable: '--codesnippet-gap' },
16
+ { label: 'code text', variable: '--codesnippet-code-text' },
17
+ { label: 'code font family', variable: '--codesnippet-code-font-family' },
18
+ { label: 'code font size', variable: '--codesnippet-code-font-size' },
19
+ { label: 'code font weight', variable: '--codesnippet-code-font-weight' },
20
+ { label: 'code line height', variable: '--codesnippet-code-line-height' },
21
+ { label: 'icon color', variable: '--codesnippet-icon' },
22
+ { label: 'icon size', variable: '--codesnippet-icon-size' },
23
+ ],
24
+ hover: [
25
+ { label: 'icon color', variable: '--codesnippet-hover-icon' },
26
+ ],
27
+ };
28
+
29
+ export const allTokens: Token[] = Object.values(states).flat();
30
+ </script>
31
+
32
+ <script lang="ts">
33
+ import CodeSnippet from '../../system/components/CodeSnippet.svelte';
34
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
35
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
36
+ </script>
37
+
38
+ <ComponentEditorBase
39
+ {component}
40
+ title="Code Snippet"
41
+ description="Single-line code display with a copy button. Shows a brief confirmation popover on copy."
42
+ tokens={allTokens}
43
+ >
44
+ <VariantGroup name="codesnippet" title="Code Snippet" {states} {component}>
45
+ {#snippet children({ activeState })}
46
+ <div class="codesnippet-preview">
47
+ <CodeSnippet
48
+ code="npx @motion-proto/live-tokens setup-claude"
49
+ class={activeState === 'hover' ? 'force-hover' : ''}
50
+ />
51
+ </div>
52
+ {/snippet}
53
+ </VariantGroup>
54
+ </ComponentEditorBase>
55
+
56
+ <style>
57
+ .codesnippet-preview {
58
+ display: flex;
59
+ padding: var(--ui-space-16);
60
+ }
61
+ </style>
@@ -0,0 +1,93 @@
1
+ <script module lang="ts">
2
+ import type { Token } from './scaffolding/types';
3
+
4
+ export const component = 'toggle';
5
+
6
+ // Single variant: the default carries the off baseline plus all geometry and
7
+ // label typography. The other states layer hover, on, on+hover, and disabled
8
+ // on top of that baseline.
9
+ const states: Record<string, Token[]> = {
10
+ default: [
11
+ { label: 'track surface', variable: '--toggle-track-surface' },
12
+ { label: 'track border', variable: '--toggle-track-border' },
13
+ { label: 'track border width',variable: '--toggle-track-border-width' },
14
+ { label: 'track radius', variable: '--toggle-track-radius' },
15
+ { label: 'track width', variable: '--toggle-track-width' },
16
+ { label: 'track thickness', variable: '--toggle-track-thickness' },
17
+ { label: 'thumb surface', variable: '--toggle-thumb-surface' },
18
+ { label: 'thumb border', variable: '--toggle-thumb-border' },
19
+ { label: 'thumb size', variable: '--toggle-thumb-size' },
20
+ { label: 'label text', variable: '--toggle-label-text' },
21
+ { label: 'label font family', variable: '--toggle-label-font-family' },
22
+ { label: 'label font size', variable: '--toggle-label-font-size' },
23
+ { label: 'label font weight', variable: '--toggle-label-font-weight' },
24
+ { label: 'label gap', variable: '--toggle-gap' },
25
+ ],
26
+ hover: [
27
+ { label: 'track surface', variable: '--toggle-hover-track-surface' },
28
+ { label: 'thumb surface', variable: '--toggle-hover-thumb-surface' },
29
+ ],
30
+ on: [
31
+ { label: 'track surface', variable: '--toggle-on-track-surface' },
32
+ { label: 'track border', variable: '--toggle-on-track-border' },
33
+ { label: 'thumb surface', variable: '--toggle-on-thumb-surface' },
34
+ { label: 'thumb border', variable: '--toggle-on-thumb-border' },
35
+ ],
36
+ 'on hover': [
37
+ { label: 'track surface', variable: '--toggle-on-hover-track-surface' },
38
+ { label: 'thumb surface', variable: '--toggle-on-hover-thumb-surface' },
39
+ ],
40
+ disabled: [
41
+ { label: 'track surface', variable: '--toggle-disabled-track-surface' },
42
+ { label: 'thumb surface', variable: '--toggle-disabled-thumb-surface' },
43
+ { label: 'label text', variable: '--toggle-disabled-label-text' },
44
+ ],
45
+ };
46
+
47
+ export const allTokens: Token[] = Object.values(states).flat();
48
+ </script>
49
+
50
+ <script lang="ts">
51
+ import Toggle from '../../system/components/Toggle.svelte';
52
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
53
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
54
+
55
+ // Preview reflects the state being edited: "on" / "on hover" show checked;
56
+ // hover variants apply force-hover; disabled is terminal.
57
+ function previewProps(activeState: string) {
58
+ return {
59
+ checked: activeState === 'on' || activeState === 'on hover',
60
+ disabled: activeState === 'disabled',
61
+ forceClass: activeState === 'hover' || activeState === 'on hover' ? 'force-hover' : '',
62
+ };
63
+ }
64
+ </script>
65
+
66
+ <ComponentEditorBase
67
+ {component}
68
+ title="Toggle"
69
+ description="On/off switch with a sliding thumb. The default state is off; on is a state with its own track and thumb colors."
70
+ tokens={allTokens}
71
+ >
72
+ <VariantGroup
73
+ name="toggle"
74
+ title="Toggle"
75
+ {states}
76
+ {component}
77
+ >
78
+ {#snippet children({ activeState })}
79
+ {@const p = previewProps(activeState)}
80
+ <div class="toggle-preview">
81
+ <Toggle checked={p.checked} disabled={p.disabled} class={p.forceClass} label="Enable feature" />
82
+ </div>
83
+ {/snippet}
84
+ </VariantGroup>
85
+ </ComponentEditorBase>
86
+
87
+ <style>
88
+ .toggle-preview {
89
+ display: flex;
90
+ gap: var(--ui-space-16);
91
+ padding: var(--ui-space-16);
92
+ }
93
+ </style>
@@ -7,6 +7,7 @@ import CalloutEditor, { allTokens as calloutTokens } from './CalloutEditor.svelt
7
7
  import CornerBadgeEditor, { allTokens as cornerBadgeTokens } from './CornerBadgeEditor.svelte';
8
8
  import ButtonEditor, { allTokens as buttonTokens } from './ButtonEditor.svelte';
9
9
  import CardEditor, { allTokens as cardTokens } from './CardEditor.svelte';
10
+ import CodeSnippetEditor, { allTokens as codeSnippetTokens } from './CodeSnippetEditor.svelte';
10
11
  import CollapsibleSectionEditor, { allTokens as collapsibleSectionTokens } from './CollapsibleSectionEditor.svelte';
11
12
  import DialogEditor, { allTokens as dialogTokens } from './DialogEditor.svelte';
12
13
  import ImageEditor, { allTokens as imageTokens } from './ImageEditor.svelte';
@@ -22,6 +23,7 @@ import SegmentedControlEditor, { allTokens as segmentedControlTokens } from './S
22
23
  import SideNavigationEditor, { allTokens as sideNavigationTokens } from './SideNavigationEditor.svelte';
23
24
  import TableEditor, { allTokens as tableTokens } from './TableEditor.svelte';
24
25
  import TabBarEditor, { allTokens as tabBarTokens } from './TabBarEditor.svelte';
26
+ import ToggleEditor, { allTokens as toggleTokens } from './ToggleEditor.svelte';
25
27
  import TooltipEditor, { allTokens as tooltipTokens } from './TooltipEditor.svelte';
26
28
 
27
29
  /** Internal narrowed union of the first-party component ids. Not exposed publicly. */
@@ -34,6 +36,7 @@ type BuiltInComponentId =
34
36
  | 'card'
35
37
  | 'badge'
36
38
  | 'callout'
39
+ | 'codesnippet'
37
40
  | 'cornerbadge'
38
41
  | 'image'
39
42
  | 'imagelightbox'
@@ -45,6 +48,7 @@ type BuiltInComponentId =
45
48
  | 'sidenavigation'
46
49
  | 'table'
47
50
  | 'tabbar'
51
+ | 'toggle'
48
52
  | 'tooltip'
49
53
  | 'progressbar';
50
54
 
@@ -152,6 +156,15 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
152
156
  schema: calloutTokens,
153
157
  origin: 'system',
154
158
  },
159
+ codesnippet: {
160
+ id: 'codesnippet',
161
+ label: 'Code Snippet',
162
+ icon: 'fas fa-code',
163
+ sourceFile: 'src/system/components/CodeSnippet.svelte',
164
+ editorComponent: CodeSnippetEditor,
165
+ schema: codeSnippetTokens,
166
+ origin: 'system',
167
+ },
155
168
  cornerbadge: {
156
169
  id: 'cornerbadge',
157
170
  label: 'Corner Badge',
@@ -251,6 +264,15 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
251
264
  schema: tabBarTokens,
252
265
  origin: 'system',
253
266
  },
267
+ toggle: {
268
+ id: 'toggle',
269
+ label: 'Toggle',
270
+ icon: 'fas fa-toggle-on',
271
+ sourceFile: 'src/system/components/Toggle.svelte',
272
+ editorComponent: ToggleEditor,
273
+ schema: toggleTokens,
274
+ origin: 'system',
275
+ },
254
276
  tooltip: {
255
277
  id: 'tooltip',
256
278
  label: 'Tooltip',