@motion-proto/live-tokens 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
- package/README.md +84 -29
- package/dist-plugin/index.cjs +177 -125
- package/dist-plugin/index.d.cts +3 -2
- package/dist-plugin/index.d.ts +3 -2
- package/dist-plugin/index.js +177 -125
- package/package.json +8 -2
- package/src/editor/component-editor/BadgeEditor.svelte +44 -42
- package/src/editor/component-editor/ButtonEditor.svelte +224 -0
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
- package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
- package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
- package/src/editor/component-editor/InputEditor.svelte +272 -0
- package/src/editor/component-editor/NotificationEditor.svelte +44 -65
- package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
- package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
- package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
- package/src/editor/component-editor/index.ts +16 -1
- package/src/editor/component-editor/registry.ts +138 -28
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
- package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
- package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
- package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
- package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
- package/src/editor/component-editor/scaffolding/types.ts +2 -1
- package/src/editor/core/components/componentConfigKeys.ts +14 -3
- package/src/editor/core/components/componentConfigService.ts +7 -6
- package/src/editor/core/manifests/manifestService.ts +5 -4
- package/src/editor/core/storage/apiBase.ts +15 -0
- package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
- package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
- package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
- package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
- package/src/editor/core/themes/migrations/index.ts +10 -0
- package/src/editor/core/themes/slices/components.ts +9 -0
- package/src/editor/core/themes/themeInit.ts +3 -2
- package/src/editor/core/themes/themeService.ts +3 -2
- package/src/editor/index.ts +10 -1
- package/src/editor/pages/ComponentEditorPage.svelte +53 -3
- package/src/editor/pages/EditorShell.svelte +53 -3
- package/src/editor/ui/UIEasingSelector.svelte +240 -0
- package/src/editor/ui/variantScales.ts +34 -0
- package/src/system/components/Button.svelte +34 -85
- package/src/system/components/CollapsibleSection.svelte +1 -48
- package/src/system/components/CornerBadge.svelte +72 -138
- package/src/system/components/Dialog.svelte +24 -4
- package/src/system/components/ImageLightbox.svelte +578 -0
- package/src/system/components/Input.svelte +387 -0
- package/src/system/components/ProgressBar.svelte +62 -258
- package/src/system/components/SectionDivider.svelte +117 -43
- package/src/system/components/SegmentedControl.svelte +81 -15
- package/src/system/components/SideNavigation.svelte +777 -0
- package/src/system/styles/tokens.css +43 -0
- package/src/system/styles/tokens.generated.css +4 -183
- package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-tokens-add-component
|
|
3
|
+
description: Author a new component for a project that uses @motion-proto/live-tokens. Covers the runtime Svelte file, the editor Svelte file, registerComponent() wiring, public-imports rule, naming conventions, state model, and verification. Use when the user asks to add a component to a live-tokens project, make a Svelte component editable in the live-tokens editor, extend the live-tokens system with a new tokenized component, or create a tokenized [Thing] component.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoring a component for a live-tokens project
|
|
7
|
+
|
|
8
|
+
This skill teaches how to add a new editable component to a project that consumes `@motion-proto/live-tokens`. The end state: a runtime Svelte component, an editor Svelte component, one `registerComponent()` call, and a `/components` page entry under the **CUSTOM** group with full token editing, linked-block sharing, and persistence.
|
|
9
|
+
|
|
10
|
+
The skill has two pillars. Read both before writing any code.
|
|
11
|
+
|
|
12
|
+
## Pillar 1 — Token discipline
|
|
13
|
+
|
|
14
|
+
Every editable property is a CSS custom property declared in `:global(:root)` inside the runtime component's `<style>` block. Defaults reference theme tokens; never raw values. The token surface is *meaningful*: one row per state, per part, per property. The dev plugin parses `:global(:root)` to seed `component-configs/<id>/default.json` on first save. Variables not declared in `:global(:root)` cannot be edited.
|
|
15
|
+
|
|
16
|
+
### Naming scheme
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
--<componentId>-<part>[-<state>][-<element>]-<property>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **`componentId`** — the literal id passed to `registerComponent()`. Lowercase, no dashes, no abbreviations. The component file id matches: `MyWidget.svelte` → id `mywidget`.
|
|
23
|
+
- **`part`** — which sub-region of the component. Examples: `bar`, `option`, `selected`, `track`, `header`, `body`, `footer`, `overlay`, `value`, `label`.
|
|
24
|
+
- **`state`** — optional interaction or component state. State comes **before** the property, never after. Examples: `hover`, `disabled`, `selected`, `focus`.
|
|
25
|
+
- **`element`** — optional sub-element within a part. Examples: `dot`, `icon`, `label`, `text`.
|
|
26
|
+
- **`property`** — always last. Either a theme role (`surface`, `border`, `text`, `icon`, `label`, `fill`) or a CSS property name (`radius`, `border-width`, `padding`, `font-family`, `font-weight`, `font-size`).
|
|
27
|
+
|
|
28
|
+
### No abbreviations
|
|
29
|
+
|
|
30
|
+
- `bg` → `surface`. `fg` → `text`. Component ids are never abbreviated: `segmentedcontrol` not `sc`, `mywidget` not `mw`.
|
|
31
|
+
|
|
32
|
+
### Property suffix vocabulary
|
|
33
|
+
|
|
34
|
+
| Suffix | Meaning |
|
|
35
|
+
|----------------|--------------------------------------------------------------|
|
|
36
|
+
| `-surface` | Fill / background color |
|
|
37
|
+
| `-border` | Border color |
|
|
38
|
+
| `-text` | Text color |
|
|
39
|
+
| `-icon` | Icon color |
|
|
40
|
+
| `-label` | Label text color |
|
|
41
|
+
| `-fill` | Inner fill (distinct from outer surface) |
|
|
42
|
+
| `-radius` | Corner radius |
|
|
43
|
+
| `-border-width`| Stroke thickness |
|
|
44
|
+
| `-font-family` | Font family reference |
|
|
45
|
+
| `-font-weight` | Font weight reference |
|
|
46
|
+
| `-font-size` | Font size reference |
|
|
47
|
+
| `-thickness` | Alternative to `-width` when fallback siblings would collide |
|
|
48
|
+
|
|
49
|
+
### State order matters
|
|
50
|
+
|
|
51
|
+
State comes **before** the property:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
--mywidget-button-hover-surface ✓ state before property; siblings on `-surface`
|
|
55
|
+
--mywidget-button-surface-hover ✗ breaks sibling matching and reads oddly
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Default values reference theme tokens
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
--mywidget-primary-surface: var(--surface-primary); ✓
|
|
62
|
+
--mywidget-primary-surface: #6a4ce8; ✗ raw value, can't be re-pointed
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Text token aliases use the neutral text scale by default: `--text-primary`, `--text-secondary`, `--text-tertiary`, `--text-muted`, `--text-disabled`. Family-tinted text is `--text-primary-color`, `--text-accent`, `--text-success`, etc. There is no `--text-neutral` — neutral text is `--text-primary`.
|
|
66
|
+
|
|
67
|
+
### Linked siblings (the link toggle)
|
|
68
|
+
|
|
69
|
+
Tokens that share a `groupKey` form a sibling set. Declaring `canBeLinked: true` plus a `groupKey` in the editor's token list creates a link toggle that broadcasts one value across every sibling. Linkage is **dev-declared**: you author it when you build the component; users opt out of an existing link per-property, never add or reshape links. Do not expose UI for users to add siblings or mark properties as linkable.
|
|
70
|
+
|
|
71
|
+
For typography on multi-slot components, the `groupKey` must include the slot prefix:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
groupKey: 'value-font-family', 'label-font-family' ✓ separate link trees per slot
|
|
75
|
+
groupKey: 'font-family' ✗ silently links value and label together
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Single-slot components can use a bare typography `groupKey` (`font-family`). Add the slot prefix the moment a second typography slot appears.
|
|
79
|
+
|
|
80
|
+
## Pillar 2 — Editor patterns
|
|
81
|
+
|
|
82
|
+
The editor file mounts inside `ComponentEditorBase` and lays out tokens via `VariantGroup` per variant. Use the shared scaffolding components from `@motion-proto/live-tokens/component-editor`.
|
|
83
|
+
|
|
84
|
+
### State model
|
|
85
|
+
|
|
86
|
+
Components have two state axes. Don't mix them.
|
|
87
|
+
|
|
88
|
+
- **Component states** — mutually exclusive top-level fieldsets: `default`, `selected`, `disabled` (names vary by component). One fieldset per component state.
|
|
89
|
+
- **Interaction states** — a select inside each component-state fieldset: `default`, `hover`. (Add `focus`/`active` later if needed.)
|
|
90
|
+
|
|
91
|
+
Rules:
|
|
92
|
+
|
|
93
|
+
- **Disabled is terminal.** A disabled component can't be hovered or focused. The `disabled` fieldset is flat — no interaction selector.
|
|
94
|
+
- **`selected-disabled` is impossible.** Don't author tokens or fieldsets for it.
|
|
95
|
+
- **Don't call interaction states "option states" or "selected states"** in the UI. `selected` is a *component* state. Interaction states are interaction states.
|
|
96
|
+
|
|
97
|
+
Token naming consequence:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
--mywidget-disabled-surface ✓ component-state-level
|
|
101
|
+
--mywidget-option-disabled-surface ✗ implies disabled is an interaction state
|
|
102
|
+
--mywidget-option-hover-surface ✓ default-component-state, hover-interaction
|
|
103
|
+
--mywidget-selected-hover-surface ✓ selected-component-state, hover-interaction
|
|
104
|
+
--mywidget-selected-disabled-text ✗ selected-disabled doesn't exist
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Parts vs states
|
|
108
|
+
|
|
109
|
+
A component's structural sub-regions (Dialog's `overlay | header | body | footer`) are **parts**, not states. All present simultaneously. The VariantGroup tab strip defaults its label to "Element" — a neutral noun that fits both — but if you label individual tabs anywhere, use **part** when describing structure and **state** when describing runtime conditions. Never call a footer a state.
|
|
110
|
+
|
|
111
|
+
### Editor chrome (greyscale, pill buttons, no em-dashes)
|
|
112
|
+
|
|
113
|
+
The editor chrome is greyscale only. Never introduce accent colors (blue, etc.) for buttons, links, or hover states. The sole exception is file-state indicators. Buttons in editor chrome are pill-shaped with a subtle white gradient. Section underlines are bright (`--ui-border-high`); sub-element outlines stay dim (`--ui-border-faint`).
|
|
114
|
+
|
|
115
|
+
Editor heading scale (semantic):
|
|
116
|
+
|
|
117
|
+
| Role | Token | Treatment |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| Body | `--ui-font-size-md` | — |
|
|
120
|
+
| Section | `--ui-font-size-2xl` | semibold primary, 2px `--ui-border-high` underline |
|
|
121
|
+
| Group | `--ui-font-size-lg` | semibold secondary |
|
|
122
|
+
| Eyebrow | `--ui-font-size-xs` | semibold tertiary, uppercase, letter-spacing |
|
|
123
|
+
|
|
124
|
+
Reference tokens by role; never hardcode pixel sizes.
|
|
125
|
+
|
|
126
|
+
**User-facing copy uses periods and commas, no em-dashes.** Em-dashes read as an AI tell. Restructure sentences. This applies to titles, descriptions, info popovers, button labels, and dialog body text. (Code comments are unaffected.)
|
|
127
|
+
|
|
128
|
+
## Project structure
|
|
129
|
+
|
|
130
|
+
Co-locate the runtime and editor files in `src/system/components/` in the consumer project:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
src/system/components/
|
|
134
|
+
MyWidget.svelte # runtime — declares CSS vars in :global(:root)
|
|
135
|
+
MyWidgetEditor.svelte # editor — exports `allTokens`, mounts via ComponentEditorBase
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Register the component in `src/main.ts` before `mount(App, ...)`:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { registerComponent } from '@motion-proto/live-tokens';
|
|
142
|
+
import MyWidgetEditor, { allTokens as myWidgetTokens } from './system/components/MyWidgetEditor.svelte';
|
|
143
|
+
|
|
144
|
+
registerComponent({
|
|
145
|
+
id: 'mywidget',
|
|
146
|
+
label: 'My Widget',
|
|
147
|
+
icon: 'fas fa-magic',
|
|
148
|
+
sourceFile: 'src/system/components/MyWidget.svelte',
|
|
149
|
+
editorComponent: MyWidgetEditor,
|
|
150
|
+
schema: myWidgetTokens,
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The plugin's `componentsSrcDir` defaults to `src/system/components/` and scans runtime `.svelte` files. The scanner ignores editors because they don't declare `:global(:root)`. On first save in the editor, `component-configs/<id>/default.json` is seeded automatically from the runtime file.
|
|
155
|
+
|
|
156
|
+
## Public imports only
|
|
157
|
+
|
|
158
|
+
Imports in your runtime, editor, and `main.ts` must come from exactly two paths:
|
|
159
|
+
|
|
160
|
+
- `@motion-proto/live-tokens`
|
|
161
|
+
- `@motion-proto/live-tokens/component-editor`
|
|
162
|
+
|
|
163
|
+
Never deep-import `node_modules/@motion-proto/live-tokens/src/...`. Doing so couples your project to internal refactors and is not a supported API.
|
|
164
|
+
|
|
165
|
+
### Legal imports from `@motion-proto/live-tokens`
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import {
|
|
169
|
+
registerComponent,
|
|
170
|
+
editorState,
|
|
171
|
+
setComponentAlias,
|
|
172
|
+
setComponentConfig,
|
|
173
|
+
registerComponentSchema,
|
|
174
|
+
// plus the wider editor init API: configureEditor, initEditorStore, etc.
|
|
175
|
+
} from '@motion-proto/live-tokens';
|
|
176
|
+
|
|
177
|
+
import type {
|
|
178
|
+
RegisterComponentEntry,
|
|
179
|
+
RegistryEntry,
|
|
180
|
+
ComponentId,
|
|
181
|
+
} from '@motion-proto/live-tokens';
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Legal imports from `@motion-proto/live-tokens/component-editor`
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import {
|
|
188
|
+
ComponentEditorBase,
|
|
189
|
+
VariantGroup,
|
|
190
|
+
LinkedBlock,
|
|
191
|
+
TypeEditor,
|
|
192
|
+
TokenLayout,
|
|
193
|
+
buildSiblings,
|
|
194
|
+
computeLinkedBlock,
|
|
195
|
+
withLinkedDisabled,
|
|
196
|
+
buildTypeGroupTokens,
|
|
197
|
+
} from '@motion-proto/live-tokens/component-editor';
|
|
198
|
+
|
|
199
|
+
import type {
|
|
200
|
+
Token,
|
|
201
|
+
Sibling,
|
|
202
|
+
LinkedToken,
|
|
203
|
+
LinkedGroup,
|
|
204
|
+
LinkedBlockResult,
|
|
205
|
+
ComponentSection,
|
|
206
|
+
} from '@motion-proto/live-tokens/component-editor';
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
If you find yourself needing something not in this list, stop. Either restructure the component to avoid the dependency, or file an issue against `@motion-proto/live-tokens` to add the export. Do not deep-import.
|
|
210
|
+
|
|
211
|
+
## 4-step recipe
|
|
212
|
+
|
|
213
|
+
### Step 1 — Runtime component
|
|
214
|
+
|
|
215
|
+
Write `src/system/components/MyWidget.svelte`. Declare every editable slot in `:global(:root)` with a default that references a theme token.
|
|
216
|
+
|
|
217
|
+
### Step 2 — Editor component
|
|
218
|
+
|
|
219
|
+
Write `src/system/components/MyWidgetEditor.svelte`. In a `<script module>` block:
|
|
220
|
+
|
|
221
|
+
- Declare `const component = 'mywidget';`
|
|
222
|
+
- Build the `Token[]` list for each variant × state combination.
|
|
223
|
+
- Export `allTokens: Token[]` (flat union of every token, used by `registerComponentSchema` and the linked-block).
|
|
224
|
+
- Build the `linkableContexts: Map<string, string>` for cross-variant link rows.
|
|
225
|
+
|
|
226
|
+
In the `<script>` block, mount `ComponentEditorBase` with one `VariantGroup` per variant.
|
|
227
|
+
|
|
228
|
+
### Step 3 — Register
|
|
229
|
+
|
|
230
|
+
Add a `registerComponent({ ... })` call to `src/main.ts` before `mount(App, ...)`. The schema-side-effect happens inside `registerComponent`, so you don't call `registerComponentSchema` separately.
|
|
231
|
+
|
|
232
|
+
### Step 4 — Verify
|
|
233
|
+
|
|
234
|
+
Open `/components`. The new component appears under the **CUSTOM** group in the nav rail. Token rows render. Aliases persist. Boot validation is clean.
|
|
235
|
+
|
|
236
|
+
## Worked example: Stat
|
|
237
|
+
|
|
238
|
+
A small `Stat` component with two variants (`primary`, `subtle`) and two interaction states (`default`, `hover`). Renders a number value above a label.
|
|
239
|
+
|
|
240
|
+
### Runtime: `src/system/components/Stat.svelte`
|
|
241
|
+
|
|
242
|
+
```svelte
|
|
243
|
+
<script lang="ts">
|
|
244
|
+
interface Props {
|
|
245
|
+
variant?: 'primary' | 'subtle';
|
|
246
|
+
value: string;
|
|
247
|
+
label: string;
|
|
248
|
+
}
|
|
249
|
+
let { variant = 'primary', value, label }: Props = $props();
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
<div class="stat stat-{variant}">
|
|
253
|
+
<span class="value">{value}</span>
|
|
254
|
+
<span class="label">{label}</span>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<style>
|
|
258
|
+
:global(:root) {
|
|
259
|
+
/* primary */
|
|
260
|
+
--stat-primary-surface: var(--surface-primary);
|
|
261
|
+
--stat-primary-border: var(--border-primary);
|
|
262
|
+
--stat-primary-radius: var(--radius-md);
|
|
263
|
+
--stat-primary-padding: var(--space-16);
|
|
264
|
+
--stat-primary-value-text: var(--text-primary);
|
|
265
|
+
--stat-primary-value-font-family: var(--font-display);
|
|
266
|
+
--stat-primary-value-font-size: var(--font-size-2xl);
|
|
267
|
+
--stat-primary-value-font-weight: var(--font-weight-bold);
|
|
268
|
+
--stat-primary-label-text: var(--text-secondary);
|
|
269
|
+
--stat-primary-label-font-family: var(--font-sans);
|
|
270
|
+
--stat-primary-label-font-size: var(--font-size-sm);
|
|
271
|
+
--stat-primary-hover-surface: var(--surface-primary-high);
|
|
272
|
+
|
|
273
|
+
/* subtle */
|
|
274
|
+
--stat-subtle-surface: var(--surface-neutral);
|
|
275
|
+
--stat-subtle-border: var(--border-neutral);
|
|
276
|
+
--stat-subtle-radius: var(--radius-md);
|
|
277
|
+
--stat-subtle-padding: var(--space-16);
|
|
278
|
+
--stat-subtle-value-text: var(--text-primary);
|
|
279
|
+
--stat-subtle-value-font-family: var(--font-display);
|
|
280
|
+
--stat-subtle-value-font-size: var(--font-size-2xl);
|
|
281
|
+
--stat-subtle-value-font-weight: var(--font-weight-bold);
|
|
282
|
+
--stat-subtle-label-text: var(--text-tertiary);
|
|
283
|
+
--stat-subtle-label-font-family: var(--font-sans);
|
|
284
|
+
--stat-subtle-label-font-size: var(--font-size-sm);
|
|
285
|
+
--stat-subtle-hover-surface: var(--surface-neutral-high);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.stat {
|
|
289
|
+
display: inline-flex;
|
|
290
|
+
flex-direction: column;
|
|
291
|
+
gap: var(--space-4);
|
|
292
|
+
border: 1px solid;
|
|
293
|
+
transition: background 120ms ease;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.stat-primary {
|
|
297
|
+
background: var(--stat-primary-surface);
|
|
298
|
+
border-color: var(--stat-primary-border);
|
|
299
|
+
border-radius: var(--stat-primary-radius);
|
|
300
|
+
padding: var(--stat-primary-padding);
|
|
301
|
+
}
|
|
302
|
+
.stat-primary:hover { background: var(--stat-primary-hover-surface); }
|
|
303
|
+
.stat-primary .value {
|
|
304
|
+
color: var(--stat-primary-value-text);
|
|
305
|
+
font-family: var(--stat-primary-value-font-family);
|
|
306
|
+
font-size: var(--stat-primary-value-font-size);
|
|
307
|
+
font-weight: var(--stat-primary-value-font-weight);
|
|
308
|
+
}
|
|
309
|
+
.stat-primary .label {
|
|
310
|
+
color: var(--stat-primary-label-text);
|
|
311
|
+
font-family: var(--stat-primary-label-font-family);
|
|
312
|
+
font-size: var(--stat-primary-label-font-size);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.stat-subtle {
|
|
316
|
+
background: var(--stat-subtle-surface);
|
|
317
|
+
border-color: var(--stat-subtle-border);
|
|
318
|
+
border-radius: var(--stat-subtle-radius);
|
|
319
|
+
padding: var(--stat-subtle-padding);
|
|
320
|
+
}
|
|
321
|
+
.stat-subtle:hover { background: var(--stat-subtle-hover-surface); }
|
|
322
|
+
.stat-subtle .value {
|
|
323
|
+
color: var(--stat-subtle-value-text);
|
|
324
|
+
font-family: var(--stat-subtle-value-font-family);
|
|
325
|
+
font-size: var(--stat-subtle-value-font-size);
|
|
326
|
+
font-weight: var(--stat-subtle-value-font-weight);
|
|
327
|
+
}
|
|
328
|
+
.stat-subtle .label {
|
|
329
|
+
color: var(--stat-subtle-label-text);
|
|
330
|
+
font-family: var(--stat-subtle-label-font-family);
|
|
331
|
+
font-size: var(--stat-subtle-label-font-size);
|
|
332
|
+
}
|
|
333
|
+
</style>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Things to notice:
|
|
337
|
+
|
|
338
|
+
- Every editable slot lives in `:global(:root)` with a default that references a theme token.
|
|
339
|
+
- `--stat-<v>-value-font-family` and `--stat-<v>-label-font-family` are slot-prefixed because there are two typography slots (`value` and `label`). The same applies to `-font-size`. The bare typography groupKey (`font-family`) would silently link them together.
|
|
340
|
+
- `--stat-<v>-hover-surface` is one row, one state. The hover background is one editable property; we don't author a full hover copy of every slot.
|
|
341
|
+
|
|
342
|
+
### Editor: `src/system/components/StatEditor.svelte`
|
|
343
|
+
|
|
344
|
+
```svelte
|
|
345
|
+
<script module lang="ts">
|
|
346
|
+
import { buildSiblings, type Token } from '@motion-proto/live-tokens/component-editor';
|
|
347
|
+
|
|
348
|
+
export const component = 'stat';
|
|
349
|
+
const variants = ['primary', 'subtle'] as const;
|
|
350
|
+
type Variant = typeof variants[number];
|
|
351
|
+
const stateNames = ['default', 'hover'] as const;
|
|
352
|
+
type StateName = typeof stateNames[number];
|
|
353
|
+
|
|
354
|
+
function variantStateTokens(v: Variant, s: StateName): Token[] {
|
|
355
|
+
if (s === 'default') {
|
|
356
|
+
return [
|
|
357
|
+
{ label: 'surface', variable: `--stat-${v}-surface` },
|
|
358
|
+
{ label: 'border', variable: `--stat-${v}-border` },
|
|
359
|
+
{ label: 'corner radius', variable: `--stat-${v}-radius`, canBeLinked: true, groupKey: 'radius' },
|
|
360
|
+
{ label: 'padding', variable: `--stat-${v}-padding`, canBeLinked: true, groupKey: 'padding' },
|
|
361
|
+
{ label: 'value text', variable: `--stat-${v}-value-text` },
|
|
362
|
+
{ label: 'value font family', variable: `--stat-${v}-value-font-family`, canBeLinked: true, groupKey: 'value-font-family' },
|
|
363
|
+
{ label: 'value font size', variable: `--stat-${v}-value-font-size`, canBeLinked: true, groupKey: 'value-font-size' },
|
|
364
|
+
{ label: 'value font weight', variable: `--stat-${v}-value-font-weight`, canBeLinked: true, groupKey: 'value-font-weight' },
|
|
365
|
+
{ label: 'label text', variable: `--stat-${v}-label-text` },
|
|
366
|
+
{ label: 'label font family', variable: `--stat-${v}-label-font-family`, canBeLinked: true, groupKey: 'label-font-family' },
|
|
367
|
+
{ label: 'label font size', variable: `--stat-${v}-label-font-size`, canBeLinked: true, groupKey: 'label-font-size' },
|
|
368
|
+
];
|
|
369
|
+
}
|
|
370
|
+
// hover: one slot. No selected/disabled, so the hover fieldset is flat.
|
|
371
|
+
return [
|
|
372
|
+
{ label: 'surface', variable: `--stat-${v}-hover-surface` },
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function variantStates(v: Variant): Record<StateName, Token[]> {
|
|
377
|
+
return Object.fromEntries(stateNames.map((s) => [s, variantStateTokens(v, s)])) as Record<StateName, Token[]>;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export const allTokens: Token[] = variants.flatMap((v) => Object.values(variantStates(v)).flat());
|
|
381
|
+
|
|
382
|
+
// Linkable contexts: which variables anchor cross-variant link rows.
|
|
383
|
+
// The slot prefix on typography groupKeys (value-* vs label-*) keeps the two
|
|
384
|
+
// typography slots from merging into one link tree.
|
|
385
|
+
const linkableContexts = new Map<string, string>([
|
|
386
|
+
['--stat-primary-radius', 'primary'],
|
|
387
|
+
['--stat-subtle-radius', 'subtle'],
|
|
388
|
+
['--stat-primary-padding','primary'],
|
|
389
|
+
['--stat-subtle-padding', 'subtle'],
|
|
390
|
+
['--stat-primary-value-font-family', 'primary'],
|
|
391
|
+
['--stat-subtle-value-font-family', 'subtle'],
|
|
392
|
+
['--stat-primary-value-font-size', 'primary'],
|
|
393
|
+
['--stat-subtle-value-font-size', 'subtle'],
|
|
394
|
+
['--stat-primary-value-font-weight', 'primary'],
|
|
395
|
+
['--stat-subtle-value-font-weight', 'subtle'],
|
|
396
|
+
['--stat-primary-label-font-family', 'primary'],
|
|
397
|
+
['--stat-subtle-label-font-family', 'subtle'],
|
|
398
|
+
['--stat-primary-label-font-size', 'primary'],
|
|
399
|
+
['--stat-subtle-label-font-size', 'subtle'],
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
const variantOptions = variants.map((v) => ({
|
|
403
|
+
value: v,
|
|
404
|
+
label: v.charAt(0).toUpperCase() + v.slice(1),
|
|
405
|
+
}));
|
|
406
|
+
</script>
|
|
407
|
+
|
|
408
|
+
<script lang="ts">
|
|
409
|
+
import {
|
|
410
|
+
ComponentEditorBase,
|
|
411
|
+
VariantGroup,
|
|
412
|
+
computeLinkedBlock,
|
|
413
|
+
withLinkedDisabled,
|
|
414
|
+
} from '@motion-proto/live-tokens/component-editor';
|
|
415
|
+
import { editorState } from '@motion-proto/live-tokens';
|
|
416
|
+
import Stat from './Stat.svelte';
|
|
417
|
+
|
|
418
|
+
let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
|
|
419
|
+
let visibleVariantStates = $derived((v: Variant) => Object.fromEntries(
|
|
420
|
+
Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
|
|
421
|
+
) as Record<StateName, Token[]>);
|
|
422
|
+
</script>
|
|
423
|
+
|
|
424
|
+
<ComponentEditorBase
|
|
425
|
+
{component}
|
|
426
|
+
title="Stat"
|
|
427
|
+
description="A number above a label. Import from system/components/Stat.svelte."
|
|
428
|
+
tokens={allTokens}
|
|
429
|
+
{linked}
|
|
430
|
+
variants={variantOptions}
|
|
431
|
+
>
|
|
432
|
+
{#each variants as v}
|
|
433
|
+
<VariantGroup
|
|
434
|
+
name={v}
|
|
435
|
+
title={v.charAt(0).toUpperCase() + v.slice(1)}
|
|
436
|
+
states={visibleVariantStates(v)}
|
|
437
|
+
{component}
|
|
438
|
+
siblings={buildSiblings(variants, v, variantStates)}
|
|
439
|
+
>
|
|
440
|
+
<div class="stat-preview">
|
|
441
|
+
<Stat variant={v} value="248" label="Active users" />
|
|
442
|
+
</div>
|
|
443
|
+
</VariantGroup>
|
|
444
|
+
{/each}
|
|
445
|
+
</ComponentEditorBase>
|
|
446
|
+
|
|
447
|
+
<style>
|
|
448
|
+
.stat-preview {
|
|
449
|
+
display: flex;
|
|
450
|
+
gap: var(--ui-space-16);
|
|
451
|
+
padding: var(--ui-space-16);
|
|
452
|
+
}
|
|
453
|
+
</style>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Registration: `src/main.ts`
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { registerComponent } from '@motion-proto/live-tokens';
|
|
460
|
+
import StatEditor, { allTokens as statTokens } from './system/components/StatEditor.svelte';
|
|
461
|
+
|
|
462
|
+
// ... existing initEditorStore, initCssVarSync, configureEditor, etc.
|
|
463
|
+
|
|
464
|
+
registerComponent({
|
|
465
|
+
id: 'stat',
|
|
466
|
+
label: 'Stat',
|
|
467
|
+
icon: 'fas fa-chart-simple',
|
|
468
|
+
sourceFile: 'src/system/components/Stat.svelte',
|
|
469
|
+
editorComponent: StatEditor,
|
|
470
|
+
schema: statTokens,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ... mount(App, ...)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Verification checklist
|
|
477
|
+
|
|
478
|
+
After saving, navigate to `/components`:
|
|
479
|
+
|
|
480
|
+
- [ ] `Stat` appears in the nav rail under the **CUSTOM** group (system entries above, custom below the labeled divider).
|
|
481
|
+
- [ ] The token rows render. Color pickers, radius selectors, and font selectors all work.
|
|
482
|
+
- [ ] Linked-block: the `corner radius`, `padding`, and font typography rows appear with the link toggle. Changing the linked value broadcasts across both variants.
|
|
483
|
+
- [ ] Save creates `component-configs/stat/default.json`. Subsequent saves write `_active.json` plus any named files.
|
|
484
|
+
- [ ] Reset returns each variable to its `:global(:root)` default.
|
|
485
|
+
- [ ] Boot validation is clean (no warnings about `stat` being missing from the server scan or about disk-vs-registry drift).
|
|
486
|
+
- [ ] Imports in `Stat.svelte`, `StatEditor.svelte`, and `main.ts` come from only `@motion-proto/live-tokens` and `@motion-proto/live-tokens/component-editor`. No `../../node_modules/...` and no deep-imports.
|
|
487
|
+
|
|
488
|
+
If anything fails this checklist, fix it before declaring done. The verification checklist is the contract; the rest of this skill is how to satisfy it.
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A foundational design system for quickly styling and building Svelte + Vite micr
|
|
|
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
|
-
- **Vite plugin** — hosts the `/api/themes
|
|
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
16
|
|
|
17
17
|
## Quick install
|
|
18
18
|
|
|
@@ -32,21 +32,42 @@ export default defineConfig({
|
|
|
32
32
|
plugins: [
|
|
33
33
|
svelte(),
|
|
34
34
|
themeFileApi({
|
|
35
|
-
themesDir: 'themes',
|
|
36
35
|
tokensCssPath: 'src/system/styles/tokens.css',
|
|
37
36
|
}),
|
|
38
37
|
],
|
|
39
|
-
optimizeDeps: {
|
|
40
|
-
exclude: ['@motion-proto/live-tokens'],
|
|
41
|
-
},
|
|
42
38
|
});
|
|
43
39
|
```
|
|
44
40
|
|
|
45
41
|
The `themeFileApi` plugin:
|
|
46
|
-
- Seeds `themes/` with a default theme on first dev-server start.
|
|
47
|
-
- Discovers components at `src/system/components/*.svelte` and seeds `component-configs/{comp}/default.json` from each component's `:global(:root)` block.
|
|
48
|
-
- Hosts the `/api/*` routes the editor uses to save and load themes + per-component configs.
|
|
49
|
-
- Auto-injects `__PROJECT_ROOT__` for the overlay's "Page Source" link.
|
|
42
|
+
- Seeds `src/live-tokens/data/themes/` with a default theme on first dev-server start.
|
|
43
|
+
- Discovers components at `src/system/components/*.svelte` and seeds `src/live-tokens/data/component-configs/{comp}/default.json` from each component's `:global(:root)` block.
|
|
44
|
+
- Hosts the `/api/live-tokens/*` routes the editor uses to save and load themes + per-component configs.
|
|
45
|
+
- Auto-injects `__PROJECT_ROOT__` for the overlay's "Page Source" link and `__LIVE_TOKENS_API_BASE__` so the client uses whatever `apiBase` you configured.
|
|
46
|
+
|
|
47
|
+
### Where data lands — and how to move it
|
|
48
|
+
|
|
49
|
+
By default, the plugin reads and writes under one folder: `src/live-tokens/data/`. Inside that folder live three subdirectories — `themes/`, `manifests/`, `component-configs/` — each owned by the plugin.
|
|
50
|
+
|
|
51
|
+
To move them, create a `live-tokens.config.json` at your project root:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"dataDir": "src/live-tokens/data"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
All four keys are optional. `dataDir` is the headline knob — it relocates all three subfolders at once. The per-folder overrides exist for unusual layouts (e.g. a monorepo where themes are shared across packages but component-configs aren't):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"dataDir": "src/live-tokens/data",
|
|
64
|
+
"themesDir": "../shared/themes",
|
|
65
|
+
"componentConfigsDir": "src/live-tokens/data/component-configs",
|
|
66
|
+
"manifestsDir": "src/live-tokens/data/manifests"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Resolution order, per folder: explicit `themeFileApi(opts)` argument > matching key in `live-tokens.config.json` > `<dataDir>/<sub>`. The dev server reads the file once at startup — restart vite to pick up changes.
|
|
50
71
|
|
|
51
72
|
### Bootstrap in `main.ts`
|
|
52
73
|
|
|
@@ -192,40 +213,74 @@ npm run dev
|
|
|
192
213
|
|
|
193
214
|
Open http://localhost:5173 and replace `src/app/Home.svelte` with your content. The rest of the wiring is already done — it's the same code the npm package ships, just with the App-shell scaffolding included.
|
|
194
215
|
|
|
216
|
+
## Consumer-authored components
|
|
217
|
+
|
|
218
|
+
The shipped components are first-party by default, but you can author your own and get the same real-time editing experience via `registerComponent()`. Co-locate runtime and editor files in `src/system/components/` and register the pair before mounting:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// src/main.ts
|
|
222
|
+
import { registerComponent } from '@motion-proto/live-tokens';
|
|
223
|
+
import MyWidgetEditor, { allTokens as myWidgetTokens } from './system/components/MyWidgetEditor.svelte';
|
|
224
|
+
|
|
225
|
+
registerComponent({
|
|
226
|
+
id: 'mywidget',
|
|
227
|
+
label: 'My Widget',
|
|
228
|
+
icon: 'fas fa-magic',
|
|
229
|
+
sourceFile: 'src/system/components/MyWidget.svelte',
|
|
230
|
+
editorComponent: MyWidgetEditor,
|
|
231
|
+
schema: myWidgetTokens,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// then mount(App, ...)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
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
|
+
### Skill (Claude-assisted authoring)
|
|
240
|
+
|
|
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
|
+
|
|
243
|
+
```bash
|
|
244
|
+
mkdir -p .claude/skills
|
|
245
|
+
ln -s ../../node_modules/@motion-proto/live-tokens/.claude/skills/live-tokens-add-component .claude/skills/
|
|
246
|
+
```
|
|
247
|
+
|
|
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.
|
|
249
|
+
|
|
195
250
|
## How the editor ships changes to prod
|
|
196
251
|
|
|
197
|
-
1. Edit in `/editor` or `/components`. Saves write to
|
|
198
|
-
2. Promote a theme to "production." Its variables are written into `
|
|
199
|
-
3. `npm run build` bundles
|
|
252
|
+
1. Edit in `/editor` or `/components`. Saves write to `<dataDir>/themes/{name}.json` and `<dataDir>/component-configs/{comp}/{name}.json`.
|
|
253
|
+
2. Promote a theme to "production." Its variables are written into `tokens.generated.css` next to your authored `tokens.css`.
|
|
254
|
+
3. `npm run build` bundles both as plain CSS. No editor code, no JSON lookups, no dev surfaces ship to prod.
|
|
200
255
|
|
|
201
256
|
## File ownership — what the plugin writes
|
|
202
257
|
|
|
203
258
|
Knowing which files the plugin touches matters when upgrading the package or working in a repo you don't want overwritten.
|
|
204
259
|
|
|
205
|
-
**On `npm install` or `npm update`: nothing outside `node_modules/`.** No install hooks. Upgrading versions never touches your `
|
|
260
|
+
**On `npm install` or `npm update`: nothing outside `node_modules/`.** No install hooks. Upgrading versions never touches your `src/live-tokens/data/`, or any file in `src/` outside it.
|
|
206
261
|
|
|
207
|
-
**
|
|
262
|
+
**The plugin only writes inside two locations on disk:**
|
|
208
263
|
|
|
209
|
-
- `
|
|
210
|
-
- `
|
|
211
|
-
- `component-configs/{comp}/_active.json` and `_production.json` — same: only if missing.
|
|
212
|
-
- `component-configs/{comp}/default.json` — regenerated from the component's `:global(:root)` block **only when the `.svelte` source is newer than the existing default**. This file is a build artifact of the source; don't hand-edit it.
|
|
264
|
+
- `src/live-tokens/data/` (configurable via `live-tokens.config.json` — see "Where data lands").
|
|
265
|
+
- The CSS sidecars next to your `tokensCssPath` (`tokens.generated.css`, `fonts.css`).
|
|
213
266
|
|
|
214
|
-
|
|
267
|
+
It never writes to your project root, your `src/` outside the data folder, or anywhere else.
|
|
215
268
|
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
269
|
+
**At dev-server startup, the plugin only fills gaps — it never overwrites authored files:**
|
|
270
|
+
|
|
271
|
+
- `<dataDir>/themes/default.json` — written **only if missing**.
|
|
272
|
+
- `<dataDir>/themes/_active.json` and `_production.json` — written **only if missing**.
|
|
273
|
+
- `<dataDir>/component-configs/{comp}/_active.json` and `_production.json` — same: only if missing.
|
|
274
|
+
- `<dataDir>/component-configs/{comp}/default.json` — regenerated from the component's `:global(:root)` block **only when the `.svelte` source is newer than the existing default**. This file is a build artifact of the source; don't hand-edit it.
|
|
220
275
|
|
|
221
|
-
|
|
276
|
+
**At dev-time editor actions, these files get rewritten by your explicit save/promote:**
|
|
222
277
|
|
|
223
|
-
|
|
278
|
+
- `<dataDir>/themes/{name}.json` — every save in the editor.
|
|
279
|
+
- `<tokensCssPath sibling>/tokens.generated.css` — fully regenerated when you save or promote the production theme.
|
|
280
|
+
- `<tokensCssPath sibling>/fonts.css` — same rule: regenerated from the theme's font sources.
|
|
281
|
+
- `<dataDir>/component-configs/{comp}/{name}.json` — every save of a per-component config.
|
|
224
282
|
|
|
225
|
-
|
|
226
|
-
2. Verify tarball contents: `npm pack --dry-run`.
|
|
227
|
-
3. `npm publish`. `prepublishOnly` rebuilds `dist-plugin/`.
|
|
228
|
-
4. Tag and push: `git tag vX.Y.Z && git push origin main vX.Y.Z`.
|
|
283
|
+
The developer-authored `tokens.css` itself is **never written** by the plugin — it holds defaults you're free to hand-edit. The editor's overrides land in the sidecar `tokens.generated.css`, which the package imports immediately after `tokens.css`.
|
|
229
284
|
|
|
230
285
|
## License
|
|
231
286
|
|