@motion-proto/live-tokens 0.7.1 → 0.9.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 +34 -0
- package/dist-plugin/index.cjs +707 -90
- package/dist-plugin/index.d.cts +1 -0
- package/dist-plugin/index.d.ts +1 -0
- package/dist-plugin/index.js +707 -90
- package/package.json +6 -2
- package/src/app/site.css +1 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
- package/src/editor/component-editor/DialogEditor.svelte +4 -4
- package/src/editor/component-editor/NotificationEditor.svelte +3 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
- package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
- package/src/editor/component-editor/editors.d.ts +10 -0
- package/src/editor/component-editor/index.ts +16 -1
- package/src/editor/component-editor/registry.ts +103 -26
- package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
- package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
- package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
- package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
- 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/types.ts +11 -0
- package/src/editor/core/components/componentConfigKeys.ts +22 -3
- package/src/editor/core/components/componentConfigService.ts +2 -2
- package/src/editor/core/components/componentPersist.ts +7 -5
- package/src/editor/core/manifests/manifestService.ts +58 -3
- package/src/editor/core/palettes/familySwap.ts +99 -0
- package/src/editor/core/palettes/paletteDerivation.ts +69 -0
- package/src/editor/core/palettes/tokenRegistry.ts +4 -1
- package/src/editor/core/store/editorStore.ts +206 -12
- package/src/editor/core/store/editorTypes.ts +55 -12
- package/src/editor/core/store/gradientSource.ts +192 -0
- package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
- package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
- package/src/editor/core/themes/migrations/index.ts +10 -0
- package/src/editor/core/themes/slices/components.ts +27 -4
- package/src/editor/core/themes/slices/gradients.ts +88 -13
- package/src/editor/core/themes/themeInit.ts +2 -2
- package/src/editor/core/themes/themeTypes.ts +56 -1
- package/src/editor/index.ts +10 -1
- package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
- package/src/editor/pages/ComponentEditorPage.svelte +53 -3
- package/src/editor/pages/EditorShell.svelte +53 -3
- package/src/editor/styles/ui-editor.css +1 -0
- package/src/editor/styles/ui-form-controls.css +19 -20
- package/src/editor/ui/BezierCurveEditor.svelte +114 -63
- package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
- package/src/editor/ui/FileLoadList.svelte +22 -5
- package/src/editor/ui/FontStackEditor.svelte +214 -76
- package/src/editor/ui/GradientEditor.svelte +435 -215
- package/src/editor/ui/GradientStopPicker.svelte +11 -3
- package/src/editor/ui/ManifestFileManager.svelte +71 -4
- package/src/editor/ui/PaletteEditor.svelte +52 -79
- package/src/editor/ui/ProjectFontsSection.svelte +328 -293
- package/src/editor/ui/ThemeFileManager.svelte +0 -4
- package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
- package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
- package/src/editor/ui/UIInfoPopover.svelte +0 -1
- package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
- package/src/editor/ui/UIPaletteSelector.svelte +31 -4
- package/src/editor/ui/UIPillButton.svelte +33 -3
- package/src/editor/ui/UISegmentedControl.svelte +114 -0
- package/src/editor/ui/UITokenSelector.svelte +4 -1
- package/src/editor/ui/VariablesTab.svelte +41 -35
- package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
- package/src/editor/ui/palette/PaletteBase.svelte +3 -3
- package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
- package/src/editor/ui/sections/GradientsSection.svelte +1 -1
- package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
- package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
- package/src/system/components/Button.svelte +2 -2
- package/src/system/components/Card.svelte +29 -1
- package/src/system/components/CollapsibleSection.svelte +25 -2
- package/src/system/components/Dialog.svelte +24 -4
- package/src/system/components/FloatingTokenTags.css +43 -24
- package/src/system/components/FloatingTokenTags.svelte +88 -137
- package/src/system/components/Notification.svelte +8 -1
- package/src/system/components/SectionDivider.svelte +532 -381
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/fonts.css +3 -16
- package/src/system/styles/tokens.css +356 -1199
- package/src/system/styles/tokens.generated.css +544 -0
- package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
- package/src/editor/component-editor/scaffolding/GradientCard.svelte +0 -296
|
@@ -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
|
@@ -192,6 +192,40 @@ npm run dev
|
|
|
192
192
|
|
|
193
193
|
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
194
|
|
|
195
|
+
## Consumer-authored components
|
|
196
|
+
|
|
197
|
+
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:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// src/main.ts
|
|
201
|
+
import { registerComponent } from '@motion-proto/live-tokens';
|
|
202
|
+
import MyWidgetEditor, { allTokens as myWidgetTokens } from './system/components/MyWidgetEditor.svelte';
|
|
203
|
+
|
|
204
|
+
registerComponent({
|
|
205
|
+
id: 'mywidget',
|
|
206
|
+
label: 'My Widget',
|
|
207
|
+
icon: 'fas fa-magic',
|
|
208
|
+
sourceFile: 'src/system/components/MyWidget.svelte',
|
|
209
|
+
editorComponent: MyWidgetEditor,
|
|
210
|
+
schema: myWidgetTokens,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// then mount(App, ...)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
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/`.
|
|
217
|
+
|
|
218
|
+
### Skill (Claude-assisted authoring)
|
|
219
|
+
|
|
220
|
+
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:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
mkdir -p .claude/skills
|
|
224
|
+
ln -s ../../node_modules/@motion-proto/live-tokens/.claude/skills/live-tokens-add-component .claude/skills/
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
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.
|
|
228
|
+
|
|
195
229
|
## How the editor ships changes to prod
|
|
196
230
|
|
|
197
231
|
1. Edit in `/editor` or `/components`. Saves write to `themes/{name}.json` and `component-configs/{comp}/{name}.json`.
|