@motion-proto/live-tokens 0.3.9 → 0.5.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 (103) hide show
  1. package/package.json +9 -8
  2. package/src/component-editor/BadgeEditor.svelte +24 -22
  3. package/src/component-editor/CalloutEditor.svelte +3 -3
  4. package/src/component-editor/CardEditor.svelte +25 -21
  5. package/src/component-editor/CollapsibleSectionEditor.svelte +27 -25
  6. package/src/component-editor/CornerBadgeEditor.svelte +37 -35
  7. package/src/component-editor/DialogEditor.svelte +26 -24
  8. package/src/component-editor/ImageEditor.svelte +11 -9
  9. package/src/component-editor/InlineEditActionsEditor.svelte +17 -15
  10. package/src/component-editor/NotificationEditor.svelte +32 -30
  11. package/src/component-editor/ProgressBarEditor.svelte +3 -3
  12. package/src/component-editor/RadioButtonEditor.svelte +31 -29
  13. package/src/component-editor/SectionDividerEditor.svelte +30 -28
  14. package/src/component-editor/SegmentedControlEditor.svelte +29 -25
  15. package/src/component-editor/StandardButtonsEditor.svelte +42 -38
  16. package/src/component-editor/TabBarEditor.svelte +20 -18
  17. package/src/component-editor/TableEditor.svelte +4 -4
  18. package/src/component-editor/TooltipEditor.svelte +11 -9
  19. package/src/component-editor/registry.ts +2 -2
  20. package/src/component-editor/scaffolding/AngleDial.svelte +20 -19
  21. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +44 -20
  22. package/src/component-editor/scaffolding/ComponentFileManager.svelte +260 -37
  23. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +41 -29
  24. package/src/component-editor/scaffolding/ComponentsTab.svelte +7 -3
  25. package/src/component-editor/scaffolding/CopyFromMenu.svelte +21 -12
  26. package/src/component-editor/scaffolding/DemoHeader.svelte +13 -4
  27. package/src/component-editor/scaffolding/DividerEditor.svelte +27 -14
  28. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +10 -4
  29. package/src/component-editor/scaffolding/GradientCard.svelte +25 -20
  30. package/src/component-editor/scaffolding/LinkageChart.svelte +43 -34
  31. package/src/component-editor/scaffolding/LinkedBlock.svelte +24 -21
  32. package/src/component-editor/scaffolding/NonStylableConfig.svelte +6 -1
  33. package/src/component-editor/scaffolding/SaveAsDialog.svelte +39 -35
  34. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +21 -9
  35. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +8 -3
  36. package/src/component-editor/scaffolding/StateBlock.svelte +30 -13
  37. package/src/component-editor/scaffolding/TokenLayout.svelte +46 -30
  38. package/src/component-editor/scaffolding/TypeEditor.svelte +52 -26
  39. package/src/component-editor/scaffolding/VariantGroup.svelte +81 -48
  40. package/src/component-editor/scaffolding/componentSectionType.ts +2 -2
  41. package/src/components/Badge.svelte +45 -26
  42. package/src/components/Button.svelte +44 -21
  43. package/src/components/Callout.svelte +17 -12
  44. package/src/components/Card.svelte +23 -11
  45. package/src/components/CollapsibleSection.svelte +56 -27
  46. package/src/components/CornerBadge.svelte +32 -18
  47. package/src/components/Dialog.svelte +55 -31
  48. package/src/components/Image.svelte +14 -5
  49. package/src/components/InlineEditActions.svelte +22 -10
  50. package/src/components/Notification.svelte +39 -19
  51. package/src/components/ProgressBar.svelte +27 -17
  52. package/src/components/RadioButton.svelte +27 -10
  53. package/src/components/SectionDivider.svelte +34 -26
  54. package/src/components/SegmentedControl.svelte +23 -9
  55. package/src/components/TabBar.svelte +23 -10
  56. package/src/components/Table.svelte +8 -3
  57. package/src/components/Tooltip.svelte +15 -5
  58. package/src/lib/ColumnsOverlay.svelte +3 -3
  59. package/src/lib/LiveEditorOverlay.svelte +57 -36
  60. package/src/pages/ComponentEditorPage.svelte +17 -13
  61. package/src/pages/EditorShell.svelte +24 -20
  62. package/src/styles/form-controls.css +2 -2
  63. package/src/styles/tokens.css +59 -81
  64. package/src/ui/BezierCurveEditor.svelte +59 -43
  65. package/src/ui/ColorEditPanel.svelte +71 -44
  66. package/src/ui/EditorViewSwitcher.svelte +9 -5
  67. package/src/ui/FontStackEditor.svelte +16 -15
  68. package/src/ui/GradientEditor.svelte +42 -33
  69. package/src/ui/GradientStopPicker.svelte +18 -29
  70. package/src/ui/PaletteEditor.svelte +238 -212
  71. package/src/ui/PresetFileManager.svelte +20 -18
  72. package/src/ui/ProjectFontsSection.svelte +30 -30
  73. package/src/ui/SurfacesTab.svelte +3 -3
  74. package/src/ui/TextTab.svelte +2 -2
  75. package/src/ui/ThemeFileManager.svelte +38 -35
  76. package/src/ui/Toggle.svelte +11 -9
  77. package/src/ui/UICopyPopover.svelte +19 -15
  78. package/src/ui/UIDialog.svelte +48 -30
  79. package/src/ui/UIFontFamilySelector.svelte +104 -78
  80. package/src/ui/UIFontSizeSelector.svelte +38 -20
  81. package/src/ui/UIFontWeightSelector.svelte +33 -13
  82. package/src/ui/UILineHeightSelector.svelte +33 -13
  83. package/src/ui/UILinkToggle.svelte +7 -6
  84. package/src/ui/UIOptionItem.svelte +21 -7
  85. package/src/ui/UIOptionList.svelte +9 -3
  86. package/src/ui/UIPaddingSelector.svelte +108 -82
  87. package/src/ui/UIPaletteSelector.svelte +186 -161
  88. package/src/ui/UIRadio.svelte +23 -8
  89. package/src/ui/UIRadioGroup.svelte +9 -8
  90. package/src/ui/UIRelinkConfirmPopover.svelte +26 -16
  91. package/src/ui/UITokenSelector.svelte +112 -68
  92. package/src/ui/UIVariantSelector.svelte +79 -57
  93. package/src/ui/VariablesTab.svelte +15 -15
  94. package/src/ui/palette/GradientStopEditor.svelte +45 -26
  95. package/src/ui/palette/OverridesPanel.svelte +85 -49
  96. package/src/ui/palette/PaletteBase.svelte +60 -32
  97. package/src/ui/palette/ScaleCurveEditor.svelte +25 -10
  98. package/src/ui/sections/ColumnsSection.svelte +13 -13
  99. package/src/ui/sections/GradientsSection.svelte +12 -9
  100. package/src/ui/sections/OverlaysSection.svelte +50 -47
  101. package/src/ui/sections/ShadowsSection.svelte +110 -104
  102. package/src/ui/sections/TokenScaleTable.svelte +38 -22
  103. package/src/ui/sections/tokenScales.ts +2 -2
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { run, stopPropagation, createBubbler } from 'svelte/legacy';
3
+
4
+ const bubble = createBubbler();
2
5
  import { onMount, onDestroy, tick } from 'svelte';
3
6
  import { hexToOklch, oklchToHex, gamutClamp } from '../lib/oklch';
4
7
  import { type CurveAnchor, makeAnchor, sampleCurve, lightnessCurveConfig, saturationCurveConfig, textLightnessCurveConfig } from './curveEngine';
@@ -17,45 +20,24 @@
17
20
  /** Mid-gray fallback used when no base colour or computed gray-500 is available. */
18
21
  const GRAY_FALLBACK = '#808080';
19
22
 
20
- export let label: string;
21
- export let displayLabel: string | null = null;
22
- export let initialColor: string = GRAY_FALLBACK;
23
- export let mode: 'chromatic' | 'gray' = 'chromatic';
24
- export let cssNamespace: string | null = null;
25
- export let emptySelector: boolean = false;
23
+ interface Props {
24
+ label: string;
25
+ displayLabel?: string | null;
26
+ initialColor?: string;
27
+ mode?: 'chromatic' | 'gray';
28
+ cssNamespace?: string | null;
29
+ emptySelector?: boolean;
30
+ }
31
+
32
+ let {
33
+ label,
34
+ displayLabel = null,
35
+ initialColor = GRAY_FALLBACK,
36
+ mode = 'chromatic',
37
+ cssNamespace = null,
38
+ emptySelector = false
39
+ }: Props = $props();
26
40
 
27
- // --- Store-sourced config (single source of truth) ---
28
- //
29
- // All persistent palette state lives in `$editorState.palettes[label]`.
30
- // Local `$:` derivations below pull named fields with defaults; every
31
- // handler writes via `edit()` / `patchPalette()` so the store is the only
32
- // writer. No `let` mirrors, no round-trip sync reactives.
33
- //
34
- // The defaults fall back only when palettes[label] is undefined (brand-new
35
- // install, never seeded). Production seeds via themeInit → seedPalettesFromTheme.
36
- $: paletteConfig = $editorState.palettes[label];
37
- $: baseColor = paletteConfig?.baseColor ?? initialColor;
38
- $: tintHue = paletteConfig?.tintHue ?? 240;
39
- $: tintChroma = paletteConfig?.tintChroma ?? DEFAULT_TINT_CHROMA;
40
- $: lightnessCurve = paletteConfig?.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
41
- $: saturationCurve = paletteConfig?.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
42
- $: grayLightnessCurve = paletteConfig?.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS();
43
- $: graySaturationCurve = paletteConfig?.graySaturationCurve ?? DEFAULT_GRAY_SATURATION();
44
- $: scaleCurves = paletteConfig?.scaleCurves ?? defaultScaleCurvesObject();
45
- $: curveOffset = paletteConfig?.curveOffset ?? { lightness: 0, saturation: 0 };
46
- $: overrides = paletteConfig?.overrides ?? {};
47
- $: snappedScales = new Set(paletteConfig?.snappedScales ?? []);
48
- $: anchorToBase = paletteConfig?.anchorToBase ?? true;
49
- $: emptyMode = paletteConfig?.emptyMode ?? 'solid';
50
- $: emptyStep = paletteConfig?.emptyStep ?? '850';
51
- $: gradientStyle = paletteConfig?.gradientStyle ?? 'linear';
52
- $: gradientAngle = paletteConfig?.gradientAngle ?? 180;
53
- $: gradientReverse = paletteConfig?.gradientReverse ?? false;
54
- $: gradientStops = paletteConfig?.gradientStops ?? [
55
- { position: 0, paletteLabel: '800' },
56
- { position: 100, paletteLabel: '950' },
57
- ];
58
- $: gradientSize = paletteConfig?.gradientSize ?? 'page';
59
41
 
60
42
  function defaultPaletteConfig(): PaletteConfig {
61
43
  return {
@@ -97,8 +79,8 @@
97
79
  }
98
80
 
99
81
  // --- Transient UI state (not persisted; not in PaletteConfig) ---
100
- let lockedLightnessIdx: number | null = null;
101
- let lockedSaturationIdx: number | null = null;
82
+ let lockedLightnessIdx: number | null = $state(null);
83
+ let lockedSaturationIdx: number | null = $state(null);
102
84
 
103
85
  // Handle for the open palette edit scope: a clipping scope (clipUndoFloor:
104
86
  // true) bracketing one panel-open → confirm/cancel cycle. Held at component
@@ -111,9 +93,9 @@
111
93
  return ps ? ps.effective : '#000000';
112
94
  }
113
95
 
114
- let gradientColorStops = '';
115
- let gradientCssValue = '';
116
- let gradientBarPreview = '';
96
+ let gradientColorStops = $state('');
97
+ let gradientCssValue = $state('');
98
+ let gradientBarPreview = $state('');
117
99
 
118
100
  function onEmptyModeChange(e: Event) {
119
101
  edit('emptyMode', (e.currentTarget as HTMLInputElement).checked ? 'gradient' : 'solid');
@@ -142,11 +124,11 @@
142
124
  { label: '950', hue: 229, saturation: 34, lightness: 3 },
143
125
  ];
144
126
 
145
- let grayEditorOpen = false;
146
- let showDerived = false;
127
+ let grayEditorOpen = $state(false);
128
+ let showDerived = $state(false);
147
129
 
148
130
  // --- Palette curve editors (lightness + saturation) ---
149
- let paletteEditorOpen = false;
131
+ let paletteEditorOpen = $state(false);
150
132
 
151
133
  // Default curve anchors (used for initial state and reset)
152
134
  const DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
@@ -179,15 +161,8 @@
179
161
  // (`editingKey`, `editingSnapshot`, `editingDraft`, `snapshotTintHue`,
180
162
  // `snapshotTintChroma`). The compatibility `$:` derivations below preserve
181
163
  // existing read sites while writes go through `editing = { kind: ... }`.
182
- let editing: EditingState = idleState;
164
+ let editing: EditingState = $state(idleState);
183
165
 
184
- // Read-side compat: existing `editingKey === ...` etc. comparisons keep
185
- // working. New code should narrow on `editing.kind` directly.
186
- $: editingKey = editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? BASE_KEY : editing.stepKey;
187
- $: editingDraft = editing.kind === 'editingStep' ? editing.draft : null;
188
- $: editingSnapshot = editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? editing.snapshotHex : editing.snapshot;
189
- $: snapshotTintHue = editing.kind === 'editingBase' ? editing.snapshotTintHue : null;
190
- $: snapshotTintChroma = editing.kind === 'editingBase' ? editing.snapshotTintChroma : null;
191
166
 
192
167
  function computeGrayColor(index: number, hue: number, chroma: number = tintChroma): string {
193
168
  const xPos = grayStepToX(index);
@@ -206,30 +181,8 @@
206
181
  return `gray-${label}`;
207
182
  }
208
183
 
209
- // Reactive map of computed gray colors
210
- $: grayComputed = (() => {
211
- const _gl = grayLightnessCurve, _gs = graySaturationCurve, _co = curveOffset, _tc = tintChroma, _th = tintHue;
212
- return graySteps.map((step, index) => ({
213
- step,
214
- index,
215
- key: grayStepKey(step.label),
216
- hex: computeGrayColor(index, _th, _tc),
217
- }));
218
- })();
219
184
 
220
- $: grayEffective = (() => {
221
- const _ed = editingDraft, _ek = editingKey, _ov = overrides;
222
- return grayComputed.map(g => ({
223
- ...g,
224
- effective: (_ek === g.key && _ed !== null) ? _ed : (g.key in _ov) ? _ov[g.key] : g.hex,
225
- }));
226
- })();
227
185
 
228
- // Gray-500 hex — always the computed (curve-derived) value so derived
229
- // scales (surfaces, borders, text) update in realtime when tint changes.
230
- $: gray500Hex = mode === 'gray'
231
- ? (grayComputed.find(g => g.step.label === '500')?.hex ?? GRAY_FALLBACK)
232
- : baseColor;
233
186
 
234
187
 
235
188
  // --- Chromatic palette steps ---
@@ -313,34 +266,7 @@
313
266
  }
314
267
  }
315
268
 
316
- // Derive locked anchor indices from curve shape — no writes to state.
317
- $: {
318
- if (anchorToBase) {
319
- const x500 = stepIndexToX(4);
320
- const lIdx = lightnessCurve.findIndex(a => Math.abs(a.x - x500) < 0.5);
321
- lockedLightnessIdx = lIdx >= 0 ? lIdx : null;
322
- const sIdx = saturationCurve.findIndex(a => Math.abs(a.x - x500) < 0.5);
323
- lockedSaturationIdx = sIdx >= 0 ? sIdx : null;
324
- } else {
325
- lockedLightnessIdx = null;
326
- lockedSaturationIdx = null;
327
- }
328
- }
329
269
 
330
- /**
331
- * Keep the locked lightness anchor y in sync with baseColor. Idempotent —
332
- * only writes when the curve's anchor y differs from the baseColor-derived
333
- * target. During a baseColor drag (inside a slider transaction) this
334
- * additional curve edit merges into the same history entry. On undo/redo
335
- * the curve already has the correct y (they're saved together), so this
336
- * is a no-op.
337
- */
338
- $: if (anchorToBase && lockedLightnessIdx !== null && baseColor) {
339
- const targetY = hexToOklch(baseColor).l * 100;
340
- if (lightnessCurve[lockedLightnessIdx] && Math.abs(lightnessCurve[lockedLightnessIdx].y - targetY) > 0.01) {
341
- edit('lightnessCurve', lightnessCurve.map((a, i) => i === lockedLightnessIdx ? { ...a, y: targetY } : a));
342
- }
343
- }
344
270
 
345
271
  function computePaletteColor(index: number, base: string): string {
346
272
  const { c: baseC, h } = hexToOklch(base);
@@ -354,39 +280,7 @@
354
280
  return oklchToHex(clamped.l, clamped.c, clamped.h);
355
281
  }
356
282
 
357
- $: paletteComputed = (() => {
358
- const _bc = baseColor, _lc = lightnessCurve, _sc = saturationCurve, _co = curveOffset, _ed = editingDraft, _ek = editingKey, _ov = overrides, _ab = anchorToBase;
359
- return paletteStepLightness.map((ps, index) => {
360
- const k = paletteStepKey(ps.label);
361
- const hex = computePaletteColor(index, baseColor);
362
- const effective = (_ek === k && _ed !== null) ? _ed : (k in _ov) ? _ov[k] : hex;
363
- return {
364
- label: ps.label,
365
- lightness: ps.lightness,
366
- index,
367
- key: k,
368
- hex,
369
- effective,
370
- };
371
- });
372
- })();
373
283
 
374
- // Gradient reactives — must follow paletteComputed
375
- $: {
376
- const pc = paletteComputed;
377
- const sorted = [...gradientStops].sort((a, b) => gradientReverse ? b.position - a.position : a.position - b.position);
378
- gradientColorStops = sorted.map(s => `${stopColor(s, pc)} ${s.position}%`).join(', ');
379
- gradientBarPreview = `linear-gradient(to right, ${gradientColorStops})`;
380
- if (emptySelector && emptyMode === 'gradient') {
381
- switch (gradientStyle) {
382
- case 'radial': gradientCssValue = `radial-gradient(circle, ${gradientColorStops})`; break;
383
- case 'conic': gradientCssValue = `conic-gradient(from ${gradientAngle}deg, ${gradientColorStops})`; break;
384
- default: gradientCssValue = `linear-gradient(${gradientAngle}deg, ${gradientColorStops})`;
385
- }
386
- } else {
387
- gradientCssValue = '';
388
- }
389
- }
390
284
 
391
285
  function startBaseEdit() {
392
286
  if (editing.kind === 'editingBase') { confirmEdit(); return; }
@@ -460,13 +354,6 @@
460
354
  }
461
355
  ];
462
356
 
463
- // Scales to render in gray mode (varies by namespace)
464
- $: grayScales = mode === 'gray' ? scales.filter(scale => {
465
- if (scale.title === 'Surfaces') return true;
466
- if (scale.title === 'Borders') return true;
467
- if (scale.title === 'Text') return true;
468
- return false;
469
- }) : [];
470
357
 
471
358
  // --- Per-scale curve state (Surfaces & Borders) ---
472
359
 
@@ -485,7 +372,7 @@
485
372
  },
486
373
  };
487
374
 
488
- let scaleEditorOpen: Record<string, boolean> = { Surfaces: false, Borders: false, Text: false };
375
+ let scaleEditorOpen: Record<string, boolean> = $state({ Surfaces: false, Borders: false, Text: false });
489
376
 
490
377
  function toggleScaleEditor(title: string) {
491
378
  scaleEditorOpen[title] = !scaleEditorOpen[title];
@@ -516,58 +403,16 @@
516
403
  return `${scaleTitle}-${stepName}`;
517
404
  }
518
405
 
519
- $: curveVersion = JSON.stringify(scaleCurves) + JSON.stringify(curveOffset) + gray500Hex;
520
406
 
521
407
  function derivedHex(step: Step, base: string, scaleTitle: string, _version?: string): string {
522
408
  return computeDerivedColor(step, base, configForScale(scaleTitle), scaleTitle);
523
409
  }
524
410
 
525
- /**
526
- * Closure factories used by `<OverridesPanel>` so the panel doesn't have to
527
- * know about `baseColor` / `gray500Hex` / `curveVersion`. These keep
528
- * reactivity intact (the parent's `$:` blocks still drive re-render).
529
- *
530
- * Note: `effectiveColor` itself always uses `gray500Hex` for non-override
531
- * derivation (pre-existing); the chromatic vs gray distinction here is
532
- * only for the `derivedHex` (the "Ag" preview / border-color base).
533
- */
534
- $: derivedHexForBase = (step: Step, scaleTitle: string) => derivedHex(step, baseColor, scaleTitle, curveVersion);
535
- $: derivedHexForGray = (step: Step, scaleTitle: string) => derivedHex(step, gray500Hex, scaleTitle, curveVersion);
536
- $: effectiveHexAny = (k: string, step: Step, scaleTitle: string) => effectiveColor(k, step, scaleTitle, curveVersion);
537
411
 
538
- // --- Reactive editing state ---
539
412
 
540
- $: isEditingBase = isBaseEdit(editing);
541
413
 
542
- $: editingColor = isEditingBase
543
- ? (mode === 'gray' ? gray500Hex : baseColor)
544
- : editingDraft;
545
414
 
546
- $: editingStepInfo = (() => {
547
- if (!editingKey || isEditingBase) return null;
548
- if (mode === 'gray') {
549
- const gs = graySteps.find(s => grayStepKey(s.label) === editingKey);
550
- if (gs) return { scale: 'Gray', step: gs.label };
551
- }
552
- const ps = paletteStepLightness.find(p => paletteStepKey(p.label) === editingKey);
553
- if (ps) return { scale: 'Palette', step: ps.label };
554
- for (const scale of scales) {
555
- for (const step of scale.steps) {
556
- if (stepKey(scale.title, step.name) === editingKey) {
557
- return { scale: scale.title, step: step.name };
558
- }
559
- }
560
- }
561
- return null;
562
- })();
563
415
 
564
- $: panelOpen = editingKey !== null && (isEditingBase || (editingDraft !== null && editingStepInfo !== null));
565
-
566
- $: editPanelTitle = isEditingBase
567
- ? 'Base Color'
568
- : editingStepInfo
569
- ? `${editingStepInfo.scale} \u203A ${editingStepInfo.step}`
570
- : null;
571
416
 
572
417
  // --- Compute derived color via OKLCH ---
573
418
 
@@ -740,7 +585,7 @@
740
585
  return `linear-gradient(to right, ${points.join(', ')})`;
741
586
  }
742
587
 
743
- let copiedKey: string | null = null;
588
+ let copiedKey: string | null = $state(null);
744
589
  function copyHex(k: string, hex: string, event?: MouseEvent) {
745
590
  navigator.clipboard.writeText(hex);
746
591
  copiedKey = k;
@@ -748,7 +593,7 @@
748
593
  setTimeout(() => { copiedKey = null; }, 1500);
749
594
  }
750
595
 
751
- let copiedLabelKey: string | null = null;
596
+ let copiedLabelKey: string | null = $state(null);
752
597
  function copyVarName(k: string, varName: string, event?: MouseEvent) {
753
598
  navigator.clipboard.writeText(varName);
754
599
  copiedLabelKey = k;
@@ -840,7 +685,7 @@
840
685
  }
841
686
  }
842
687
 
843
- let snapPickerKey: string | null = null;
688
+ let snapPickerKey: string | null = $state(null);
844
689
 
845
690
  function handleDocClick(e: MouseEvent) {
846
691
  if (!snapPickerKey) return;
@@ -881,7 +726,6 @@
881
726
  if (changed) edit('overrides', next);
882
727
  }
883
728
 
884
- $: baseColor, scaleCurves, lightnessCurve, saturationCurve, curveOffset, snappedScales, resnapScales();
885
729
 
886
730
  // CSS-var emission lives in `paletteDerivation` → `editorRenderer`; the store
887
731
  // is the single source of truth for palette config and the renderer
@@ -899,6 +743,188 @@
899
743
  }
900
744
 
901
745
 
746
+ // --- Store-sourced config (single source of truth) ---
747
+ //
748
+ // All persistent palette state lives in `$editorState.palettes[label]`.
749
+ // Local `$:` derivations below pull named fields with defaults; every
750
+ // handler writes via `edit()` / `patchPalette()` so the store is the only
751
+ // writer. No `let` mirrors, no round-trip sync reactives.
752
+ //
753
+ // The defaults fall back only when palettes[label] is undefined (brand-new
754
+ // install, never seeded). Production seeds via themeInit → seedPalettesFromTheme.
755
+ let paletteConfig = $derived($editorState.palettes[label]);
756
+ let baseColor = $derived(paletteConfig?.baseColor ?? initialColor);
757
+ let tintHue = $derived(paletteConfig?.tintHue ?? 240);
758
+ let tintChroma = $derived(paletteConfig?.tintChroma ?? DEFAULT_TINT_CHROMA);
759
+ let lightnessCurve = $derived(paletteConfig?.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS());
760
+ let saturationCurve = $derived(paletteConfig?.saturationCurve ?? DEFAULT_PALETTE_SATURATION());
761
+ let grayLightnessCurve = $derived(paletteConfig?.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS());
762
+ let graySaturationCurve = $derived(paletteConfig?.graySaturationCurve ?? DEFAULT_GRAY_SATURATION());
763
+ let scaleCurves = $derived(paletteConfig?.scaleCurves ?? defaultScaleCurvesObject());
764
+ let curveOffset = $derived(paletteConfig?.curveOffset ?? { lightness: 0, saturation: 0 });
765
+ let overrides = $derived(paletteConfig?.overrides ?? {});
766
+ let snappedScales = $derived(new Set(paletteConfig?.snappedScales ?? []));
767
+ let anchorToBase = $derived(paletteConfig?.anchorToBase ?? true);
768
+ let emptyMode = $derived(paletteConfig?.emptyMode ?? 'solid');
769
+ let emptyStep = $derived(paletteConfig?.emptyStep ?? '850');
770
+ let gradientStyle = $derived(paletteConfig?.gradientStyle ?? 'linear');
771
+ let gradientAngle = $derived(paletteConfig?.gradientAngle ?? 180);
772
+ let gradientReverse = $derived(paletteConfig?.gradientReverse ?? false);
773
+ let gradientStops = $derived(paletteConfig?.gradientStops ?? [
774
+ { position: 0, paletteLabel: '800' },
775
+ { position: 100, paletteLabel: '950' },
776
+ ]);
777
+ let gradientSize = $derived(paletteConfig?.gradientSize ?? 'page');
778
+ // Read-side compat: existing `editingKey === ...` etc. comparisons keep
779
+ // working. New code should narrow on `editing.kind` directly.
780
+ let editingKey = $derived(editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? BASE_KEY : editing.stepKey);
781
+ let editingDraft = $derived(editing.kind === 'editingStep' ? editing.draft : null);
782
+ let editingSnapshot = $derived(editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? editing.snapshotHex : editing.snapshot);
783
+ let snapshotTintHue = $derived(editing.kind === 'editingBase' ? editing.snapshotTintHue : null);
784
+ let snapshotTintChroma = $derived(editing.kind === 'editingBase' ? editing.snapshotTintChroma : null);
785
+ // Reactive map of computed gray colors
786
+ let grayComputed = $derived((() => {
787
+ const _gl = grayLightnessCurve, _gs = graySaturationCurve, _co = curveOffset, _tc = tintChroma, _th = tintHue;
788
+ return graySteps.map((step, index) => ({
789
+ step,
790
+ index,
791
+ key: grayStepKey(step.label),
792
+ hex: computeGrayColor(index, _th, _tc),
793
+ }));
794
+ })());
795
+ let grayEffective = $derived((() => {
796
+ const _ed = editingDraft, _ek = editingKey, _ov = overrides;
797
+ return grayComputed.map(g => ({
798
+ ...g,
799
+ effective: (_ek === g.key && _ed !== null) ? _ed : (g.key in _ov) ? _ov[g.key] : g.hex,
800
+ }));
801
+ })());
802
+ // Gray-500 hex — always the computed (curve-derived) value so derived
803
+ // scales (surfaces, borders, text) update in realtime when tint changes.
804
+ let gray500Hex = $derived(mode === 'gray'
805
+ ? (grayComputed.find(g => g.step.label === '500')?.hex ?? GRAY_FALLBACK)
806
+ : baseColor);
807
+ // Derive locked anchor indices from curve shape — no writes to state.
808
+ run(() => {
809
+ if (anchorToBase) {
810
+ const x500 = stepIndexToX(4);
811
+ const lIdx = lightnessCurve.findIndex(a => Math.abs(a.x - x500) < 0.5);
812
+ lockedLightnessIdx = lIdx >= 0 ? lIdx : null;
813
+ const sIdx = saturationCurve.findIndex(a => Math.abs(a.x - x500) < 0.5);
814
+ lockedSaturationIdx = sIdx >= 0 ? sIdx : null;
815
+ } else {
816
+ lockedLightnessIdx = null;
817
+ lockedSaturationIdx = null;
818
+ }
819
+ });
820
+ /**
821
+ * Keep the locked lightness anchor y in sync with baseColor. Idempotent —
822
+ * only writes when the curve's anchor y differs from the baseColor-derived
823
+ * target. During a baseColor drag (inside a slider transaction) this
824
+ * additional curve edit merges into the same history entry. On undo/redo
825
+ * the curve already has the correct y (they're saved together), so this
826
+ * is a no-op.
827
+ */
828
+ run(() => {
829
+ if (anchorToBase && lockedLightnessIdx !== null && baseColor) {
830
+ const targetY = hexToOklch(baseColor).l * 100;
831
+ if (lightnessCurve[lockedLightnessIdx] && Math.abs(lightnessCurve[lockedLightnessIdx].y - targetY) > 0.01) {
832
+ edit('lightnessCurve', lightnessCurve.map((a, i) => i === lockedLightnessIdx ? { ...a, y: targetY } : a));
833
+ }
834
+ }
835
+ });
836
+ let paletteComputed = $derived((() => {
837
+ const _bc = baseColor, _lc = lightnessCurve, _sc = saturationCurve, _co = curveOffset, _ed = editingDraft, _ek = editingKey, _ov = overrides, _ab = anchorToBase;
838
+ return paletteStepLightness.map((ps, index) => {
839
+ const k = paletteStepKey(ps.label);
840
+ const hex = computePaletteColor(index, baseColor);
841
+ const effective = (_ek === k && _ed !== null) ? _ed : (k in _ov) ? _ov[k] : hex;
842
+ return {
843
+ label: ps.label,
844
+ lightness: ps.lightness,
845
+ index,
846
+ key: k,
847
+ hex,
848
+ effective,
849
+ };
850
+ });
851
+ })());
852
+ // Gradient reactives — must follow paletteComputed
853
+ run(() => {
854
+ const pc = paletteComputed;
855
+ const sorted = [...gradientStops].sort((a, b) => gradientReverse ? b.position - a.position : a.position - b.position);
856
+ gradientColorStops = sorted.map(s => `${stopColor(s, pc)} ${s.position}%`).join(', ');
857
+ gradientBarPreview = `linear-gradient(to right, ${gradientColorStops})`;
858
+ if (emptySelector && emptyMode === 'gradient') {
859
+ switch (gradientStyle) {
860
+ case 'radial': gradientCssValue = `radial-gradient(circle, ${gradientColorStops})`; break;
861
+ case 'conic': gradientCssValue = `conic-gradient(from ${gradientAngle}deg, ${gradientColorStops})`; break;
862
+ default: gradientCssValue = `linear-gradient(${gradientAngle}deg, ${gradientColorStops})`;
863
+ }
864
+ } else {
865
+ gradientCssValue = '';
866
+ }
867
+ });
868
+ // Scales to render in gray mode (varies by namespace)
869
+ let grayScales = $derived(mode === 'gray' ? scales.filter(scale => {
870
+ if (scale.title === 'Surfaces') return true;
871
+ if (scale.title === 'Borders') return true;
872
+ if (scale.title === 'Text') return true;
873
+ return false;
874
+ }) : []);
875
+ let curveVersion = $derived(JSON.stringify(scaleCurves) + JSON.stringify(curveOffset) + gray500Hex);
876
+ /**
877
+ * Closure factories used by `<OverridesPanel>` so the panel doesn't have to
878
+ * know about `baseColor` / `gray500Hex` / `curveVersion`. These keep
879
+ * reactivity intact (the parent's `$:` blocks still drive re-render).
880
+ *
881
+ * Note: `effectiveColor` itself always uses `gray500Hex` for non-override
882
+ * derivation (pre-existing); the chromatic vs gray distinction here is
883
+ * only for the `derivedHex` (the "Ag" preview / border-color base).
884
+ */
885
+ let derivedHexForBase = $derived((step: Step, scaleTitle: string) => derivedHex(step, baseColor, scaleTitle, curveVersion));
886
+ let derivedHexForGray = $derived((step: Step, scaleTitle: string) => derivedHex(step, gray500Hex, scaleTitle, curveVersion));
887
+ let effectiveHexAny = $derived((k: string, step: Step, scaleTitle: string) => effectiveColor(k, step, scaleTitle, curveVersion));
888
+ // --- Reactive editing state ---
889
+
890
+ let isEditingBase = $derived(isBaseEdit(editing));
891
+ let editingColor = $derived(isEditingBase
892
+ ? (mode === 'gray' ? gray500Hex : baseColor)
893
+ : editingDraft);
894
+ let editingStepInfo = $derived((() => {
895
+ if (!editingKey || isEditingBase) return null;
896
+ if (mode === 'gray') {
897
+ const gs = graySteps.find(s => grayStepKey(s.label) === editingKey);
898
+ if (gs) return { scale: 'Gray', step: gs.label };
899
+ }
900
+ const ps = paletteStepLightness.find(p => paletteStepKey(p.label) === editingKey);
901
+ if (ps) return { scale: 'Palette', step: ps.label };
902
+ for (const scale of scales) {
903
+ for (const step of scale.steps) {
904
+ if (stepKey(scale.title, step.name) === editingKey) {
905
+ return { scale: scale.title, step: step.name };
906
+ }
907
+ }
908
+ }
909
+ return null;
910
+ })());
911
+ let panelOpen = $derived(editingKey !== null && (isEditingBase || (editingDraft !== null && editingStepInfo !== null)));
912
+ let editPanelTitle = $derived(isEditingBase
913
+ ? 'Base Color'
914
+ : editingStepInfo
915
+ ? `${editingStepInfo.scale} \u203A ${editingStepInfo.step}`
916
+ : null);
917
+ $effect(() => {
918
+ // Re-snap whenever any of the inputs change. Touch each so the effect
919
+ // tracks them explicitly (resnapScales() reads them indirectly).
920
+ void baseColor;
921
+ void scaleCurves;
922
+ void lightnessCurve;
923
+ void saturationCurve;
924
+ void curveOffset;
925
+ void snappedScales;
926
+ resnapScales();
927
+ });
902
928
  </script>
903
929
 
904
930
  <div class="palette-editor" style="--editor-base: {mode === 'gray' ? gray500Hex : baseColor}">
@@ -936,40 +962,40 @@
936
962
  <input
937
963
  type="checkbox"
938
964
  checked={emptyMode === 'gradient'}
939
- on:change={onEmptyModeChange}
965
+ onchange={onEmptyModeChange}
940
966
  />
941
967
  <span>Gradient</span>
942
968
  </label>
943
969
  {/if}
944
- <button class="edit-toggle" type="button" on:click={clearPaletteOverrides}>Clear Overrides</button>
970
+ <button class="edit-toggle" type="button" onclick={clearPaletteOverrides}>Clear Overrides</button>
945
971
  <button
946
972
  class="edit-toggle"
947
973
  type="button"
948
- on:click={() => paletteEditorOpen = !paletteEditorOpen}
974
+ onclick={() => paletteEditorOpen = !paletteEditorOpen}
949
975
  >{paletteEditorOpen ? 'Close' : 'Edit'}</button>
950
976
  </div>
951
977
  <div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}">
952
978
  <div class="step-column">
953
- <button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-white'} type="button" on:click={(e) => copyVarName('palette-white', `--color-${cssNamespace}-white`, e)}>
979
+ <button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-white'} type="button" onclick={(e) => copyVarName('palette-white', `--color-${cssNamespace}-white`, e)}>
954
980
  {copiedLabelKey === 'palette-white' ? 'copied!' : 'white'}
955
981
  </button>
956
982
  <div class="swatch gray-swatch bookend" style="background: #ffffff"></div>
957
983
  </div>
958
984
  {#each paletteComputed as ps}
959
985
  <div class="step-column">
960
- <button class="step-label copyable-label" class:copied={copiedLabelKey === ps.key} type="button" on:click={(e) => copyVarName(ps.key, `--color-${cssNamespace}-${ps.label}`, e)}>
986
+ <button class="step-label copyable-label" class:copied={copiedLabelKey === ps.key} type="button" onclick={(e) => copyVarName(ps.key, `--color-${cssNamespace}-${ps.label}`, e)}>
961
987
  {copiedLabelKey === ps.key ? 'copied!' : ps.label}
962
988
  </button>
963
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
989
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
964
990
  <div
965
991
  class="swatch gray-swatch"
966
992
  class:active={editingKey === ps.key}
967
993
  class:overridden={ps.key in overrides}
968
994
  style="background: {ps.effective}"
969
- on:click={() => handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
995
+ onclick={() => handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
970
996
  role="button"
971
997
  tabindex="0"
972
- on:keydown={(e) => e.key === 'Enter' && handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
998
+ onkeydown={(e) => e.key === 'Enter' && handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
973
999
  >
974
1000
  {#if ps.key in overrides}
975
1001
  <span class="override-dot" title="Palette override"></span>
@@ -979,8 +1005,8 @@
979
1005
  type="checkbox"
980
1006
  class="empty-check"
981
1007
  checked={emptyStep === ps.label}
982
- on:click|stopPropagation={() => edit('emptyStep', ps.label)}
983
- on:keydown|stopPropagation
1008
+ onclick={stopPropagation(() => edit('emptyStep', ps.label))}
1009
+ onkeydown={stopPropagation(bubble('keydown'))}
984
1010
  title="Page background"
985
1011
  />
986
1012
  {/if}
@@ -989,12 +1015,12 @@
989
1015
  class="step-hex"
990
1016
  class:copied={copiedKey === ps.key}
991
1017
  type="button"
992
- on:click={(e) => copyHex(ps.key, ps.effective, e)}
1018
+ onclick={(e) => copyHex(ps.key, ps.effective, e)}
993
1019
  >{copiedKey === ps.key ? 'copied!' : ps.effective}</button>
994
1020
  </div>
995
1021
  {/each}
996
1022
  <div class="step-column">
997
- <button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-black'} type="button" on:click={(e) => copyVarName('palette-black', `--color-${cssNamespace}-black`, e)}>
1023
+ <button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-black'} type="button" onclick={(e) => copyVarName('palette-black', `--color-${cssNamespace}-black`, e)}>
998
1024
  {copiedLabelKey === 'palette-black' ? 'copied!' : 'black'}
999
1025
  </button>
1000
1026
  <div class="swatch gray-swatch bookend" style="background: #000000"></div>
@@ -1047,7 +1073,7 @@
1047
1073
 
1048
1074
  </div>
1049
1075
 
1050
- <button class="derived-toggle" type="button" on:click={() => showDerived = !showDerived}>
1076
+ <button class="derived-toggle" type="button" onclick={() => showDerived = !showDerived}>
1051
1077
  <i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
1052
1078
  <span>Text, Surfaces &amp; Borders</span>
1053
1079
  </button>
@@ -1133,35 +1159,35 @@
1133
1159
  <div class="scale-section">
1134
1160
  <div class="scale-header">
1135
1161
  <h4 class="scale-title">{displayLabel ?? label}</h4>
1136
- <button class="edit-toggle" type="button" on:click={clearPaletteOverrides}>Clear Overrides</button>
1162
+ <button class="edit-toggle" type="button" onclick={clearPaletteOverrides}>Clear Overrides</button>
1137
1163
  <button
1138
1164
  class="edit-toggle"
1139
1165
  type="button"
1140
- on:click={() => grayEditorOpen = !grayEditorOpen}
1166
+ onclick={() => grayEditorOpen = !grayEditorOpen}
1141
1167
  >{grayEditorOpen ? 'Close' : 'Edit'}</button>
1142
1168
  </div>
1143
1169
  <div class="swatch-grid" style="--swatch-cols: {graySteps.length + 2}">
1144
1170
  <div class="step-column">
1145
- <button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-white'} type="button" on:click={(e) => copyVarName('gray-white', `--color-${cssNamespace}-white`, e)}>
1171
+ <button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-white'} type="button" onclick={(e) => copyVarName('gray-white', `--color-${cssNamespace}-white`, e)}>
1146
1172
  {copiedLabelKey === 'gray-white' ? 'copied!' : 'white'}
1147
1173
  </button>
1148
1174
  <div class="swatch gray-swatch bookend" style="background: #ffffff"></div>
1149
1175
  </div>
1150
1176
  {#each grayEffective as g}
1151
1177
  <div class="step-column">
1152
- <button class="step-label copyable-label" class:copied={copiedLabelKey === g.key} type="button" on:click={(e) => copyVarName(g.key, `--color-${cssNamespace}-${g.step.label}`, e)}>
1178
+ <button class="step-label copyable-label" class:copied={copiedLabelKey === g.key} type="button" onclick={(e) => copyVarName(g.key, `--color-${cssNamespace}-${g.step.label}`, e)}>
1153
1179
  {copiedLabelKey === g.key ? 'copied!' : g.step.label}
1154
1180
  </button>
1155
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1181
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
1156
1182
  <div
1157
1183
  class="swatch gray-swatch"
1158
1184
  class:active={editingKey === g.key}
1159
1185
  class:overridden={g.key in overrides}
1160
1186
  style="background: {g.effective}"
1161
- on:click={() => handleGrayClick(g.step, g.index)}
1187
+ onclick={() => handleGrayClick(g.step, g.index)}
1162
1188
  role="button"
1163
1189
  tabindex="0"
1164
- on:keydown={(e) => e.key === 'Enter' && handleGrayClick(g.step, g.index)}
1190
+ onkeydown={(e) => e.key === 'Enter' && handleGrayClick(g.step, g.index)}
1165
1191
  >
1166
1192
  {#if g.key in overrides}
1167
1193
  <span class="override-dot" title="Palette override"></span>
@@ -1171,12 +1197,12 @@
1171
1197
  class="step-hex"
1172
1198
  class:copied={copiedKey === g.key}
1173
1199
  type="button"
1174
- on:click={(e) => copyHex(g.key, g.effective, e)}
1200
+ onclick={(e) => copyHex(g.key, g.effective, e)}
1175
1201
  >{copiedKey === g.key ? 'copied!' : g.effective}</button>
1176
1202
  </div>
1177
1203
  {/each}
1178
1204
  <div class="step-column">
1179
- <button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-black'} type="button" on:click={(e) => copyVarName('gray-black', `--color-${cssNamespace}-black`, e)}>
1205
+ <button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-black'} type="button" onclick={(e) => copyVarName('gray-black', `--color-${cssNamespace}-black`, e)}>
1180
1206
  {copiedLabelKey === 'gray-black' ? 'copied!' : 'black'}
1181
1207
  </button>
1182
1208
  <div class="swatch gray-swatch bookend" style="background: #000000"></div>
@@ -1209,7 +1235,7 @@
1209
1235
  </div>
1210
1236
  </div>
1211
1237
 
1212
- <button class="derived-toggle" type="button" on:click={() => showDerived = !showDerived}>
1238
+ <button class="derived-toggle" type="button" onclick={() => showDerived = !showDerived}>
1213
1239
  <i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
1214
1240
  <span>Text, Surfaces &amp; Borders</span>
1215
1241
  </button>