@motion-proto/live-tokens 0.55.1 → 0.56.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/.claude/skills/{live-tokens-adjust-shape-space → live-tokens-adjust-geometry}/SKILL.md +5 -5
- package/.claude/skills/live-tokens-build-page/SKILL.md +2 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +20 -357
- package/.claude/skills/live-tokens-create-component/references/fixed-overlays.md +3 -0
- package/.claude/skills/live-tokens-create-component/references/intrinsics.md +58 -0
- package/.claude/skills/live-tokens-create-component/references/linked-siblings.md +70 -0
- package/.claude/skills/live-tokens-generate-theme/SKILL.md +79 -98
- package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +18 -0
- package/.claude/skills/live-tokens-pair-fonts/SKILL.md +88 -0
- package/.claude/skills/live-tokens-pick-component/SKILL.md +4 -1
- package/CHANGELOG.md +73 -0
- package/README.md +20 -7
- package/bin/cli.mjs +41 -3
- package/bin/set-fonts.mjs +280 -0
- package/dist-plugin/adjust/index.d.cts +2 -2
- package/dist-plugin/adjust/index.d.ts +2 -2
- package/dist-plugin/fontPairing/index.cjs +411 -0
- package/dist-plugin/fontPairing/index.d.cts +109 -0
- package/dist-plugin/fontPairing/index.d.ts +109 -0
- package/dist-plugin/fontPairing/index.js +320 -0
- package/dist-plugin/generateColorsAndType/index.d.cts +2 -2
- package/dist-plugin/generateColorsAndType/index.d.ts +2 -2
- package/dist-plugin/index-4N-Orzzi.d.cts +3 -0
- package/dist-plugin/index-4N-Orzzi.d.ts +3 -0
- package/dist-plugin/{index-DpTIRZ2H.d.cts → themeTypes-BAqtv4XO.d.cts} +1 -3
- package/dist-plugin/{index-DpTIRZ2H.d.ts → themeTypes-BAqtv4XO.d.ts} +1 -3
- package/package.json +3 -2
- package/src/editor/core/fonts/applyFontPairing.ts +157 -0
- package/src/editor/core/fonts/fontPairing.ts +13 -6
- package/src/editor/core/fonts/googleFontsUrl.ts +123 -0
- package/src/editor/core/fonts/weightCoverage.ts +92 -0
- package/src/editor/docs/content/themes-workflow.md +39 -0
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/ui/ProjectFontsSection.svelte +24 -36
package/.claude/skills/{live-tokens-adjust-shape-space → live-tokens-adjust-geometry}/SKILL.md
RENAMED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: live-tokens-adjust-
|
|
3
|
-
description: Adjust corner radius, padding, gap, and border width across live-tokens components by moving each token alias along the shipped scales. Use when the user asks for pill or capsule buttons, rounded, rounder, sharp, sharper, square corners, softer, harder, a bigger or smaller corner radius, thicker or thinner borders, or talks about spacing and padding. Make the buttons pill shaped, give the cards sharp corners, make the windows sharper, make the UI softer, space it out, tighter, denser, more compact, airier, more breathing room. Changes shape and space aliases per component, never color, fonts, or tokens.css. Not for editing a single token (use the editor) or for color (see live-tokens-generate-theme).
|
|
2
|
+
name: live-tokens-adjust-geometry
|
|
3
|
+
description: Adjust corner radius, padding, gap, and border width across live-tokens components by moving each token alias along the shipped scales. Use when the user asks for pill or capsule buttons, rounded, rounder, sharp, sharper, square corners, softer, harder, a bigger or smaller corner radius, thicker or thinner borders, or talks about spacing and padding. Make the buttons pill shaped, give the cards sharp corners, make the windows sharper, make the UI softer, space it out, tighter, denser, more compact, airier, more breathing room. Also invoked by live-tokens-generate-theme for the geometry half of a whole look. Changes shape and space aliases per component, never color, fonts, or tokens.css. Not for editing a single token (use the editor) or for color (see live-tokens-generate-theme).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Adjusting
|
|
6
|
+
# Adjusting geometry
|
|
7
7
|
|
|
8
8
|
You translate the request into a small ops file; the CLI resolves each matching alias on its token ladder, writes the result into each component's unsaved buffer, and prints a report card. Never hand-edit the data tree.
|
|
9
9
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
12
|
-
1. Write the ops file to
|
|
13
|
-
2. Run `npx live-tokens adjust /
|
|
12
|
+
1. Write the ops file to `scratch/adjust-ops.json`.
|
|
13
|
+
2. Run `npx live-tokens adjust scratch/adjust-ops.json`. It writes `component-configs/<id>/_working.json` for every component the ops change, which is the buffer the page already runs. `--dry-run` prints the report without writing.
|
|
14
14
|
3. Read the report card: every changed alias old → new, plus skips (raw value, off the ladder, already at the ladder end, pill preserved). Exit 1 means the run was rejected; the message names the offending op or the missing input, so fix it and re-run. Read where the controls landed, not only that the run succeeded: a button, badge, input, or tab padding sitting at `--space-6` is on its floor, and one that also carries `--radius-full` wants a targeted lift.
|
|
15
15
|
4. Tell the user to reload the app and look. Offer the inverse op as the undo, and say the edit is unsaved until they save the open theme.
|
|
16
16
|
|
|
@@ -10,6 +10,8 @@ Two rules above all else:
|
|
|
10
10
|
1. **Use a shipped component if one fits.** Import from `@motion-proto/live-tokens/components/<Name>.svelte`. See [[live-tokens-pick-component]] for the catalogue and the confusing-pair decisions. Author custom markup only when nothing fits, and then consider [[live-tokens-create-component]] so the new piece is editable too.
|
|
11
11
|
2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`. No hex literals. No pixel literals. A change in `/live-tokens/editor` should repaint your page.
|
|
12
12
|
|
|
13
|
+
For text, reach for a whole text style rather than assembling one: `--heading-xl` through `--heading-sm`, `--body-md`, `--body-sm`, `--eyebrow`, and `--code` each carry a `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. A heading set from `--heading-lg-*` retypes when the theme's fonts change; one set from a raw `font-size` does not.
|
|
14
|
+
|
|
13
15
|
## Layout
|
|
14
16
|
|
|
15
17
|
Pages sit inside the column grid via `--columns-count`, `--columns-gutter`, `--columns-max-width`. Toggle `ColumnsOverlay` (Cmd+G in dev) to visualise it while placing content.
|
|
@@ -5,7 +5,7 @@ description: Author a brand-new editable component for a @motion-proto/live-toke
|
|
|
5
5
|
|
|
6
6
|
# Authoring a component for a live-tokens project
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
The end state is a runtime Svelte file, an editor Svelte file, one registration, and an entry on `/live-tokens/components` under the **CUSTOM** group with full token editing, linked-block sharing, and persistence.
|
|
9
9
|
|
|
10
10
|
## Worked examples ship inside the package
|
|
11
11
|
|
|
@@ -21,10 +21,10 @@ For pattern reference, read any shipped component's source directly from the con
|
|
|
21
21
|
|
|
22
22
|
**File-location note.** Shipped editors live in `src/editor/component-editor/` because they're library-internal. For *your* component, **co-locate** both files in `src/system/components/` per the recipe below. Read the shipped files for pattern, ignore their location.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## The recipe
|
|
25
25
|
|
|
26
26
|
1. **Runtime file** — `src/system/components/MyWidget.svelte`. Declare every editable slot as a CSS custom property inside `:global(:root)`, defaulting to a theme token (never a raw value). The plugin parses `:global(:root)` to seed `component-configs/<id>/default.json`; variables declared anywhere else can't be edited.
|
|
27
|
-
2. **Editor file** — `src/system/components/MyWidgetEditor.svelte`. In a `<script module>` block, declare `const component = 'mywidget'`, build a `states: Record<string, Token[]>` for each VariantGroup, and export the flat union as `allTokens: Token[]`. Components with linked siblings also build a `linkableContexts: Map<string, string>` (
|
|
27
|
+
2. **Editor file** — `src/system/components/MyWidgetEditor.svelte`. In a `<script module>` block, declare `const component = 'mywidget'`, build a `states: Record<string, Token[]>` for each VariantGroup, and export the flat union as `allTokens: Token[]`. Components with linked siblings also build a `linkableContexts: Map<string, string>` (read `references/linked-siblings.md`). Components with structural/display controls that aren't token values (alignment, element visibility, layout position) also export an `intrinsics: IntrinsicSpec[]` (read `references/intrinsics.md`). In the runtime `<script>` block, mount `ComponentEditorBase` with one `VariantGroup` per variant.
|
|
28
28
|
3. **Register** — pass the component to `bootLiveTokens` in `src/main.ts`. This is the standard boot the scaffold generates and the README documents; `bootLiveTokens` calls `registerComponent` internally at the right point — after its editor init hooks (`cssVarSync.init`, `editorStore.init`), before it seeds configs and mounts the app:
|
|
29
29
|
```ts
|
|
30
30
|
import { bootLiveTokens } from '@motion-proto/live-tokens';
|
|
@@ -44,7 +44,7 @@ For pattern reference, read any shipped component's source directly from the con
|
|
|
44
44
|
```
|
|
45
45
|
The schema side-effect happens inside `registerComponent` (which `bootLiveTokens` calls for you), so you don't call `registerComponentSchema` separately. **Do not place a standalone `registerComponent(...)` *before* `bootLiveTokens`** — that registers before the editor's init hooks run, which is the wrong window and can leave editor changes disconnected from the live page. Only call `registerComponent` directly if your app mounts manually (no `bootLiveTokens`), in which case call it before `mount(App, ...)`.
|
|
46
46
|
4. **Tell the picker** — open `.claude/skills/live-tokens-pick-component/SKILL.md` and add your new component to the **Catalogue** line under the family it belongs to (Action / Input / Selection / Containers / Messaging / Display). If it's confusable with an existing component (a second selection control, a competing container), add a row to that family's decision table explaining the use-case it owns. Without this step, the component exists but [[live-tokens-pick-component]] can't recommend it when a user asks "which component should I use?" — the same rule applies whether the component is first-party (update the picker shipped in this package) or consumer-authored (update the local copy at `.claude/skills/live-tokens-pick-component/SKILL.md` that `setup-claude` placed in your project).
|
|
47
|
-
5. **Verify**
|
|
47
|
+
5. **Verify** with the checklist at the bottom of this file.
|
|
48
48
|
|
|
49
49
|
## Token discipline
|
|
50
50
|
|
|
@@ -106,12 +106,12 @@ The authoritative recognised list lives in `bin/check-component.mjs` (`KNOWN_SUF
|
|
|
106
106
|
|
|
107
107
|
### Rules that bite
|
|
108
108
|
|
|
109
|
-
- **Fixed overlays must portal to `<body>`.** Any `position: fixed` layer
|
|
109
|
+
- **Fixed overlays must portal to `<body>`.** Any `position: fixed` layer is trapped by a transformed or `contain`ed ancestor, which real pages and the editor's preview pane both have. `check:overlay-portal` fails the build without it. Read `references/fixed-overlays.md` before authoring a modal, lightbox, or backdrop.
|
|
110
110
|
- **State before property.** `--mywidget-button-hover-surface` ✓ — `--mywidget-button-surface-hover` ✗ (breaks sibling matching).
|
|
111
111
|
- **Defaults reference theme tokens, never raw values.** `var(--surface-primary)` ✓ — `#6a4ce8` ✗.
|
|
112
112
|
- **No abbreviations.** `bg` → `surface`; `fg` → `text`; component ids are never abbreviated.
|
|
113
113
|
- **Text aliases.** Neutral scale is `--text-primary` / `--text-secondary` / `--text-tertiary` / `--text-muted` / `--text-disabled`. Family-tinted is `--text-primary-color`, `--text-accent`, `--text-success`. There is no `--text-neutral`.
|
|
114
|
-
- **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'` ✓ — bare `groupKey: 'font-family'` silently merges them into one link tree ✗. Single-slot components can use a bare typography `groupKey`; add the slot prefix the moment a second slot appears. The same trap applies to type-group **colors** (two slots ending in `-text` collapsing to one `text` key). Let the helpers handle both
|
|
114
|
+
- **Typography `groupKey` on multi-slot components must include the slot prefix.** `groupKey: 'value-font-family'` and `groupKey: 'label-font-family'` ✓ — bare `groupKey: 'font-family'` silently merges them into one link tree ✗. Single-slot components can use a bare typography `groupKey`; add the slot prefix the moment a second slot appears. The same trap applies to type-group **colors** (two slots ending in `-text` collapsing to one `text` key). Let the helpers handle both, below.
|
|
115
115
|
- **Let the type-group helpers derive slot-scoped keys; never rely on the bare last-dash default.** When you build typography tokens with `buildTypeGroupColorTokens` / `buildTypeGroupTokens` / `buildTypeGroupFontTokens`, **pass `{ component, variants }`** so each slot gets a distinct, structural `groupKey`:
|
|
116
116
|
|
|
117
117
|
```ts
|
|
@@ -123,10 +123,6 @@ The authoritative recognised list lives in `bin/check-component.mjs` (`KNOWN_SUF
|
|
|
123
123
|
|
|
124
124
|
The helper strips the `--<component>-` prefix and those segments, keeping the rest: `--mywidget-header-default-text` → `header-text`, `--mywidget-header-default-text-font-family` → `header-text-font-family`. Two parts ending in the same word stay distinct; one slot across variants collapses to one key. To override a single derived key, set `colorGroupKey` on that type-group config — it wins and is never recomputed, so your fix survives. There is no name-based fallback: a bare `buildTypeGroupColorTokens` call emits un-grouped (solo) colors rather than guessing, and a bare *font* helper across multiple slots is a `check-component` warning (its default `font-family`/… keys would merge the slots' fonts).
|
|
125
125
|
|
|
126
|
-
### Linked siblings
|
|
127
|
-
|
|
128
|
-
Tokens that share a `groupKey` and declare `canBeLinked: true` form a sibling set with a link toggle in the editor. Linkage is **dev-declared** — you author it when building the component. Users opt out of an existing link per-property; they never add or reshape links. Do not expose UI for users to add siblings or mark properties as linkable.
|
|
129
|
-
|
|
130
126
|
## State model
|
|
131
127
|
|
|
132
128
|
Components *can* have two state axes. Many don't: container and messaging components (Card, Badge, Callout, CollapsibleSection) have only variants, no hover/disabled. Skip the rest of this section for those.
|
|
@@ -176,365 +172,32 @@ That covers everything the worked examples use. Additional primitives (`LinkedBl
|
|
|
176
172
|
|
|
177
173
|
**Never deep-import `node_modules/@motion-proto/live-tokens/src/...`.** Reading those files for pattern reference is fine; importing them at runtime is not. If you need something not exported, file an issue rather than reaching in.
|
|
178
174
|
|
|
179
|
-
## Worked example: shipped Toggle
|
|
180
|
-
|
|
181
|
-
Toggle ships in the package and exercises every rule above. For your own component, copy this pattern and substitute your id; just don't reuse `toggle` itself (registrations against a built-in id win with a console warning, but the right call is a unique id).
|
|
182
|
-
|
|
183
|
-
### Runtime: `src/system/components/Toggle.svelte`
|
|
184
|
-
|
|
185
|
-
```svelte
|
|
186
|
-
<script lang="ts">
|
|
187
|
-
interface Props {
|
|
188
|
-
checked?: boolean;
|
|
189
|
-
disabled?: boolean;
|
|
190
|
-
label?: string;
|
|
191
|
-
/** Editor preview hook. Paints hover tokens without a real pointer. */
|
|
192
|
-
class?: string;
|
|
193
|
-
onchange?: (checked: boolean) => void;
|
|
194
|
-
}
|
|
195
|
-
let {
|
|
196
|
-
checked = false, disabled = false, label = '',
|
|
197
|
-
class: className = '', onchange,
|
|
198
|
-
}: Props = $props();
|
|
199
|
-
function toggle() {
|
|
200
|
-
if (disabled) return;
|
|
201
|
-
onchange?.(!checked);
|
|
202
|
-
}
|
|
203
|
-
</script>
|
|
204
|
-
|
|
205
|
-
<button
|
|
206
|
-
type="button" role="switch" aria-checked={checked}
|
|
207
|
-
class="toggle {className}" class:on={checked}
|
|
208
|
-
{disabled} onclick={toggle}
|
|
209
|
-
>
|
|
210
|
-
<span class="track"><span class="thumb"></span></span>
|
|
211
|
-
{#if label}<span class="label">{label}</span>{/if}
|
|
212
|
-
</button>
|
|
213
|
-
|
|
214
|
-
<style>
|
|
215
|
-
:global(:root) {
|
|
216
|
-
/* Default (off resting). Carries geometry + label typography for every state. */
|
|
217
|
-
--toggle-track-surface: var(--surface-neutral);
|
|
218
|
-
--toggle-track-border: var(--border-neutral);
|
|
219
|
-
--toggle-track-border-width: var(--border-width-1);
|
|
220
|
-
--toggle-track-radius: var(--radius-full);
|
|
221
|
-
--toggle-track-width: var(--space-32);
|
|
222
|
-
--toggle-track-thickness: var(--space-16);
|
|
223
|
-
--toggle-thumb-surface: var(--surface-neutral-highest);
|
|
224
|
-
--toggle-thumb-border: var(--border-neutral-strong);
|
|
225
|
-
--toggle-thumb-size: var(--space-12);
|
|
226
|
-
--toggle-label-text: var(--text-primary);
|
|
227
|
-
--toggle-label-font-family: var(--font-sans);
|
|
228
|
-
--toggle-label-font-size: var(--font-size-sm);
|
|
229
|
-
--toggle-label-font-weight: var(--font-weight-normal);
|
|
230
|
-
--toggle-gap: var(--space-8);
|
|
231
|
-
|
|
232
|
-
/* Hover (default + hover interaction). */
|
|
233
|
-
--toggle-hover-track-surface: var(--surface-neutral-high);
|
|
234
|
-
--toggle-hover-thumb-surface: var(--text-primary);
|
|
235
|
-
|
|
236
|
-
/* On (component state). */
|
|
237
|
-
--toggle-on-track-surface: var(--surface-brand-high);
|
|
238
|
-
--toggle-on-track-border: var(--border-brand);
|
|
239
|
-
--toggle-on-thumb-surface: var(--text-primary);
|
|
240
|
-
--toggle-on-thumb-border: var(--border-brand-strong);
|
|
241
|
-
|
|
242
|
-
/* On + hover. */
|
|
243
|
-
--toggle-on-hover-track-surface: var(--surface-brand-higher);
|
|
244
|
-
--toggle-on-hover-thumb-surface: var(--text-primary);
|
|
245
|
-
|
|
246
|
-
/* Disabled (terminal, applies regardless of on/off). */
|
|
247
|
-
--toggle-disabled-track-surface: var(--surface-neutral-lower);
|
|
248
|
-
--toggle-disabled-thumb-surface: var(--surface-neutral);
|
|
249
|
-
--toggle-disabled-label-text: var(--text-disabled);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
.toggle { display: inline-flex; align-items: center; gap: var(--toggle-gap); }
|
|
253
|
-
.track {
|
|
254
|
-
width: var(--toggle-track-width); height: var(--toggle-track-thickness);
|
|
255
|
-
background: var(--toggle-track-surface);
|
|
256
|
-
border: var(--toggle-track-border-width) solid var(--toggle-track-border);
|
|
257
|
-
border-radius: var(--toggle-track-radius);
|
|
258
|
-
}
|
|
259
|
-
.thumb {
|
|
260
|
-
width: var(--toggle-thumb-size); height: var(--toggle-thumb-size);
|
|
261
|
-
background: var(--toggle-thumb-surface);
|
|
262
|
-
border: var(--toggle-track-border-width) solid var(--toggle-thumb-border);
|
|
263
|
-
}
|
|
264
|
-
.label {
|
|
265
|
-
color: var(--toggle-label-text);
|
|
266
|
-
font-family: var(--toggle-label-font-family);
|
|
267
|
-
font-size: var(--toggle-label-font-size);
|
|
268
|
-
font-weight: var(--toggle-label-font-weight);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
.toggle:hover:not(:disabled) .track,
|
|
272
|
-
.toggle.force-hover:not(:disabled) .track { background: var(--toggle-hover-track-surface); }
|
|
273
|
-
.toggle:hover:not(:disabled) .thumb,
|
|
274
|
-
.toggle.force-hover:not(:disabled) .thumb { background: var(--toggle-hover-thumb-surface); }
|
|
275
|
-
|
|
276
|
-
.toggle.on .track {
|
|
277
|
-
background: var(--toggle-on-track-surface);
|
|
278
|
-
border-color: var(--toggle-on-track-border);
|
|
279
|
-
}
|
|
280
|
-
.toggle.on .thumb {
|
|
281
|
-
background: var(--toggle-on-thumb-surface);
|
|
282
|
-
border-color: var(--toggle-on-thumb-border);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
.toggle.on:hover:not(:disabled) .track,
|
|
286
|
-
.toggle.on.force-hover:not(:disabled) .track { background: var(--toggle-on-hover-track-surface); }
|
|
287
|
-
.toggle.on:hover:not(:disabled) .thumb,
|
|
288
|
-
.toggle.on.force-hover:not(:disabled) .thumb { background: var(--toggle-on-hover-thumb-surface); }
|
|
289
|
-
|
|
290
|
-
.toggle:disabled .track { background: var(--toggle-disabled-track-surface); }
|
|
291
|
-
.toggle:disabled .thumb { background: var(--toggle-disabled-thumb-surface); }
|
|
292
|
-
.toggle:disabled .label { color: var(--toggle-disabled-label-text); }
|
|
293
|
-
</style>
|
|
294
|
-
```
|
|
175
|
+
## Worked example: the shipped Toggle
|
|
295
176
|
|
|
296
|
-
|
|
177
|
+
Toggle exercises every rule above in the fewest lines. Read both files from the package rather than from memory, because they are the contract and this skill is not:
|
|
178
|
+
|
|
179
|
+
- `node_modules/@motion-proto/live-tokens/src/system/components/Toggle.svelte`
|
|
180
|
+
- `node_modules/@motion-proto/live-tokens/src/editor/component-editor/ToggleEditor.svelte`
|
|
181
|
+
|
|
182
|
+
What to notice in the runtime file:
|
|
297
183
|
|
|
298
|
-
- The `:global(:root)` block declares every editable variable. Variables in scoped selectors don't get edited; the plugin only parses `:global(:root)`.
|
|
299
|
-
- Every default references a theme token; no raw values.
|
|
300
184
|
- Component states (`on`, `disabled`) name themselves in the token: `--toggle-on-*`, `--toggle-disabled-*`.
|
|
301
185
|
- Interaction states layer on top: `--toggle-hover-*` for default+hover, `--toggle-on-hover-*` for on+hover.
|
|
302
186
|
- Disabled is terminal: no `--toggle-disabled-hover-*`, no `--toggle-on-disabled-*`.
|
|
303
187
|
- The `force-hover` class pairs with the editor's preview hook so hover tokens paint without a real pointer. Each `:hover` selector has a matching `.force-hover` sibling.
|
|
304
188
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
```svelte
|
|
308
|
-
<script module lang="ts">
|
|
309
|
-
import type { Token } from '@motion-proto/live-tokens/component-editor';
|
|
310
|
-
export const component = 'toggle';
|
|
311
|
-
|
|
312
|
-
const states: Record<string, Token[]> = {
|
|
313
|
-
default: [
|
|
314
|
-
{ label: 'track surface', variable: '--toggle-track-surface' },
|
|
315
|
-
{ label: 'track border', variable: '--toggle-track-border' },
|
|
316
|
-
{ label: 'track border width', variable: '--toggle-track-border-width' },
|
|
317
|
-
{ label: 'track radius', variable: '--toggle-track-radius' },
|
|
318
|
-
{ label: 'track width', variable: '--toggle-track-width' },
|
|
319
|
-
{ label: 'track thickness', variable: '--toggle-track-thickness' },
|
|
320
|
-
{ label: 'thumb surface', variable: '--toggle-thumb-surface' },
|
|
321
|
-
{ label: 'thumb border', variable: '--toggle-thumb-border' },
|
|
322
|
-
{ label: 'thumb size', variable: '--toggle-thumb-size' },
|
|
323
|
-
{ label: 'label text', variable: '--toggle-label-text' },
|
|
324
|
-
{ label: 'label font family', variable: '--toggle-label-font-family' },
|
|
325
|
-
{ label: 'label font size', variable: '--toggle-label-font-size' },
|
|
326
|
-
{ label: 'label font weight', variable: '--toggle-label-font-weight' },
|
|
327
|
-
{ label: 'label gap', variable: '--toggle-gap' },
|
|
328
|
-
],
|
|
329
|
-
hover: [
|
|
330
|
-
{ label: 'track surface', variable: '--toggle-hover-track-surface' },
|
|
331
|
-
{ label: 'thumb surface', variable: '--toggle-hover-thumb-surface' },
|
|
332
|
-
],
|
|
333
|
-
on: [
|
|
334
|
-
{ label: 'track surface', variable: '--toggle-on-track-surface' },
|
|
335
|
-
{ label: 'track border', variable: '--toggle-on-track-border' },
|
|
336
|
-
{ label: 'thumb surface', variable: '--toggle-on-thumb-surface' },
|
|
337
|
-
{ label: 'thumb border', variable: '--toggle-on-thumb-border' },
|
|
338
|
-
],
|
|
339
|
-
'on hover': [
|
|
340
|
-
{ label: 'track surface', variable: '--toggle-on-hover-track-surface' },
|
|
341
|
-
{ label: 'thumb surface', variable: '--toggle-on-hover-thumb-surface' },
|
|
342
|
-
],
|
|
343
|
-
disabled: [
|
|
344
|
-
{ label: 'track surface', variable: '--toggle-disabled-track-surface' },
|
|
345
|
-
{ label: 'thumb surface', variable: '--toggle-disabled-thumb-surface' },
|
|
346
|
-
{ label: 'label text', variable: '--toggle-disabled-label-text' },
|
|
347
|
-
],
|
|
348
|
-
};
|
|
349
|
-
export const allTokens: Token[] = Object.values(states).flat();
|
|
350
|
-
</script>
|
|
351
|
-
|
|
352
|
-
<script lang="ts">
|
|
353
|
-
import Toggle from './Toggle.svelte';
|
|
354
|
-
import {
|
|
355
|
-
VariantGroup, ComponentEditorBase,
|
|
356
|
-
} from '@motion-proto/live-tokens/component-editor';
|
|
357
|
-
|
|
358
|
-
function previewProps(state: string) {
|
|
359
|
-
return {
|
|
360
|
-
checked: state === 'on' || state === 'on hover',
|
|
361
|
-
disabled: state === 'disabled',
|
|
362
|
-
forceClass: state === 'hover' || state === 'on hover' ? 'force-hover' : '',
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
</script>
|
|
366
|
-
|
|
367
|
-
<ComponentEditorBase
|
|
368
|
-
{component}
|
|
369
|
-
title="Toggle"
|
|
370
|
-
description="On/off switch with sliding thumb."
|
|
371
|
-
tokens={allTokens}
|
|
372
|
-
>
|
|
373
|
-
<VariantGroup name="toggle" title="Toggle" {states} {component}>
|
|
374
|
-
{#snippet children({ activeState })}
|
|
375
|
-
{@const p = previewProps(activeState)}
|
|
376
|
-
<Toggle checked={p.checked} disabled={p.disabled} class={p.forceClass} label="Enable feature" />
|
|
377
|
-
{/snippet}
|
|
378
|
-
</VariantGroup>
|
|
379
|
-
</ComponentEditorBase>
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
What to notice:
|
|
383
|
-
|
|
384
|
-
- `component` is the string id; must match `registerComponent({ id })` exactly.
|
|
385
|
-
- `states` keys become the VariantGroup tab labels the user sees. Multi-word keys (`'on hover'`) need quoting.
|
|
386
|
-
- `allTokens` is a flat union of every state's tokens. The editor store needs it for reset-to-default and sibling resolution.
|
|
387
|
-
- `previewProps` translates the active editor tab into runtime props (`checked`, `disabled`, `force-hover` class).
|
|
388
|
-
- No `groupKey`, no `canBeLinked`: Toggle has no linked siblings. For components that share base properties across variants, see the linked-siblings extension below.
|
|
389
|
-
|
|
390
|
-
### Register: `src/main.ts`
|
|
391
|
-
|
|
392
|
-
```ts
|
|
393
|
-
import { bootLiveTokens } from '@motion-proto/live-tokens';
|
|
394
|
-
import App from './App.svelte';
|
|
395
|
-
import ToggleEditor, { allTokens as toggleTokens } from './system/components/ToggleEditor.svelte';
|
|
396
|
-
|
|
397
|
-
bootLiveTokens(App, '#app', {
|
|
398
|
-
components: [{
|
|
399
|
-
id: 'mytoggle', // unique id; don't reuse 'toggle'
|
|
400
|
-
label: 'My Toggle',
|
|
401
|
-
icon: 'fas fa-toggle-on',
|
|
402
|
-
sourceFile: 'src/system/components/Toggle.svelte',
|
|
403
|
-
editorComponent: ToggleEditor,
|
|
404
|
-
schema: toggleTokens,
|
|
405
|
-
}],
|
|
406
|
-
});
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
If your app mounts manually instead of via `bootLiveTokens`, call `registerComponent({ id: 'mytoggle', ... })` yourself before `mount(App, ...)`. If you reuse `id: 'toggle'`, the consumer's component wins over the built-in (with a console warning). The collision rule protects you, but for a fresh component pick an id that doesn't collide.
|
|
410
|
-
|
|
411
|
-
## Extension: linked siblings
|
|
412
|
-
|
|
413
|
-
Toggle's tokens are flat per state. Most multi-variant components (Badge, Card, SegmentedControl) share base properties across variants and surface that equality via a *linked block*: one edit propagates to every variant, while per-variant properties stay independent. Five additions to the Toggle pattern; see `BadgeEditor.svelte` in `node_modules` for the full file.
|
|
414
|
-
|
|
415
|
-
1. **Mark linkable tokens** with `canBeLinked: true` + a `groupKey`. Peers sharing a `groupKey` form a link set across variants.
|
|
416
|
-
|
|
417
|
-
```ts
|
|
418
|
-
function variantBaseTokens(v: Variant): Token[] {
|
|
419
|
-
return [
|
|
420
|
-
{ label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--badge-${v}-padding` },
|
|
421
|
-
{ label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--badge-${v}-radius` },
|
|
422
|
-
];
|
|
423
|
-
}
|
|
424
|
-
// Colors omit canBeLinked. Per-variant by design.
|
|
425
|
-
function variantColorTokens(v: Variant): Token[] {
|
|
426
|
-
return [
|
|
427
|
-
{ label: 'surface color', groupKey: 'surface', variable: `--badge-${v}-surface` },
|
|
428
|
-
{ label: 'text color', groupKey: 'text', variable: `--badge-${v}-text` },
|
|
429
|
-
];
|
|
430
|
-
}
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
2. **Build a `linkableContexts: Map<variable, contextLabel>`** in `<script module>`. The label (e.g. `"success base"`) is how the LinkageChart row identifies this variable. Plain literal Map, no helper needed.
|
|
189
|
+
What to notice in the editor file:
|
|
434
190
|
|
|
435
|
-
|
|
436
|
-
const linkableContexts = new Map<string, string>(
|
|
437
|
-
variants.flatMap((v) =>
|
|
438
|
-
variantBaseTokens(v)
|
|
439
|
-
.filter((t) => t.canBeLinked)
|
|
440
|
-
.map((t) => [t.variable, `${v} base`] as [string, string]),
|
|
441
|
-
),
|
|
442
|
-
);
|
|
443
|
-
```
|
|
191
|
+
- No `groupKey`, no `canBeLinked`: Toggle has no linked siblings. For components that share base properties across variants, read `references/linked-siblings.md`.
|
|
444
192
|
|
|
445
|
-
|
|
193
|
+
For your own component, copy the pattern and substitute your id. Registering against a built-in id wins with a console warning, but the right call is a unique id.
|
|
446
194
|
|
|
447
|
-
|
|
448
|
-
import { editorState } from '@motion-proto/live-tokens';
|
|
449
|
-
import { computeLinkedBlock, withLinkedDisabled, buildSiblings }
|
|
450
|
-
from '@motion-proto/live-tokens/component-editor';
|
|
451
|
-
|
|
452
|
-
let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
|
|
453
|
-
let visibleVariantStates = $derived((v: Variant) => Object.fromEntries(
|
|
454
|
-
Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
|
|
455
|
-
));
|
|
456
|
-
```
|
|
195
|
+
## Extensions
|
|
457
196
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
5. **Multi-variant editors iterate VariantGroups** with `buildSiblings` so cross-variant link rows resolve to their peers.
|
|
461
|
-
|
|
462
|
-
```svelte
|
|
463
|
-
<ComponentEditorBase {component} title="Badge" tokens={allTokens} {linked} variants={variantOptions}>
|
|
464
|
-
{#each variants as v}
|
|
465
|
-
<VariantGroup
|
|
466
|
-
name={v}
|
|
467
|
-
title={v}
|
|
468
|
-
states={visibleVariantStates(v)}
|
|
469
|
-
{component}
|
|
470
|
-
siblings={buildSiblings(variants, v, variantStates)}
|
|
471
|
-
>
|
|
472
|
-
...preview snippet
|
|
473
|
-
</VariantGroup>
|
|
474
|
-
{/each}
|
|
475
|
-
</ComponentEditorBase>
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
Single-variant components with multi-state linked tokens still set `canBeLinked` + `linkableContexts`, but skip `buildSiblings` and the `{#each}` loop. Components with no linked tokens (Toggle, SectionDivider) skip all five steps — `ComponentEditorBase` renders fine without a `{linked}` prop.
|
|
479
|
-
|
|
480
|
-
## Extension: intrinsics
|
|
481
|
-
|
|
482
|
-
Some components expose **structural or display choices** that aren't token values: an alignment (start / center), an element's visibility (show / hide), a layout position. These ride a bespoke `<select>` or checkbox you author in an editor snippet, not the generic token grid, so they don't belong in `allTokens`. Toggle and most components have none. SectionDivider is the worked example (alignment, hairline position, eyebrow / description visibility).
|
|
483
|
-
|
|
484
|
-
An intrinsic still cascades through a CSS custom property with a default in the runtime `:global(:root)`. The trap: that default now lives in two places, the runtime `:global(:root)` AND the editor's read-back getter. When they disagree the control displays a state the page never renders, and a native `<select>` won't even fire `onchange` to write the "change" the user thinks they made. `:global(:root)` is the source of truth.
|
|
485
|
-
|
|
486
|
-
Declare intrinsics so the editor and the contract test stay honest:
|
|
487
|
-
|
|
488
|
-
1. **Runtime `:global(:root)`** carries the per-variant default like any other variable:
|
|
489
|
-
|
|
490
|
-
```css
|
|
491
|
-
--mywidget-lg-align: start;
|
|
492
|
-
--mywidget-lg-eyebrow-display: block;
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
2. **Editor `<script module>`** exports `intrinsics: IntrinsicSpec[]`, one entry per structural property, each `default` mirroring `:global(:root)` per variant:
|
|
496
|
-
|
|
497
|
-
```ts
|
|
498
|
-
import type { IntrinsicSpec } from '@motion-proto/live-tokens/component-editor';
|
|
499
|
-
|
|
500
|
-
export const intrinsics: IntrinsicSpec[] = [
|
|
501
|
-
{
|
|
502
|
-
key: 'align',
|
|
503
|
-
variants: ['lg', 'md', 'sm'],
|
|
504
|
-
variable: (v) => `--mywidget-${v}-align`,
|
|
505
|
-
values: ['start', 'center'],
|
|
506
|
-
default: { lg: 'start', md: 'start', sm: 'start' },
|
|
507
|
-
},
|
|
508
|
-
];
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
3. **Read-back getters fall back to the spec default**, never a hard-coded constant. This is the rule that keeps the control's displayed default in step with what an unedited instance renders:
|
|
512
|
-
|
|
513
|
-
```ts
|
|
514
|
-
const byKey = new Map(intrinsics.map((i) => [i.key, i]));
|
|
515
|
-
function readIntrinsic(key: string, v: string): string {
|
|
516
|
-
const spec = byKey.get(key)!;
|
|
517
|
-
const raw = readLiteral(spec.variable(v)) ?? spec.default[v]; // store override, else runtime default
|
|
518
|
-
return spec.normalize ? spec.normalize(raw) : raw;
|
|
519
|
-
}
|
|
520
|
-
function getAlign(v: string) {
|
|
521
|
-
return readIntrinsic('align', v) === 'center' ? 'center' : 'start';
|
|
522
|
-
}
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
Writes go through `setComponentAlias(component, spec.variable(v), { kind: 'literal', value })` so the choice cascades to `:root` like any token.
|
|
526
|
-
|
|
527
|
-
4. **Pass `intrinsics` to `registerComponent`** so the contract test can see it:
|
|
528
|
-
|
|
529
|
-
```ts
|
|
530
|
-
registerComponent({
|
|
531
|
-
id: 'mywidget',
|
|
532
|
-
// ...label, icon, sourceFile, editorComponent, schema...
|
|
533
|
-
intrinsics: myWidgetIntrinsics,
|
|
534
|
-
});
|
|
535
|
-
```
|
|
197
|
+
Read the one you need; most components need neither.
|
|
536
198
|
|
|
537
|
-
|
|
199
|
+
- `references/linked-siblings.md`: variants that share base properties and should move together (Badge, Card, SegmentedControl).
|
|
200
|
+
- `references/intrinsics.md`: structural or display choices that are not token values (an alignment, an element's visibility), where the runtime default and the editor's read-back must agree.
|
|
538
201
|
|
|
539
202
|
## Verification checklist
|
|
540
203
|
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Fixed overlays must portal to body
|
|
2
|
+
|
|
3
|
+
Any `position: fixed` layer (modal, lightbox, full-screen backdrop) is trapped — clipped or painted under other chrome — by a transformed / `isolation` / `contain` / `will-change` ancestor, which real consumer pages (and the editor's own preview pane) commonly have. Render the fixed layer with `use:portal` from `src/system/internal/portal.ts` so it escapes to `<body>`; pass `use:portal={enabled}` to keep an in-flow preview variant in place (see `Dialog`). `check:overlay-portal` fails the build if a component has `position: fixed` without it. Anchored popovers (`position: absolute` relative to a trigger, like `Tooltip`) are exempt. Two consequences of moving to `<body>`: DOM events from the layer no longer bubble to a consumer ancestor (use component callbacks, as `Dialog` does), and a subtree-scoped CSS-variable theme no longer reaches it (this library themes via `:root`, so fine here). If the layer is a modal, also give it `role="dialog"` + `aria-modal`, move focus in on open and restore it on close, and trap `Tab` (see `ImageLightbox`).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Extension: intrinsics
|
|
2
|
+
|
|
3
|
+
Some components expose **structural or display choices** that aren't token values: an alignment (start / center), an element's visibility (show / hide), a layout position. These ride a bespoke `<select>` or checkbox you author in an editor snippet, not the generic token grid, so they don't belong in `allTokens`. Toggle and most components have none. SectionDivider is the worked example (alignment, hairline position, eyebrow / description visibility).
|
|
4
|
+
|
|
5
|
+
An intrinsic still cascades through a CSS custom property with a default in the runtime `:global(:root)`. The trap: that default now lives in two places, the runtime `:global(:root)` AND the editor's read-back getter. When they disagree the control displays a state the page never renders, and a native `<select>` won't even fire `onchange` to write the "change" the user thinks they made. `:global(:root)` is the source of truth.
|
|
6
|
+
|
|
7
|
+
Declare intrinsics so the editor and the contract test stay honest:
|
|
8
|
+
|
|
9
|
+
1. **Runtime `:global(:root)`** carries the per-variant default like any other variable:
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
--mywidget-lg-align: start;
|
|
13
|
+
--mywidget-lg-eyebrow-display: block;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
2. **Editor `<script module>`** exports `intrinsics: IntrinsicSpec[]`, one entry per structural property, each `default` mirroring `:global(:root)` per variant:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import type { IntrinsicSpec } from '@motion-proto/live-tokens/component-editor';
|
|
20
|
+
|
|
21
|
+
export const intrinsics: IntrinsicSpec[] = [
|
|
22
|
+
{
|
|
23
|
+
key: 'align',
|
|
24
|
+
variants: ['lg', 'md', 'sm'],
|
|
25
|
+
variable: (v) => `--mywidget-${v}-align`,
|
|
26
|
+
values: ['start', 'center'],
|
|
27
|
+
default: { lg: 'start', md: 'start', sm: 'start' },
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. **Read-back getters fall back to the spec default**, never a hard-coded constant. This is the rule that keeps the control's displayed default in step with what an unedited instance renders:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const byKey = new Map(intrinsics.map((i) => [i.key, i]));
|
|
36
|
+
function readIntrinsic(key: string, v: string): string {
|
|
37
|
+
const spec = byKey.get(key)!;
|
|
38
|
+
const raw = readLiteral(spec.variable(v)) ?? spec.default[v]; // store override, else runtime default
|
|
39
|
+
return spec.normalize ? spec.normalize(raw) : raw;
|
|
40
|
+
}
|
|
41
|
+
function getAlign(v: string) {
|
|
42
|
+
return readIntrinsic('align', v) === 'center' ? 'center' : 'start';
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Writes go through `setComponentAlias(component, spec.variable(v), { kind: 'literal', value })` so the choice cascades to `:root` like any token.
|
|
47
|
+
|
|
48
|
+
4. **Pass `intrinsics` to `registerComponent`** so the contract test can see it:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
registerComponent({
|
|
52
|
+
id: 'mywidget',
|
|
53
|
+
// ...label, icon, sourceFile, editorComponent, schema...
|
|
54
|
+
intrinsics: myWidgetIntrinsics,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use `normalize` only when two raw values render identically and the dropdown lists just one (SectionDivider folds `above-description` into `below-label`). Properties that look like intrinsics but aren't: preview-only props with no persistence (a size selector that only changes the demo), `setComponentConfig` editor metadata (Dialog's button variants), and token-valued selects (a control choosing between two tokens). None carry a duplicated runtime default, so none need an `IntrinsicSpec`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Extension: linked siblings
|
|
2
|
+
|
|
3
|
+
Read this when your component has more than one variant and those variants share base properties (surface, radius, padding) that should move together. Toggle and SectionDivider have no linked tokens and skip all of it.
|
|
4
|
+
|
|
5
|
+
Toggle's tokens are flat per state. Most multi-variant components (Badge, Card, SegmentedControl) share base properties across variants and surface that equality via a *linked block*: one edit propagates to every variant, while per-variant properties stay independent. Five additions to the Toggle pattern; see `BadgeEditor.svelte` in `node_modules` for the full file.
|
|
6
|
+
|
|
7
|
+
1. **Mark linkable tokens** with `canBeLinked: true` + a `groupKey`. Peers sharing a `groupKey` form a link set across variants.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
function variantBaseTokens(v: Variant): Token[] {
|
|
11
|
+
return [
|
|
12
|
+
{ label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--badge-${v}-padding` },
|
|
13
|
+
{ label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--badge-${v}-radius` },
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
// Colors omit canBeLinked. Per-variant by design.
|
|
17
|
+
function variantColorTokens(v: Variant): Token[] {
|
|
18
|
+
return [
|
|
19
|
+
{ label: 'surface color', groupKey: 'surface', variable: `--badge-${v}-surface` },
|
|
20
|
+
{ label: 'text color', groupKey: 'text', variable: `--badge-${v}-text` },
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. **Build a `linkableContexts: Map<variable, contextLabel>`** in `<script module>`. The label (e.g. `"success base"`) is how the LinkageChart row identifies this variable. Plain literal Map, no helper needed.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const linkableContexts = new Map<string, string>(
|
|
29
|
+
variants.flatMap((v) =>
|
|
30
|
+
variantBaseTokens(v)
|
|
31
|
+
.filter((t) => t.canBeLinked)
|
|
32
|
+
.map((t) => [t.variable, `${v} base`] as [string, string]),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. **Compute `linked` and mask currently-linked rows** out of per-state lists, so they render once inside the LinkedBlock instead of twice.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { editorState } from '@motion-proto/live-tokens';
|
|
41
|
+
import { computeLinkedBlock, withLinkedDisabled, buildSiblings }
|
|
42
|
+
from '@motion-proto/live-tokens/component-editor';
|
|
43
|
+
|
|
44
|
+
let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
|
|
45
|
+
let visibleVariantStates = $derived((v: Variant) => Object.fromEntries(
|
|
46
|
+
Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
|
|
47
|
+
));
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
4. **Pass `{linked}` to `ComponentEditorBase`** so the LinkedBlock renders above the variant groups.
|
|
51
|
+
|
|
52
|
+
5. **Multi-variant editors iterate VariantGroups** with `buildSiblings` so cross-variant link rows resolve to their peers.
|
|
53
|
+
|
|
54
|
+
```svelte
|
|
55
|
+
<ComponentEditorBase {component} title="Badge" tokens={allTokens} {linked} variants={variantOptions}>
|
|
56
|
+
{#each variants as v}
|
|
57
|
+
<VariantGroup
|
|
58
|
+
name={v}
|
|
59
|
+
title={v}
|
|
60
|
+
states={visibleVariantStates(v)}
|
|
61
|
+
{component}
|
|
62
|
+
siblings={buildSiblings(variants, v, variantStates)}
|
|
63
|
+
>
|
|
64
|
+
...preview snippet
|
|
65
|
+
</VariantGroup>
|
|
66
|
+
{/each}
|
|
67
|
+
</ComponentEditorBase>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Single-variant components with multi-state linked tokens still set `canBeLinked` + `linkableContexts`, but skip `buildSiblings` and the `{#each}` loop. Components with no linked tokens (Toggle, SectionDivider) skip all five steps — `ComponentEditorBase` renders fine without a `{linked}` prop.
|