@motion-proto/live-tokens 0.9.0 → 0.11.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 (65) hide show
  1. package/README.md +50 -29
  2. package/dist-plugin/index.cjs +177 -125
  3. package/dist-plugin/index.d.cts +3 -2
  4. package/dist-plugin/index.d.ts +3 -2
  5. package/dist-plugin/index.js +177 -125
  6. package/package.json +4 -1
  7. package/src/editor/component-editor/BadgeEditor.svelte +44 -42
  8. package/src/editor/component-editor/ButtonEditor.svelte +224 -0
  9. package/src/editor/component-editor/CardEditor.svelte +2 -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/registry.ts +35 -2
  19. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
  20. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +10 -1
  21. package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
  22. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
  23. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
  24. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
  25. package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
  26. package/src/editor/component-editor/scaffolding/types.ts +2 -1
  27. package/src/editor/core/components/componentConfigService.ts +7 -6
  28. package/src/editor/core/manifests/manifestService.ts +5 -4
  29. package/src/editor/core/storage/apiBase.ts +15 -0
  30. package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
  31. package/src/editor/core/store/editorRenderer.ts +1 -4
  32. package/src/editor/core/store/editorStore.ts +5 -9
  33. package/src/editor/core/store/editorTypes.ts +6 -13
  34. package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -29
  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/2026-05-26-drop-overlay-extra-stops.ts +46 -0
  41. package/src/editor/core/themes/migrations/index.ts +16 -0
  42. package/src/editor/core/themes/slices/overlays.ts +44 -75
  43. package/src/editor/core/themes/themeInit.ts +3 -2
  44. package/src/editor/core/themes/themeService.ts +3 -2
  45. package/src/editor/ui/SurfacesTab.svelte +3 -7
  46. package/src/editor/ui/UIEasingSelector.svelte +240 -0
  47. package/src/editor/ui/UIPaddingSelector.svelte +2 -2
  48. package/src/editor/ui/UIPaletteSelector.svelte +151 -36
  49. package/src/editor/ui/{UIRelinkConfirmPopover.svelte → UIRelinkConfirmDialog.svelte} +107 -75
  50. package/src/editor/ui/UITokenSelector.svelte +15 -2
  51. package/src/editor/ui/sections/OverlaysSection.svelte +107 -540
  52. package/src/editor/ui/variantScales.ts +34 -0
  53. package/src/system/components/Button.svelte +34 -85
  54. package/src/system/components/Card.svelte +2 -1
  55. package/src/system/components/CollapsibleSection.svelte +1 -48
  56. package/src/system/components/CornerBadge.svelte +72 -138
  57. package/src/system/components/ImageLightbox.svelte +578 -0
  58. package/src/system/components/Input.svelte +387 -0
  59. package/src/system/components/ProgressBar.svelte +62 -258
  60. package/src/system/components/SegmentedControl.svelte +81 -15
  61. package/src/system/components/SideNavigation.svelte +777 -0
  62. package/src/system/components/TabBar.svelte +1 -1
  63. package/src/system/styles/tokens.css +48 -5
  64. package/src/system/styles/tokens.generated.css +33 -185
  65. package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
@@ -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>
@@ -18,7 +18,7 @@
18
18
  unlinkComponentProperty,
19
19
  relinkComponentProperty,
20
20
  } from '../core/store/editorStore';
21
- import UIRelinkConfirmPopover from './UIRelinkConfirmPopover.svelte';
21
+ import UIRelinkConfirmDialog from './UIRelinkConfirmDialog.svelte';
22
22
  import UILinkToggle from './UILinkToggle.svelte';
23
23
 
24
24
  interface Props {
@@ -383,7 +383,7 @@
383
383
  <div class="link-toggle-wrap">
384
384
  <UILinkToggle linked={isLinkedParent} ontoggle={toggleLinkPaddingGroup} />
385
385
  {#if relinkOpen && component}
386
- <UIRelinkConfirmPopover
386
+ <UIRelinkConfirmDialog
387
387
  candidates={relinkCandidates}
388
388
  initialVariable={variable}
389
389
  prefixToStrip={`--${component}-`}
@@ -41,7 +41,14 @@
41
41
  * rendered disabled and not clickable. Out-of-family already-set
42
42
  * choices still surface in the trigger meta. */
43
43
  familyFilter?: string | null;
44
+ /** When false, omit the "None" (transparent) option from the family list.
45
+ * Slots that must always paint something (e.g. overlay backdrops) opt
46
+ * out — picking None would just degenerate to opacity 0. */
47
+ showNone?: boolean;
44
48
  onchange?: () => void;
49
+ /** Forwarded to UITokenSelector — when set, writes route through this
50
+ * callback instead of the DOM. See UITokenSelector.onwrite. */
51
+ onwrite?: (value: string | null) => void;
45
52
  }
46
53
 
47
54
  let {
@@ -51,7 +58,9 @@
51
58
  disabled = false,
52
59
  selectionsLocked = false,
53
60
  familyFilter = null,
61
+ showNone = true,
54
62
  onchange,
63
+ onwrite,
55
64
  }: Props = $props();
56
65
 
57
66
  type Category = 'palette' | 'surface' | 'border' | 'text';
@@ -137,6 +146,9 @@
137
146
  let chosenFamily = $state<string | null>(null);
138
147
  let chosenStep = $state<string | null>(null);
139
148
  let chosenNone = $state(false);
149
+ /** Picked one of the invariants. Bypasses category/family/step — these are
150
+ * not part of any ramp and always resolve to pure white/black. */
151
+ let chosenStatic = $state<'white' | 'black' | null>(null);
140
152
  let chosenGradient = $state<string | null>(null);
141
153
  /** Per-slot angle override on the chosen linear gradient. Null means
142
154
  * "no override" — the slot writes `var(--gradient-N)` and inherits the
@@ -243,6 +255,14 @@
243
255
  return { inner: m[1], opacity: parseInt(m[2]) };
244
256
  }
245
257
 
258
+ function parseStatic(raw: string): { name: 'white' | 'black'; opacity: number } | null {
259
+ const direct = raw.match(/^var\(--color-(white|black)\)$/);
260
+ if (direct) return { name: direct[1] as 'white' | 'black', opacity: 100 };
261
+ const wrapped = raw.match(/^color-mix\(in srgb,\s*var\(--color-(white|black)\)\s+(\d+)%,\s*transparent\)$/);
262
+ if (wrapped) return { name: wrapped[1] as 'white' | 'black', opacity: parseInt(wrapped[2]) };
263
+ return null;
264
+ }
265
+
246
266
  function buildValue(varName: string): string | null {
247
267
  if (varName === variable && opacity >= 100) return null;
248
268
  if (opacity >= 100) return varName;
@@ -251,6 +271,11 @@
251
271
 
252
272
  function applyOpacity() {
253
273
  opacity = Math.max(0, Math.min(100, Math.round(opacity)));
274
+ if (chosenStatic !== null) {
275
+ selector?.writeOverride(buildValue(`--color-${chosenStatic}`));
276
+ onchange?.();
277
+ return;
278
+ }
254
279
  if (chosenCategory === null || chosenFamily === null || chosenStep === null) return;
255
280
  const varName = getVarName(chosenCategory, chosenFamily, chosenStep);
256
281
  selector?.writeOverride(buildValue(varName));
@@ -323,6 +348,7 @@
323
348
 
324
349
  if (raw === 'transparent') {
325
350
  chosenNone = true;
351
+ chosenStatic = null;
326
352
  chosenCategory = null;
327
353
  chosenFamily = null;
328
354
  chosenStep = null;
@@ -362,6 +388,17 @@
362
388
  chosenGradient = null;
363
389
  chosenAngle = null;
364
390
 
391
+ const staticParsed = parseStatic(raw);
392
+ if (staticParsed) {
393
+ chosenStatic = staticParsed.name;
394
+ chosenCategory = null;
395
+ chosenFamily = null;
396
+ chosenStep = null;
397
+ opacity = staticParsed.opacity;
398
+ return;
399
+ }
400
+ chosenStatic = null;
401
+
365
402
  const opacityParsed = parseOpacity(raw);
366
403
  if (opacityParsed) {
367
404
  const parsed = parseRef(opacityParsed.inner);
@@ -424,6 +461,7 @@
424
461
 
425
462
  function selectNone(close: () => void) {
426
463
  chosenNone = true;
464
+ chosenStatic = null;
427
465
  chosenCategory = null;
428
466
  chosenFamily = null;
429
467
  chosenStep = null;
@@ -436,9 +474,24 @@
436
474
  onchange?.();
437
475
  }
438
476
 
477
+ function selectStatic(name: 'white' | 'black', close: () => void) {
478
+ chosenNone = false;
479
+ chosenStatic = name;
480
+ chosenCategory = null;
481
+ chosenFamily = null;
482
+ chosenStep = null;
483
+ chosenGradient = null;
484
+ chosenAngle = null;
485
+ selector?.writeOverride(buildValue(`--color-${name}`));
486
+ selectedFamily = null;
487
+ close();
488
+ onchange?.();
489
+ }
490
+
439
491
  function selectSwatch(category: Category, step: string, close: () => void) {
440
492
  const varName = getVarName(category, selectedFamily!, step);
441
493
  chosenNone = false;
494
+ chosenStatic = null;
442
495
  chosenGradient = null;
443
496
  chosenAngle = null;
444
497
  chosenCategory = category;
@@ -456,6 +509,7 @@
456
509
  // token's default angle but with no local override active.
457
510
  function selectGradient(gradientVar: string, close: () => void) {
458
511
  chosenNone = false;
512
+ chosenStatic = null;
459
513
  chosenCategory = null;
460
514
  chosenFamily = null;
461
515
  chosenStep = null;
@@ -493,11 +547,13 @@
493
547
 
494
548
  let metaLabel = $derived(chosenNone
495
549
  ? 'none'
496
- : chosenGradient
497
- ? chosenGradient.replace(/^--/, '') + (chosenAngle !== null ? ` (${effectiveAngle}°)` : '')
498
- : (chosenCategory && chosenFamily && chosenStep !== null
499
- ? getVarName(chosenCategory, chosenFamily, chosenStep).replace(/^--/, '') + (opacity < 100 ? ` (${opacity}%)` : '')
500
- : ''));
550
+ : chosenStatic
551
+ ? `color-${chosenStatic}` + (opacity < 100 ? ` (${opacity}%)` : '')
552
+ : chosenGradient
553
+ ? chosenGradient.replace(/^--/, '') + (chosenAngle !== null ? ` (${effectiveAngle}°)` : '')
554
+ : (chosenCategory && chosenFamily && chosenStep !== null
555
+ ? getVarName(chosenCategory, chosenFamily, chosenStep).replace(/^--/, '') + (opacity < 100 ? ` (${opacity}%)` : '')
556
+ : ''));
501
557
 
502
558
  let availableTabs = $derived(selectedFamily
503
559
  ? allCategories.filter(c => c.id !== 'text' || familiesWithText.includes(selectedFamily!))
@@ -517,6 +573,7 @@
517
573
  {canBeLinked}
518
574
  {disabled}
519
575
  {selectionsLocked}
576
+ {onwrite}
520
577
  dropdownMinWidth="14rem"
521
578
  dropdownMaxWidth="calc(100vw - 2rem)"
522
579
  hideDefaultHeader={!!selectedFamily}
@@ -542,13 +599,23 @@
542
599
  {#snippet children({ close })}
543
600
 
544
601
  {#if selectedFamily === null}
545
- <div class="family-list">
546
- <button class="family-item" class:active={chosenNone} onclick={() => selectNone(close)}>
547
- <div class="family-swatches">
548
- <div class="none-swatch"></div>
549
- </div>
550
- <span class="family-label">None</span>
602
+ <div class="static-band">
603
+ <button class="static-chip" class:active={chosenStatic === 'white'} onclick={() => selectStatic('white', close)}>
604
+ <div class="static-swatch static-swatch--white"></div>
605
+ <span class="static-label">White</span>
551
606
  </button>
607
+ <button class="static-chip" class:active={chosenStatic === 'black'} onclick={() => selectStatic('black', close)}>
608
+ <div class="static-swatch static-swatch--black"></div>
609
+ <span class="static-label">Black</span>
610
+ </button>
611
+ {#if showNone}
612
+ <button class="static-chip" class:active={chosenNone} onclick={() => selectNone(close)}>
613
+ <div class="static-swatch static-swatch--none"></div>
614
+ <span class="static-label">None</span>
615
+ </button>
616
+ {/if}
617
+ </div>
618
+ <div class="family-list">
552
619
  {#each families as fam}
553
620
  {@const outOfFamily = familyFilter !== null && fam.name !== familyFilter}
554
621
  <button
@@ -850,6 +917,79 @@
850
917
  background: var(--ui-hover);
851
918
  }
852
919
 
920
+ /* Inline band of instant-apply atoms (White / Black / None) above the
921
+ family list. Same swatch+label vocabulary as `.step-item`, scaled to
922
+ fit three across so the eye groups them as peer one-clicks distinct
923
+ from the multi-step family rows below. */
924
+ .static-band {
925
+ display: flex;
926
+ gap: var(--ui-space-4);
927
+ padding: var(--ui-space-8);
928
+ border-bottom: 1px solid var(--ui-border-low);
929
+ }
930
+
931
+ .static-chip {
932
+ flex: 1;
933
+ display: flex;
934
+ flex-direction: column;
935
+ align-items: center;
936
+ gap: var(--ui-space-2);
937
+ padding: var(--ui-space-4);
938
+ background: none;
939
+ border: 1px solid transparent;
940
+ border-radius: var(--ui-radius-sm);
941
+ cursor: pointer;
942
+ transition: all var(--ui-transition-fast);
943
+ }
944
+
945
+ .static-chip:hover {
946
+ background: var(--ui-hover);
947
+ border-color: var(--ui-border);
948
+ }
949
+
950
+ .static-chip.active {
951
+ border-color: var(--ui-text-accent);
952
+ border-width: 2px;
953
+ background: var(--ui-hover-high);
954
+ padding: 3px;
955
+ }
956
+
957
+ .static-chip.active .static-label {
958
+ color: var(--ui-text-accent);
959
+ font-weight: var(--ui-font-weight-semibold);
960
+ }
961
+
962
+ .static-swatch {
963
+ width: 2rem;
964
+ height: 1.5rem;
965
+ border-radius: var(--ui-radius-sm);
966
+ border: 1px solid var(--ui-border-low);
967
+ }
968
+
969
+ .static-swatch--white {
970
+ background: var(--color-white);
971
+ }
972
+
973
+ .static-swatch--black {
974
+ background: var(--color-black);
975
+ }
976
+
977
+ .static-swatch--none {
978
+ background: repeating-linear-gradient(
979
+ -45deg,
980
+ transparent,
981
+ transparent 3px,
982
+ var(--ui-border-low) 3px,
983
+ var(--ui-border-low) 4px
984
+ );
985
+ }
986
+
987
+ .static-label {
988
+ font-size: var(--ui-font-size-xs);
989
+ color: var(--ui-text-secondary);
990
+ font-family: var(--ui-font-mono);
991
+ }
992
+
853
993
  .family-list {
854
994
  display: flex;
855
995
  flex-direction: column;
@@ -905,15 +1045,6 @@
905
1045
  border-radius: 2px;
906
1046
  }
907
1047
 
908
- .none-swatch {
909
- width: 2.5rem;
910
- height: 0.75rem;
911
- border-radius: 2px;
912
- border: 1px solid var(--ui-border-low);
913
- position: relative;
914
- overflow: hidden;
915
- }
916
-
917
1048
  .gradient-swatch {
918
1049
  width: 2.5rem;
919
1050
  height: 0.75rem;
@@ -931,22 +1062,6 @@
931
1062
  border-top: 1px solid var(--ui-border-low);
932
1063
  }
933
1064
 
934
- .none-swatch::after {
935
- content: '';
936
- position: absolute;
937
- top: -1px;
938
- left: -1px;
939
- right: -1px;
940
- bottom: -1px;
941
- background: repeating-linear-gradient(
942
- -45deg,
943
- transparent,
944
- transparent 3px,
945
- var(--ui-border-low) 3px,
946
- var(--ui-border-low) 4px
947
- );
948
- }
949
-
950
1065
  .family-label {
951
1066
  flex: 1;
952
1067
  font-size: var(--ui-font-size-sm);