@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.
Files changed (61) hide show
  1. package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
  2. package/README.md +84 -29
  3. package/dist-plugin/index.cjs +177 -125
  4. package/dist-plugin/index.d.cts +3 -2
  5. package/dist-plugin/index.d.ts +3 -2
  6. package/dist-plugin/index.js +177 -125
  7. package/package.json +8 -2
  8. package/src/editor/component-editor/BadgeEditor.svelte +44 -42
  9. package/src/editor/component-editor/ButtonEditor.svelte +224 -0
  10. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
  11. package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
  12. package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
  13. package/src/editor/component-editor/InputEditor.svelte +272 -0
  14. package/src/editor/component-editor/NotificationEditor.svelte +44 -65
  15. package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
  16. package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
  17. package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
  18. package/src/editor/component-editor/index.ts +16 -1
  19. package/src/editor/component-editor/registry.ts +138 -28
  20. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
  21. package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
  22. package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
  23. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
  24. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
  25. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
  26. package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
  27. package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
  28. package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
  29. package/src/editor/component-editor/scaffolding/types.ts +2 -1
  30. package/src/editor/core/components/componentConfigKeys.ts +14 -3
  31. package/src/editor/core/components/componentConfigService.ts +7 -6
  32. package/src/editor/core/manifests/manifestService.ts +5 -4
  33. package/src/editor/core/storage/apiBase.ts +15 -0
  34. package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
  35. package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
  36. package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
  37. package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
  38. package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
  39. package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
  40. package/src/editor/core/themes/migrations/index.ts +10 -0
  41. package/src/editor/core/themes/slices/components.ts +9 -0
  42. package/src/editor/core/themes/themeInit.ts +3 -2
  43. package/src/editor/core/themes/themeService.ts +3 -2
  44. package/src/editor/index.ts +10 -1
  45. package/src/editor/pages/ComponentEditorPage.svelte +53 -3
  46. package/src/editor/pages/EditorShell.svelte +53 -3
  47. package/src/editor/ui/UIEasingSelector.svelte +240 -0
  48. package/src/editor/ui/variantScales.ts +34 -0
  49. package/src/system/components/Button.svelte +34 -85
  50. package/src/system/components/CollapsibleSection.svelte +1 -48
  51. package/src/system/components/CornerBadge.svelte +72 -138
  52. package/src/system/components/Dialog.svelte +24 -4
  53. package/src/system/components/ImageLightbox.svelte +578 -0
  54. package/src/system/components/Input.svelte +387 -0
  55. package/src/system/components/ProgressBar.svelte +62 -258
  56. package/src/system/components/SectionDivider.svelte +117 -43
  57. package/src/system/components/SegmentedControl.svelte +81 -15
  58. package/src/system/components/SideNavigation.svelte +777 -0
  59. package/src/system/styles/tokens.css +43 -0
  60. package/src/system/styles/tokens.generated.css +4 -183
  61. package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
@@ -5,7 +5,7 @@
5
5
  import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
6
6
  import ManifestFileManager from '../ui/ManifestFileManager.svelte';
7
7
  import { navigate } from '../core/routing/router';
8
- import { componentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
8
+ import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
9
9
  import { listComponents } from '../core/components/componentConfigService';
10
10
  import { selectedComponent } from '../core/store/editorViewStore';
11
11
  import { componentDirty } from '../core/store/editorStore';
@@ -96,7 +96,9 @@
96
96
  window.removeEventListener('keydown', handleKeydown);
97
97
  });
98
98
 
99
- const componentNavItems = componentRegistryEntries.map(({ id, label, icon }) => ({ id, label, icon }));
99
+ const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
100
+ const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
101
+ const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
100
102
  </script>
101
103
 
102
104
  <!--
@@ -146,7 +148,7 @@
146
148
  {/if}
147
149
  </div>
148
150
  <div class="nav-items">
149
- {#each componentNavItems as item}
151
+ {#each systemNavItems as item}
150
152
  <button
151
153
  class="nav-item"
152
154
  class:active={$selectedComponent === item.id}
@@ -162,6 +164,27 @@
162
164
  {/if}
163
165
  </button>
164
166
  {/each}
167
+ {#if customNavItems.length > 0}
168
+ <div class="nav-divider">
169
+ <span class="nav-divider-label">Custom</span>
170
+ </div>
171
+ {#each customNavItems as item}
172
+ <button
173
+ class="nav-item"
174
+ class:active={$selectedComponent === item.id}
175
+ class:dirty={$componentDirty[item.id]}
176
+ onmouseenter={(e) => showHint(item.label, e.currentTarget)}
177
+ onmouseleave={hideHint}
178
+ onclick={() => selectComponent(item.id)}
179
+ >
180
+ <i class={item.icon}></i>
181
+ <span class="rail-label">{item.label}</span>
182
+ {#if $componentDirty[item.id]}
183
+ <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
184
+ {/if}
185
+ </button>
186
+ {/each}
187
+ {/if}
165
188
  </div>
166
189
  {#if drawerOpen}
167
190
  <div class="sidebar-footer">
@@ -331,6 +354,33 @@
331
354
  background: black;
332
355
  }
333
356
 
357
+ /* Divider between SYSTEM and CUSTOM groups. The horizontal line uses the
358
+ dimmer border token (sub-element separator), with an uppercase eyebrow
359
+ label that fades out when the rail is collapsed so the line still reads. */
360
+ .nav-divider {
361
+ display: grid;
362
+ grid-template-columns: 48px 1fr;
363
+ align-items: center;
364
+ height: 28px;
365
+ margin-top: var(--ui-space-8);
366
+ border-top: 1px solid var(--ui-border-low);
367
+ }
368
+
369
+ .nav-divider-label {
370
+ grid-column: 2;
371
+ font-size: var(--ui-font-size-xs);
372
+ font-weight: var(--ui-font-weight-semibold);
373
+ color: var(--ui-text-tertiary);
374
+ text-transform: uppercase;
375
+ letter-spacing: 0.04em;
376
+ opacity: 0;
377
+ transition: opacity 180ms ease;
378
+ }
379
+
380
+ .components-shell.rail-expanded .nav-divider-label {
381
+ opacity: 1;
382
+ }
383
+
334
384
  .sidebar-footer {
335
385
  flex-shrink: 0;
336
386
  margin-top: auto;
@@ -13,7 +13,7 @@
13
13
  import { editorState } from '../core/store/editorStore';
14
14
  import { editorView, sidebarCondensed, selectedComponent } from '../core/store/editorViewStore';
15
15
  import { componentDirty } from '../core/store/editorStore';
16
- import { componentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
16
+ import { getComponentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
17
17
  import { listComponents } from '../core/components/componentConfigService';
18
18
 
19
19
  const tokenNavItems = [
@@ -29,7 +29,9 @@
29
29
  { id: 'utility-tokens', label: 'Utility Tokens', icon: 'fas fa-sliders' }
30
30
  ];
31
31
 
32
- const componentNavItems = componentRegistryEntries.map(({ id, label, icon }) => ({ id, label, icon }));
32
+ const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
33
+ const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
34
+ const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
33
35
 
34
36
  let selectedTokenSection: string | null = $state(null);
35
37
  let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
@@ -149,7 +151,7 @@
149
151
  {/if}
150
152
  {:else}
151
153
  <div class="nav-items">
152
- {#each componentNavItems as item}
154
+ {#each systemNavItems as item}
153
155
  <button
154
156
  class="nav-item"
155
157
  class:active={$selectedComponent === item.id}
@@ -165,6 +167,27 @@
165
167
  {/if}
166
168
  </button>
167
169
  {/each}
170
+ {#if customNavItems.length > 0}
171
+ <div class="nav-divider">
172
+ <span class="nav-divider-label">Custom</span>
173
+ </div>
174
+ {#each customNavItems as item}
175
+ <button
176
+ class="nav-item"
177
+ class:active={$selectedComponent === item.id}
178
+ class:dirty={$componentDirty[item.id]}
179
+ onmouseenter={(e) => showHint(item.label, e.currentTarget)}
180
+ onmouseleave={hideHint}
181
+ onclick={() => selectComponent(item.id)}
182
+ >
183
+ <i class={item.icon}></i>
184
+ <span class="nav-label">{item.label}</span>
185
+ {#if $componentDirty[item.id]}
186
+ <span class="dirty-dot" aria-label="Unsaved changes" title="Unsaved changes"></span>
187
+ {/if}
188
+ </button>
189
+ {/each}
190
+ {/if}
168
191
  </div>
169
192
  {#if !condensed}
170
193
  <div class="sidebar-footer">
@@ -248,6 +271,33 @@
248
271
  flex-shrink: 0;
249
272
  }
250
273
 
274
+ /* Divider between SYSTEM and CUSTOM groups. The horizontal line uses the
275
+ dimmer border token (sub-element separator), with an uppercase eyebrow
276
+ label that fades out when the rail is condensed so the line still reads. */
277
+ .nav-divider {
278
+ display: grid;
279
+ grid-template-columns: 48px 1fr;
280
+ align-items: center;
281
+ height: 28px;
282
+ margin-top: var(--ui-space-8);
283
+ border-top: 1px solid var(--ui-border-low);
284
+ }
285
+
286
+ .nav-divider-label {
287
+ grid-column: 2;
288
+ font-size: var(--ui-font-size-xs);
289
+ font-weight: var(--ui-font-weight-semibold);
290
+ color: var(--ui-text-tertiary);
291
+ text-transform: uppercase;
292
+ letter-spacing: 0.04em;
293
+ opacity: 1;
294
+ transition: opacity 180ms ease;
295
+ }
296
+
297
+ .layout.condensed .nav-divider-label {
298
+ opacity: 0;
299
+ }
300
+
251
301
  .nav-item {
252
302
  position: relative;
253
303
  display: grid;
@@ -0,0 +1,240 @@
1
+ <script lang="ts">
2
+ import { resolveAliasChain } from '../core/palettes/tokenRegistry';
3
+ import UITokenSelector from './UITokenSelector.svelte';
4
+ import UIOptionList from './UIOptionList.svelte';
5
+ import UIOptionItem from './UIOptionItem.svelte';
6
+
7
+ interface Props {
8
+ variable: string;
9
+ component?: string | undefined;
10
+ canBeLinked?: boolean;
11
+ disabled?: boolean;
12
+ selectionsLocked?: boolean;
13
+ onchange?: () => void;
14
+ }
15
+
16
+ let {
17
+ variable,
18
+ component = undefined,
19
+ canBeLinked = false,
20
+ disabled = false,
21
+ selectionsLocked = false,
22
+ onchange,
23
+ }: Props = $props();
24
+
25
+ /** Family list mirrors easings.net's catalog. `linear` is the special-case
26
+ curve with no variant; all others take in/out/in-out. Keys are the suffix
27
+ portion of the corresponding `--ease-*` token name. */
28
+ const FAMILIES = [
29
+ { key: 'linear', label: 'Linear' },
30
+ { key: 'sine', label: 'Sine' },
31
+ { key: 'quad', label: 'Quad' },
32
+ { key: 'cubic', label: 'Cubic' },
33
+ { key: 'quart', label: 'Quart' },
34
+ { key: 'quint', label: 'Quint' },
35
+ { key: 'expo', label: 'Expo' },
36
+ { key: 'circ', label: 'Circ' },
37
+ { key: 'back', label: 'Back' },
38
+ { key: 'elastic', label: 'Elastic' },
39
+ { key: 'bounce', label: 'Bounce' },
40
+ ] as const;
41
+ type FamilyKey = typeof FAMILIES[number]['key'];
42
+
43
+ const VARIANTS = [
44
+ { key: 'in', label: 'In' },
45
+ { key: 'out', label: 'Out' },
46
+ { key: 'in-out', label: 'In-Out' },
47
+ ] as const;
48
+ type VariantKey = typeof VARIANTS[number]['key'];
49
+
50
+ const FAMILY_KEYS = new Set<string>(FAMILIES.map((f) => f.key));
51
+ const VARIANT_KEYS = new Set<string>(VARIANTS.map((v) => v.key));
52
+
53
+ let selector: UITokenSelector;
54
+ let chosenFamily: FamilyKey | null = $state(null);
55
+ let chosenVariant: VariantKey | null = $state(null);
56
+ let currentValue: string = $state('');
57
+
58
+ /** Parse `--ease-linear` or `--ease-<variant>-<family>` into its parts.
59
+ Returns nulls when the name doesn't match the ease-token convention. */
60
+ function parseEaseToken(varName: string): { family: FamilyKey | null; variant: VariantKey | null } {
61
+ if (varName === '--ease-linear') return { family: 'linear', variant: null };
62
+ const m = varName.match(/^--ease-(in-out|in|out)-([a-z]+)$/);
63
+ if (!m) return { family: null, variant: null };
64
+ const variant = m[1] as VariantKey;
65
+ const family = m[2] as FamilyKey;
66
+ if (!FAMILY_KEYS.has(family) || family === 'linear') return { family: null, variant: null };
67
+ if (!VARIANT_KEYS.has(variant)) return { family: null, variant: null };
68
+ return { family, variant };
69
+ }
70
+
71
+ function buildEaseToken(family: FamilyKey, variant: VariantKey | null): string | null {
72
+ if (family === 'linear') return '--ease-linear';
73
+ if (!variant) return null;
74
+ return `--ease-${variant}-${family}`;
75
+ }
76
+
77
+ /** Pull the var() reference out of `var(--ease-...)`, returns the inner name. */
78
+ function parseRef(raw: string): string | null {
79
+ const m = raw.match(/var\((--ease-[a-z-]+)\)/);
80
+ return m ? m[1] : null;
81
+ }
82
+
83
+ function readResolved() {
84
+ currentValue = getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
85
+ }
86
+
87
+ function initFromCurrent() {
88
+ readResolved();
89
+ const raw = document.documentElement.style.getPropertyValue(variable).trim();
90
+ if (raw) {
91
+ const inner = parseRef(raw);
92
+ if (inner) {
93
+ const parts = parseEaseToken(inner);
94
+ chosenFamily = parts.family;
95
+ chosenVariant = parts.variant;
96
+ return;
97
+ }
98
+ }
99
+ for (const alias of resolveAliasChain(variable)) {
100
+ const parts = parseEaseToken(alias);
101
+ if (parts.family) {
102
+ chosenFamily = parts.family;
103
+ chosenVariant = parts.variant;
104
+ return;
105
+ }
106
+ }
107
+ chosenFamily = null;
108
+ chosenVariant = null;
109
+ }
110
+
111
+ function handleReset() {
112
+ chosenFamily = null;
113
+ chosenVariant = null;
114
+ readResolved();
115
+ onchange?.();
116
+ }
117
+
118
+ /** Commit (family, variant) — writes the override if both halves are valid;
119
+ for non-linear families without a variant yet, defaults to `out`. Closes
120
+ the dropdown only when a full token name was produced. */
121
+ function commit(family: FamilyKey, variant: VariantKey | null, close: () => void): void {
122
+ let v = variant;
123
+ if (family !== 'linear' && !v) v = 'out';
124
+ const target = buildEaseToken(family, v);
125
+ if (!target) return;
126
+ if (target === variable) {
127
+ selector.writeOverride(null);
128
+ } else {
129
+ selector.writeOverride(target);
130
+ }
131
+ chosenFamily = family;
132
+ chosenVariant = family === 'linear' ? null : v;
133
+ readResolved();
134
+ close();
135
+ onchange?.();
136
+ }
137
+
138
+ function selectFamily(key: FamilyKey, close: () => void) {
139
+ commit(key, chosenVariant, close);
140
+ }
141
+ function selectVariant(key: VariantKey, close: () => void) {
142
+ if (!chosenFamily || chosenFamily === 'linear') return;
143
+ commit(chosenFamily, key, close);
144
+ }
145
+
146
+ let lastSeenVariable: string | null = null;
147
+ $effect(() => {
148
+ if (variable !== lastSeenVariable) {
149
+ lastSeenVariable = variable;
150
+ initFromCurrent();
151
+ }
152
+ });
153
+
154
+ let familyLabel = $derived(FAMILIES.find((f) => f.key === chosenFamily)?.label ?? '');
155
+ let variantLabel = $derived(VARIANTS.find((v) => v.key === chosenVariant)?.label ?? '');
156
+ let triggerTitleText = $derived(
157
+ chosenFamily === 'linear'
158
+ ? 'Linear'
159
+ : chosenFamily && chosenVariant
160
+ ? `${variantLabel} ${familyLabel}`
161
+ : chosenFamily
162
+ ? `${familyLabel} —`
163
+ : '',
164
+ );
165
+ </script>
166
+
167
+ <UITokenSelector
168
+ bind:this={selector}
169
+ {variable}
170
+ {component}
171
+ {canBeLinked}
172
+ {disabled}
173
+ {selectionsLocked}
174
+ dropdownMinWidth="18rem"
175
+ onreset={handleReset}
176
+ onvarChange={initFromCurrent}
177
+ >
178
+ {#snippet triggerTitle()}{triggerTitleText}{/snippet}
179
+ {#snippet triggerMeta()}{currentValue || '—'}{/snippet}
180
+
181
+ {#snippet children({ close })}
182
+ <div class="ease-grid" class:no-variants={chosenFamily === 'linear'}>
183
+ <div class="ease-col">
184
+ <span class="ease-col-label">Curve</span>
185
+ <UIOptionList>
186
+ {#each FAMILIES as fam (fam.key)}
187
+ <UIOptionItem
188
+ active={chosenFamily === fam.key}
189
+ onclick={() => selectFamily(fam.key, close)}
190
+ >
191
+ {#snippet label()}{fam.label}{/snippet}
192
+ </UIOptionItem>
193
+ {/each}
194
+ </UIOptionList>
195
+ </div>
196
+ {#if chosenFamily && chosenFamily !== 'linear'}
197
+ <div class="ease-col">
198
+ <span class="ease-col-label">Variant</span>
199
+ <UIOptionList>
200
+ {#each VARIANTS as v (v.key)}
201
+ <UIOptionItem
202
+ active={chosenVariant === v.key}
203
+ onclick={() => selectVariant(v.key, close)}
204
+ >
205
+ {#snippet label()}{v.label}{/snippet}
206
+ </UIOptionItem>
207
+ {/each}
208
+ </UIOptionList>
209
+ </div>
210
+ {/if}
211
+ </div>
212
+ {/snippet}
213
+ </UITokenSelector>
214
+
215
+ <style>
216
+ .ease-grid {
217
+ display: grid;
218
+ grid-template-columns: 1fr 1fr;
219
+ gap: var(--ui-space-8);
220
+ min-width: 18rem;
221
+ }
222
+ .ease-grid.no-variants {
223
+ grid-template-columns: 1fr;
224
+ }
225
+ .ease-col {
226
+ display: flex;
227
+ flex-direction: column;
228
+ gap: var(--ui-space-4);
229
+ min-width: 0;
230
+ }
231
+ .ease-col-label {
232
+ padding: var(--ui-space-4) var(--ui-space-8) 0;
233
+ font-size: var(--ui-font-size-xs);
234
+ font-weight: var(--ui-font-weight-semibold);
235
+ color: var(--ui-text-tertiary);
236
+ font-family: var(--ui-font-mono);
237
+ text-transform: uppercase;
238
+ letter-spacing: 0.04em;
239
+ }
240
+ </style>
@@ -106,3 +106,37 @@ export const DIVIDER_HEIGHT: VariantScaleEntry = {
106
106
  { key: 'full', label: 'Full', value: '100%' },
107
107
  ],
108
108
  };
109
+
110
+ /** Used by `*-duration` variables (CSS transition timing). Pulls keys directly
111
+ * from the shared `--duration-*` scale in tokens.css — labels mirror the token
112
+ * slugs so the picker reflects the utility token namespace, no synthetic names. */
113
+ export const DURATION: VariantScaleEntry = {
114
+ varPrefix: '--duration-',
115
+ options: [
116
+ { key: '75', label: '75', value: '75ms' },
117
+ { key: '150', label: '150', value: '150ms' },
118
+ { key: '200', label: '200', value: '200ms' },
119
+ { key: '300', label: '300', value: '300ms' },
120
+ { key: '500', label: '500', value: '500ms' },
121
+ { key: '750', label: '750', value: '750ms' },
122
+ { key: '1000', label: '1000', value: '1000ms' },
123
+ ],
124
+ };
125
+
126
+ /** Used by `*-divider-inset` variables (margin-block trimmed off a stretched
127
+ * divider). Labels describe the resulting divider, not the inset value: 0
128
+ * inset = bar-height divider ("Full"); larger insets = shorter divider.
129
+ * Replaces the percentage-height approach that collapsed to 0 in
130
+ * auto-height flex parents. */
131
+ export const DIVIDER_INSET: VariantScaleEntry = {
132
+ varPrefix: '--space-',
133
+ options: [
134
+ { key: '0', label: 'Full', value: '0px' },
135
+ { key: '2', label: 'Tall', value: '0.125rem' },
136
+ { key: '4', label: 'Large', value: '0.25rem' },
137
+ { key: '6', label: 'Medium', value: '0.375rem' },
138
+ { key: '8', label: 'Short', value: '0.5rem' },
139
+ { key: '12', label: 'XS', value: '0.75rem' },
140
+ { key: '16', label: 'Tiny', value: '1rem' },
141
+ ],
142
+ };