@motion-proto/live-tokens 0.6.2 → 0.7.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.
Files changed (212) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +147 -136
  3. package/dist-plugin/index.d.cts +1 -1
  4. package/dist-plugin/index.d.ts +1 -1
  5. package/dist-plugin/index.js +145 -135
  6. package/package.json +25 -40
  7. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  8. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  9. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  10. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +3 -3
  11. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  12. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  13. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  14. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  15. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  16. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +64 -37
  17. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  18. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  19. package/src/{component-editor → editor/component-editor}/SectionDividerEditor.svelte +57 -84
  20. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  21. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +16 -20
  22. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  23. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  24. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  25. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  26. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +2 -2
  27. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  28. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +144 -416
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  31. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  32. package/src/{component-editor → editor/component-editor}/scaffolding/DividerEditor.svelte +1 -1
  33. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  34. package/src/{component-editor → editor/component-editor}/scaffolding/GradientCard.svelte +6 -6
  35. package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
  36. package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -11
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  39. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +72 -0
  40. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  41. package/src/editor/component-editor/scaffolding/StateBlock.svelte +257 -0
  42. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +9 -7
  43. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +644 -0
  44. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  45. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  46. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +14 -0
  47. package/src/{lib → editor/core/components}/componentConfigService.ts +2 -2
  48. package/src/{lib → editor/core/components}/componentPersist.ts +5 -5
  49. package/src/editor/core/flashStatus.ts +30 -0
  50. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  51. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  52. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  53. package/src/editor/core/manifests/manifestService.ts +116 -0
  54. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +2 -2
  55. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +5 -5
  56. package/src/editor/core/productionPulse.ts +37 -0
  57. package/src/{lib → editor/core/routing}/router.ts +1 -1
  58. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  59. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  60. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  61. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  62. package/src/{lib → editor/core/store}/editorStore.ts +17 -17
  63. package/src/{lib → editor/core/store}/editorTypes.ts +1 -1
  64. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  65. package/src/{lib → editor/core/themes}/slices/components.ts +2 -2
  66. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  67. package/src/{lib → editor/core/themes}/slices/gradients.ts +2 -2
  68. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  69. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  70. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  71. package/src/{lib → editor/core/themes}/themeInit.ts +6 -6
  72. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  73. package/src/{lib → editor/core/themes}/themeTypes.ts +11 -7
  74. package/src/editor/index.ts +69 -0
  75. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +79 -125
  76. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  77. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  78. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  79. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  80. package/src/{styles → editor/styles}/ui-editor.css +41 -21
  81. package/src/{styles → editor/styles}/ui-form-controls.css +8 -8
  82. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +8 -8
  83. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  84. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +8 -6
  85. package/src/editor/ui/FileLoadList.svelte +350 -0
  86. package/src/editor/ui/FilePill.svelte +80 -0
  87. package/src/{ui → editor/ui}/FontStackEditor.svelte +7 -7
  88. package/src/{ui → editor/ui}/GradientEditor.svelte +11 -11
  89. package/src/{ui → editor/ui}/GradientStopPicker.svelte +1 -1
  90. package/src/editor/ui/ManifestFileManager.svelte +371 -0
  91. package/src/{ui → editor/ui}/PaletteEditor.svelte +132 -598
  92. package/src/{ui → editor/ui}/ProjectFontsSection.svelte +102 -144
  93. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  94. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  95. package/src/{ui → editor/ui}/ThemeFileManager.svelte +286 -519
  96. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  97. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -6
  98. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +1 -1
  99. package/src/editor/ui/UIInfoPopover.svelte +244 -0
  100. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  101. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  102. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  103. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +26 -26
  104. package/src/editor/ui/UIPillButton.svelte +138 -0
  105. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  106. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  107. package/src/editor/ui/UISquareButton.svelte +172 -0
  108. package/src/{ui → editor/ui}/UITokenSelector.svelte +10 -10
  109. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  110. package/src/{ui → editor/ui}/VariablesTab.svelte +31 -8
  111. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  112. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +13 -13
  113. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +8 -5
  114. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  115. package/src/editor/ui/palette/paletteMath.ts +275 -0
  116. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -17
  117. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +7 -7
  118. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +17 -17
  119. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +22 -22
  120. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  121. package/src/{components → system/components}/Badge.svelte +0 -36
  122. package/src/{components → system/components}/Card.svelte +8 -62
  123. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  124. package/src/{components → system/components}/Dialog.svelte +1 -1
  125. package/src/system/components/FloatingTokenTags.css +256 -0
  126. package/src/system/components/FloatingTokenTags.svelte +592 -0
  127. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  128. package/src/system/components/MenuSelect.svelte +229 -0
  129. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  130. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  131. package/src/{components → system/components}/TabBar.svelte +81 -65
  132. package/src/{components → system/components}/Table.svelte +17 -3
  133. package/src/{components → system/components}/Tooltip.svelte +6 -4
  134. package/src/system/styles/CONVENTIONS.md +178 -0
  135. package/src/{styles → system/styles}/fonts.css +6 -3
  136. package/src/{styles → system/styles}/tokens.css +149 -29
  137. package/src/component-editor/ImageEditor.svelte +0 -74
  138. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  139. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  140. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  141. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  142. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  143. package/src/data/google-fonts.json +0 -75
  144. package/src/lib/index.ts +0 -68
  145. package/src/lib/presetService.ts +0 -214
  146. package/src/lib/productionPulse.ts +0 -32
  147. package/src/ui/PresetFileManager.svelte +0 -1116
  148. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  149. /package/src/{styles → app}/site.css +0 -0
  150. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  151. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  152. /package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +0 -0
  153. /package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +0 -0
  154. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  155. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  156. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  157. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  158. /package/src/{lib → editor/core/components}/componentConfigKeys.ts +0 -0
  159. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  160. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  161. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  162. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  163. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  164. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  165. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  166. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  167. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  168. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  169. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  170. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  171. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  172. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  173. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  174. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  175. /package/src/{lib → editor/core/themes}/migrations/index.ts +0 -0
  176. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  177. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  178. /package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -0
  179. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  180. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  181. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  182. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  183. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  184. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  185. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  186. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  187. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  188. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  189. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  190. /package/src/{ui → editor/ui}/index.ts +0 -0
  191. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  192. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  193. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  194. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  195. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  196. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  197. /package/src/{assets → system/assets}/offering.webp +0 -0
  198. /package/src/{components → system/components}/Button.svelte +0 -0
  199. /package/src/{components → system/components}/Callout.svelte +0 -0
  200. /package/src/{components → system/components}/CollapsibleSection.svelte +0 -0
  201. /package/src/{components → system/components}/Image.svelte +0 -0
  202. /package/src/{components → system/components}/Notification.svelte +0 -0
  203. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  204. /package/src/{components → system/components}/SectionDivider.svelte +0 -0
  205. /package/src/{components → system/components}/types.ts +0 -0
  206. /package/src/{styles → system/styles}/_padding.scss +0 -0
  207. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  208. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  209. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  210. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  211. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  212. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -3,22 +3,31 @@
3
3
 
4
4
  const bubble = createBubbler();
5
5
  import { onMount, onDestroy, tick } from 'svelte';
6
- import { hexToOklch, oklchToHex, gamutClamp } from '../lib/oklch';
7
- import { type CurveAnchor, makeAnchor, sampleCurve, lightnessCurveConfig, saturationCurveConfig, textLightnessCurveConfig } from './curveEngine';
6
+ import { hexToOklch } from '../core/palettes/oklch';
7
+ import { type CurveAnchor, lightnessCurveConfig, saturationCurveConfig } from './curveEngine';
8
8
  import ColorEditPanel from './ColorEditPanel.svelte';
9
9
  import OverridesPanel from './palette/OverridesPanel.svelte';
10
10
  import GradientStopEditor from './palette/GradientStopEditor.svelte';
11
11
  import ScaleCurveEditor from './palette/ScaleCurveEditor.svelte';
12
12
  import PaletteBase from './palette/PaletteBase.svelte';
13
13
  import { type EditingState, idleState, BASE_KEY, isEditingBase as isBaseEdit } from './palette/paletteEditorState';
14
- import type { PaletteConfig, GradientStyle, GradientStop } from '../lib/themeTypes';
15
- import { editorState, mutate, setPaletteConfig, beginSliderGesture, beginScope, commitScope, cancelScope, type Scope } from '../lib/editorStore';
16
- import { scaleToCssVar } from '../lib/paletteDerivation';
17
- import { showCopyPopover } from '../lib/copyPopover';
18
- import { get } from 'svelte/store';
19
-
20
- /** Mid-gray fallback used when no base colour or computed gray-500 is available. */
21
- const GRAY_FALLBACK = '#808080';
14
+ import {
15
+ type Step, type Scale, type GrayStep, type CurveOffset, type ScaleCurves,
16
+ GRAY_FALLBACK, DEFAULT_TINT_CHROMA,
17
+ DEFAULT_PALETTE_LIGHTNESS, DEFAULT_PALETTE_SATURATION, DEFAULT_GRAY_LIGHTNESS, DEFAULT_GRAY_SATURATION,
18
+ defaultScaleCurves, defaultScaleCurvesObject,
19
+ paletteStepLightness, graySteps, scales,
20
+ paletteStepKey, grayStepKey, stepKey, scaleCurveKey as getScaleCurveKey,
21
+ stepIndexToX,
22
+ injectLockedAnchor, removeLockedAnchor,
23
+ computeGrayColor as computeGrayColorPure,
24
+ computePaletteColor as computePaletteColorPure,
25
+ computeDerivedColor as computeDerivedColorPure,
26
+ snapScaleToPalette as snapScaleToPalettePure,
27
+ } from './palette/paletteMath';
28
+ import type { PaletteConfig, GradientStop } from '../core/themes/themeTypes';
29
+ import { editorState, mutate, setPaletteConfig, beginSliderGesture, beginScope, commitScope, cancelScope, type Scope } from '../core/store/editorStore';
30
+ import { showCopyPopover } from './copyPopover';
22
31
 
23
32
  interface Props {
24
33
  label: string;
@@ -56,14 +65,6 @@
56
65
  };
57
66
  }
58
67
 
59
- function defaultScaleCurvesObject() {
60
- return {
61
- Surfaces: { lightness: defaultScaleCurves.Surfaces.lightness(), saturation: defaultScaleCurves.Surfaces.saturation() },
62
- Borders: { lightness: defaultScaleCurves.Borders.lightness(), saturation: defaultScaleCurves.Borders.saturation() },
63
- Text: { lightness: defaultScaleCurves.Text.lightness(), saturation: defaultScaleCurves.Text.saturation() },
64
- };
65
- }
66
-
67
68
  function edit<K extends keyof PaletteConfig>(field: K, value: PaletteConfig[K]): void {
68
69
  mutate(`${label}: ${String(field)}`, (s) => {
69
70
  if (!s.palettes[label]) s.palettes[label] = defaultPaletteConfig();
@@ -78,16 +79,17 @@
78
79
  });
79
80
  }
80
81
 
81
- // --- Transient UI state (not persisted; not in PaletteConfig) ---
82
82
  let lockedLightnessIdx: number | null = $state(null);
83
83
  let lockedSaturationIdx: number | null = $state(null);
84
84
 
85
- // Handle for the open palette edit scope: a clipping scope (clipUndoFloor:
86
- // true) bracketing one panel-open confirm/cancel cycle. Held at component
87
- // scope so the inline header-swatch handlers and the function handlers
88
- // share the same handle for commitScope/cancelScope.
85
+ // Held at component scope so inline header-swatch handlers and the function
86
+ // handlers share one handle for commit/cancel.
89
87
  let paletteEditScope: Scope | null = null;
90
88
 
89
+ function openSession() {
90
+ paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
91
+ }
92
+
91
93
  function stopColor(stop: GradientStop, pc: typeof paletteComputed): string {
92
94
  const ps = pc?.find(p => p.label === stop.paletteLabel);
93
95
  return ps ? ps.effective : '#000000';
@@ -101,133 +103,34 @@
101
103
  edit('emptyMode', (e.currentTarget as HTMLInputElement).checked ? 'gradient' : 'solid');
102
104
  }
103
105
 
104
- // --- Gray mode ---
105
-
106
- interface GrayStep {
107
- label: string;
108
- hue: number;
109
- saturation: number;
110
- lightness: number;
111
- }
112
-
113
- const graySteps: GrayStep[] = [
114
- { label: '100', hue: 240, saturation: 5, lightness: 92 },
115
- { label: '200', hue: 220, saturation: 13, lightness: 84 },
116
- { label: '300', hue: 216, saturation: 12, lightness: 72 },
117
- { label: '400', hue: 240, saturation: 5, lightness: 61 },
118
- { label: '500', hue: 240, saturation: 5, lightness: 50 },
119
- { label: '600', hue: 240, saturation: 5, lightness: 42 },
120
- { label: '700', hue: 240, saturation: 5, lightness: 34 },
121
- { label: '800', hue: 240, saturation: 10, lightness: 25 },
122
- { label: '850', hue: 229, saturation: 20, lightness: 18 },
123
- { label: '900', hue: 240, saturation: 30, lightness: 10 },
124
- { label: '950', hue: 229, saturation: 34, lightness: 3 },
125
- ];
126
-
127
106
  let grayEditorOpen = $state(false);
128
107
  let showDerived = $state(false);
129
-
130
- // --- Palette curve editors (lightness + saturation) ---
131
108
  let paletteEditorOpen = $state(false);
132
109
 
133
- // Default curve anchors (used for initial state and reset)
134
- const DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
135
- const DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
136
- const DEFAULT_GRAY_LIGHTNESS = () => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
137
- const DEFAULT_GRAY_SATURATION = () => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
138
-
139
110
  function setLightnessCurve(a: CurveAnchor[]) { edit('lightnessCurve', a); }
140
111
  function setSaturationCurve(a: CurveAnchor[]) { edit('saturationCurve', a); }
141
112
  function setGrayLightnessCurve(a: CurveAnchor[]) { edit('grayLightnessCurve', a); }
142
113
  function setGraySaturationCurve(a: CurveAnchor[]) { edit('graySaturationCurve', a); }
143
114
 
144
- // --- Curve offset + clipboard (shared across all curve editors) ---
145
-
146
115
  function handleOffset(key: string, value: number) {
147
116
  edit('curveOffset', { ...curveOffset, [key]: value });
148
117
  }
149
118
 
150
- // Gray step index to curve x-position
151
- function grayStepToX(index: number): number {
152
- return graySteps.length > 1 ? (index / (graySteps.length - 1)) * 100 : 50;
153
- }
154
-
155
- // Base chroma for gray tinting (editable via the color panel's chroma slider)
156
- const DEFAULT_TINT_CHROMA = 0.04;
157
-
158
- // --- Editing-state machine (M4 fold) ---
159
- //
160
- // Single discriminated union replaces five independent `let` decls
161
- // (`editingKey`, `editingSnapshot`, `editingDraft`, `snapshotTintHue`,
162
- // `snapshotTintChroma`). The compatibility `$:` derivations below preserve
163
- // existing read sites while writes go through `editing = { kind: ... }`.
164
119
  let editing: EditingState = $state(idleState);
165
120
 
121
+ let injectedLightness = false;
122
+ let injectedSaturation = false;
166
123
 
167
124
  function computeGrayColor(index: number, hue: number, chroma: number = tintChroma): string {
168
- const xPos = grayStepToX(index);
169
- const lOff = curveOffset['gray-lightness'] ?? 0;
170
- const sOff = curveOffset['gray-saturation'] ?? 0;
171
-
172
- const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
173
- const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
174
- const targetC = chroma * satMul;
175
-
176
- const clamped = gamutClamp(targetL, targetC, hue);
177
- return oklchToHex(clamped.l, clamped.c, clamped.h);
178
- }
179
-
180
- function grayStepKey(label: string): string {
181
- return `gray-${label}`;
182
- }
183
-
184
-
185
-
186
-
187
-
188
- // --- Chromatic palette steps ---
189
-
190
- const paletteStepLightness = [
191
- { label: '100', lightness: 95 },
192
- { label: '200', lightness: 88 },
193
- { label: '300', lightness: 78 },
194
- { label: '400', lightness: 68 },
195
- { label: '500', lightness: 57 },
196
- { label: '600', lightness: 49 },
197
- { label: '700', lightness: 41 },
198
- { label: '800', lightness: 32 },
199
- { label: '850', lightness: 25 },
200
- { label: '900', lightness: 17 },
201
- { label: '950', lightness: 8 },
202
- ];
203
-
204
- function paletteStepKey(label: string): string {
205
- return `Palette-${label}`;
206
- }
207
-
208
- function stepIndexToX(index: number): number {
209
- return (index / (paletteStepLightness.length - 1)) * 100;
125
+ return computeGrayColorPure(index, hue, chroma, grayLightnessCurve, graySaturationCurve, curveOffset);
210
126
  }
211
127
 
212
- // --- Locked anchor management ---
213
-
214
- let injectedLightness = false;
215
- let injectedSaturation = false;
216
-
217
- function injectLockedAnchor(curve: CurveAnchor[], x: number, y: number): { curve: CurveAnchor[], idx: number, injected: boolean } {
218
- const existing = curve.findIndex(a => Math.abs(a.x - x) < 0.5);
219
- if (existing >= 0) {
220
- if (curve[existing].x === x && Math.abs(curve[existing].y - y) < 0.01) return { curve, idx: existing, injected: false };
221
- return { curve: curve.map((a, i) => i === existing ? { ...a, x, y } : a), idx: existing, injected: false };
222
- }
223
- let insertAt = curve.findIndex(a => a.x > x);
224
- if (insertAt < 0) insertAt = curve.length;
225
- return { curve: [...curve.slice(0, insertAt), makeAnchor(x, y, 15), ...curve.slice(insertAt)], idx: insertAt, injected: true };
128
+ function computePaletteColor(index: number, base: string): string {
129
+ return computePaletteColorPure(index, base, lightnessCurve, saturationCurve, curveOffset);
226
130
  }
227
131
 
228
- function removeLockedAnchor(curve: CurveAnchor[], idx: number | null): CurveAnchor[] {
229
- if (idx === null || idx === 0 || idx === curve.length - 1) return curve;
230
- return curve.filter((_, i) => i !== idx);
132
+ function computeDerivedColor(step: Step, base: string, scaleTitle: string): string {
133
+ return computeDerivedColorPure(step, base, scaleTitle, scaleCurves, curveOffset);
231
134
  }
232
135
 
233
136
  /**
@@ -268,26 +171,12 @@
268
171
 
269
172
 
270
173
 
271
- function computePaletteColor(index: number, base: string): string {
272
- const { c: baseC, h } = hexToOklch(base);
273
- const xPos = stepIndexToX(index);
274
-
275
- const targetL = Math.max(0, Math.min(100, sampleCurve(lightnessCurve, xPos) + (curveOffset['lightness'] ?? 0))) / 100;
276
- const satMul = Math.max(0, Math.min(2, (sampleCurve(saturationCurve, xPos) + (curveOffset['saturation'] ?? 0)) / 100));
277
- const targetC = baseC * satMul;
278
-
279
- const clamped = gamutClamp(targetL, targetC, h);
280
- return oklchToHex(clamped.l, clamped.c, clamped.h);
281
- }
282
-
283
-
284
-
285
174
  function startBaseEdit() {
286
175
  if (editing.kind === 'editingBase') { confirmEdit(); return; }
287
176
  editing = mode === 'gray'
288
177
  ? { kind: 'editingBase', snapshotHex: gray500Hex, snapshotTintHue: tintHue, snapshotTintChroma: tintChroma }
289
178
  : { kind: 'editingBase', snapshotHex: baseColor, snapshotTintHue: null, snapshotTintChroma: null };
290
- paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
179
+ openSession();
291
180
  }
292
181
 
293
182
  function handlePaletteClick(ps: { label: string; lightness: number; index: number }) {
@@ -298,80 +187,9 @@
298
187
  }
299
188
  const current = (k in overrides) ? overrides[k] : computePaletteColor(ps.index, baseColor);
300
189
  editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
301
- paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
190
+ openSession();
302
191
  }
303
192
 
304
- // --- Scale types ---
305
-
306
- interface Step {
307
- name: string;
308
- position: number;
309
- lightness?: number;
310
- saturation?: number;
311
- }
312
-
313
- interface Scale {
314
- title: string;
315
- isText: boolean;
316
- steps: Step[];
317
- }
318
-
319
- const scales: Scale[] = [
320
- {
321
- title: 'Surfaces',
322
- isText: false,
323
- steps: [
324
- { name: 'lowest', position: -1 },
325
- { name: 'lower', position: -2/3 },
326
- { name: 'low', position: -1/3 },
327
- { name: 'default', position: 0 },
328
- { name: 'high', position: 1/3 },
329
- { name: 'higher', position: 2/3 },
330
- { name: 'highest', position: 1 },
331
- ]
332
- },
333
- {
334
- title: 'Borders',
335
- isText: false,
336
- steps: [
337
- { name: 'faint', position: -1 },
338
- { name: 'subtle', position: -0.5 },
339
- { name: 'default', position: 0 },
340
- { name: 'medium', position: 0.5 },
341
- { name: 'strong', position: 1 },
342
- ]
343
- },
344
- {
345
- title: 'Text',
346
- isText: true,
347
- steps: [
348
- { name: 'primary', position: 0 },
349
- { name: 'secondary', position: 0 },
350
- { name: 'tertiary', position: 0 },
351
- { name: 'muted', position: 0 },
352
- { name: 'disabled', position: 0 },
353
- ]
354
- }
355
- ];
356
-
357
-
358
- // --- Per-scale curve state (Surfaces & Borders) ---
359
-
360
- const defaultScaleCurves: Record<string, { lightness: () => CurveAnchor[]; saturation: () => CurveAnchor[] }> = {
361
- Surfaces: {
362
- lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
363
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)],
364
- },
365
- Borders: {
366
- lightness: () => [makeAnchor(0, 25, 5), makeAnchor(100, 80, 5)],
367
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)],
368
- },
369
- Text: {
370
- lightness: () => [makeAnchor(0, 120, 30), makeAnchor(100, 55, 30)],
371
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 15, 30)],
372
- },
373
- };
374
-
375
193
  let scaleEditorOpen: Record<string, boolean> = $state({ Surfaces: false, Borders: false, Text: false });
376
194
 
377
195
  function toggleScaleEditor(title: string) {
@@ -384,77 +202,9 @@
384
202
  edit('scaleCurves', { ...scaleCurves, [title]: { ...cur, [channel]: a } });
385
203
  }
386
204
 
387
- function getScaleCurveKey(scaleTitle: string, channel: 'lightness' | 'saturation'): string {
388
- return `${scaleTitle}-${channel}`;
389
- }
390
-
391
- interface ScaleConfig {
392
- lightnessLow: number;
393
- lightnessHigh: number;
394
- saturation: number;
395
- }
396
-
397
- function configForScale(title: string): ScaleConfig {
398
- return { lightnessLow: 0, lightnessHigh: 100, saturation: 100 };
399
- }
400
-
401
-
402
- function stepKey(scaleTitle: string, stepName: string): string {
403
- return `${scaleTitle}-${stepName}`;
404
- }
405
-
406
-
407
- function derivedHex(step: Step, base: string, scaleTitle: string, _version?: string): string {
408
- return computeDerivedColor(step, base, configForScale(scaleTitle), scaleTitle);
409
- }
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
- // --- Compute derived color via OKLCH ---
418
-
419
- function scaleStepToX(step: Step, scale: Scale): number {
420
- const idx = scale.steps.indexOf(step);
421
- return scale.steps.length > 1 ? (idx / (scale.steps.length - 1)) * 100 : 50;
422
- }
423
-
424
- function computeDerivedColor(step: Step, base: string, config: ScaleConfig, scaleTitle: string): string {
425
- const { l: baseL, c: baseC, h: baseH } = hexToOklch(base);
426
- const scale = scales.find(s => s.title === scaleTitle)!;
427
- const xPos = scaleStepToX(step, scale);
428
-
429
- const lCurve = scaleCurves[scaleTitle]?.lightness ?? [];
430
- const sCurve = scaleCurves[scaleTitle]?.saturation ?? [];
431
- const lKey = getScaleCurveKey(scaleTitle, 'lightness');
432
- const sKey = getScaleCurveKey(scaleTitle, 'saturation');
433
- const lOff = curveOffset[lKey] ?? 0;
434
- const sOff = curveOffset[sKey] ?? 0;
435
-
436
- let targetL: number;
437
- if (scale.isText) {
438
- // Text: lightness curve is a multiplier (100 = 1x base lightness)
439
- const lMul = Math.max(0, Math.min(2, (sampleCurve(lCurve, xPos) + lOff) / 100));
440
- targetL = Math.max(0, Math.min(1, baseL * lMul));
441
- } else {
442
- targetL = Math.max(0, Math.min(100, sampleCurve(lCurve, xPos) + lOff)) / 100;
443
- }
444
-
445
- const satMul = Math.max(0, Math.min(2, (sampleCurve(sCurve, xPos) + sOff) / 100));
446
- const targetC = baseC * satMul;
447
-
448
- const clamped = gamutClamp(targetL, targetC, baseH);
449
- return oklchToHex(clamped.l, clamped.c, clamped.h);
450
- }
451
-
452
- // --- Interaction handlers ---
453
-
454
205
  function handleColorChange(hex: string) {
455
206
  if (isEditingBase) {
456
- // Gray mode's base is derived from tintHue/tintChroma (onHueChromaChange
457
- // writes those). The raw hex path only applies to chromatic palettes.
207
+ // Gray mode's base is derived from tintHue/tintChroma; raw hex only applies to chromatic.
458
208
  if (mode === 'chromatic') edit('baseColor', hex);
459
209
  return;
460
210
  }
@@ -477,9 +227,9 @@
477
227
  confirmEdit();
478
228
  return;
479
229
  }
480
- const current = (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, configForScale(scaleTitle), scaleTitle);
230
+ const current = (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, scaleTitle);
481
231
  editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
482
- paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
232
+ openSession();
483
233
  }
484
234
 
485
235
  function handleGrayClick(gStep: GrayStep, index: number) {
@@ -490,13 +240,13 @@
490
240
  }
491
241
  const current = (k in overrides) ? overrides[k] : computeGrayColor(index, tintHue, tintChroma);
492
242
  editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
493
- paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
243
+ openSession();
494
244
  }
495
245
 
496
246
  async function confirmEdit() {
497
247
  if (editingKey) {
498
- // Accumulate all override changes into one patch so the session commit
499
- // sees a single final state (no intermediate reactive round-trips).
248
+ // Accumulate override changes into one patch so the session commit sees
249
+ // a single final state (no intermediate reactive round-trips).
500
250
  let nextOverrides = { ...overrides };
501
251
  if (editingDraft !== null) {
502
252
  const computed = computedValueForKey(editingKey);
@@ -525,8 +275,7 @@
525
275
 
526
276
  function cancelEdit() {
527
277
  editing = idleState;
528
- // Restoring the session snapshot in the store fires the sync reactive,
529
- // which pulls baseColor/tintHue/tintChroma/overrides/… back to pre-open.
278
+ // Restoring the session snapshot pulls baseColor/tintHue/overrides/… back to pre-open.
530
279
  if (paletteEditScope) { cancelScope(paletteEditScope); paletteEditScope = null; }
531
280
  }
532
281
 
@@ -547,7 +296,7 @@
547
296
  for (const scale of scales) {
548
297
  for (const step of scale.steps) {
549
298
  if (stepKey(scale.title, step.name) === key) {
550
- return computeDerivedColor(step, gray500Hex, configForScale(scale.title), scale.title);
299
+ return computeDerivedColor(step, gray500Hex, scale.title);
551
300
  }
552
301
  }
553
302
  }
@@ -556,33 +305,7 @@
556
305
 
557
306
  function effectiveColor(k: string, step: Step, scaleTitle: string, _version?: string): string {
558
307
  if (editingKey === k && editingDraft !== null) return editingDraft;
559
- return (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, configForScale(scaleTitle), scaleTitle);
560
- }
561
-
562
- // --- Brightness/Saturation gradient helpers for scale editor ---
563
-
564
- function lightnessGrad(base: string): string {
565
- const { c, h } = hexToOklch(base);
566
- const points: string[] = [];
567
- for (let i = 0; i <= 8; i++) {
568
- const l = i / 8;
569
- const clamped = gamutClamp(l, c, h);
570
- points.push(`${oklchToHex(clamped.l, clamped.c, clamped.h)} ${Math.round((i / 8) * 100)}%`);
571
- }
572
- return `linear-gradient(to right, ${points.join(', ')})`;
573
- }
574
-
575
- function saturationGrad(base: string): string {
576
- const { l, c, h } = hexToOklch(base);
577
- const midL = Math.max(0.3, Math.min(0.7, l));
578
- const points: string[] = [];
579
- for (let i = 0; i <= 8; i++) {
580
- const scale = (i / 8) * 2;
581
- const targetC = c * scale;
582
- const clamped = gamutClamp(midL, targetC, h);
583
- points.push(`${oklchToHex(clamped.l, clamped.c, clamped.h)} ${Math.round((i / 8) * 100)}%`);
584
- }
585
- return `linear-gradient(to right, ${points.join(', ')})`;
308
+ return (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, scaleTitle);
586
309
  }
587
310
 
588
311
  let copiedKey: string | null = $state(null);
@@ -601,42 +324,8 @@
601
324
  setTimeout(() => { copiedLabelKey = null; }, 1500);
602
325
  }
603
326
 
604
- // --- Snap-all: constrain an entire scale to unique palette steps ---
605
-
606
327
  function snapScaleToPalette(scale: Scale): Record<string, string> {
607
- const cfg = configForScale(scale.title);
608
- const n = scale.steps.length;
609
-
610
- const stepL = scale.steps.map(step => {
611
- const derived = computeDerivedColor(step, baseColor, cfg, scale.title);
612
- return hexToOklch(derived).l;
613
- });
614
-
615
- const palL = paletteComputed.map(ps => hexToOklch(ps.hex).l);
616
-
617
- const palDarkFirst = [...paletteComputed].reverse();
618
- const palLDarkFirst = [...palL].reverse();
619
-
620
- let bestStart = 0;
621
- let bestCost = Infinity;
622
- for (let start = 0; start <= palDarkFirst.length - n; start++) {
623
- let cost = 0;
624
- for (let i = 0; i < n; i++) {
625
- const d = stepL[i] - palLDarkFirst[start + i];
626
- cost += d * d;
627
- }
628
- if (cost < bestCost) {
629
- bestCost = cost;
630
- bestStart = start;
631
- }
632
- }
633
-
634
- const assigned: Record<string, string> = {};
635
- for (let i = 0; i < n; i++) {
636
- const k = stepKey(scale.title, scale.steps[i].name);
637
- assigned[k] = palDarkFirst[bestStart + i].hex;
638
- }
639
- return assigned;
328
+ return snapScaleToPalettePure(scale, baseColor, scaleCurves, curveOffset, paletteComputed);
640
329
  }
641
330
 
642
331
  function toggleSnapAll(scale: Scale) {
@@ -668,10 +357,6 @@
668
357
  edit('overrides', next);
669
358
  }
670
359
 
671
- function scaleHasOverrides(scale: Scale): boolean {
672
- return scale.steps.some(s => stepKey(scale.title, s.name) in overrides);
673
- }
674
-
675
360
  function clearScaleOverrides(scale: Scale) {
676
361
  snapPickerKey = null;
677
362
  const nextOverrides = { ...overrides };
@@ -726,63 +411,38 @@
726
411
  if (changed) edit('overrides', next);
727
412
  }
728
413
 
729
-
730
- // CSS-var emission lives in `paletteDerivation` → `editorRenderer`; the store
731
- // is the single source of truth for palette config and the renderer
732
- // subscription writes the derived `--color-*` / `--surface-*` / `--border-*`
733
- // / `--text-*` / `--page-bg` variables to :root.
734
-
735
- // --- Load external config ---
736
- //
737
- // External file loads come through editorStore.loadFromFile, which
738
- // overwrites $editorState.palettes — no component-side mirroring needed.
739
- // This export is kept only for callers that want to push a config in
740
- // directly.
741
414
  export function loadConfig(config: PaletteConfig) {
742
415
  setPaletteConfig(label, config);
743
416
  }
744
417
 
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 ?? [
418
+ // Each field reads `$editorState.palettes[label]` directly. A chained
419
+ // `$derived` of the whole config would return the same object reference on
420
+ // every mutate-in-place store update (Svelte 5 uses `===` equality) and
421
+ // short-circuit the downstream chain swatches would freeze.
422
+ let baseColor = $derived($editorState.palettes[label]?.baseColor ?? initialColor);
423
+ let tintHue = $derived($editorState.palettes[label]?.tintHue ?? 240);
424
+ let tintChroma = $derived($editorState.palettes[label]?.tintChroma ?? DEFAULT_TINT_CHROMA);
425
+ let lightnessCurve = $derived($editorState.palettes[label]?.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS());
426
+ let saturationCurve = $derived($editorState.palettes[label]?.saturationCurve ?? DEFAULT_PALETTE_SATURATION());
427
+ let grayLightnessCurve = $derived($editorState.palettes[label]?.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS());
428
+ let graySaturationCurve = $derived($editorState.palettes[label]?.graySaturationCurve ?? DEFAULT_GRAY_SATURATION());
429
+ let scaleCurves = $derived($editorState.palettes[label]?.scaleCurves ?? defaultScaleCurvesObject());
430
+ let curveOffset = $derived($editorState.palettes[label]?.curveOffset ?? { lightness: 0, saturation: 0 });
431
+ let overrides = $derived($editorState.palettes[label]?.overrides ?? {});
432
+ let snappedScales = $derived(new Set($editorState.palettes[label]?.snappedScales ?? []));
433
+ let anchorToBase = $derived($editorState.palettes[label]?.anchorToBase ?? true);
434
+ let emptyMode = $derived($editorState.palettes[label]?.emptyMode ?? 'solid');
435
+ let emptyStep = $derived($editorState.palettes[label]?.emptyStep ?? '850');
436
+ let gradientStyle = $derived($editorState.palettes[label]?.gradientStyle ?? 'linear');
437
+ let gradientAngle = $derived($editorState.palettes[label]?.gradientAngle ?? 180);
438
+ let gradientReverse = $derived($editorState.palettes[label]?.gradientReverse ?? false);
439
+ let gradientStops = $derived($editorState.palettes[label]?.gradientStops ?? [
774
440
  { position: 0, paletteLabel: '800' },
775
441
  { position: 100, paletteLabel: '950' },
776
442
  ]);
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.
443
+ let gradientSize = $derived($editorState.palettes[label]?.gradientSize ?? 'page');
780
444
  let editingKey = $derived(editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? BASE_KEY : editing.stepKey);
781
445
  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
446
  let grayComputed = $derived((() => {
787
447
  const _gl = grayLightnessCurve, _gs = graySaturationCurve, _co = curveOffset, _tc = tintChroma, _th = tintHue;
788
448
  return graySteps.map((step, index) => ({
@@ -799,12 +459,11 @@
799
459
  effective: (_ek === g.key && _ed !== null) ? _ed : (g.key in _ov) ? _ov[g.key] : g.hex,
800
460
  }));
801
461
  })());
802
- // Gray-500 hex — always the computed (curve-derived) value so derived
803
- // scales (surfaces, borders, text) update in realtime when tint changes.
462
+ // Always use the computed (curve-derived) value so derived scales update in
463
+ // realtime when tint changes.
804
464
  let gray500Hex = $derived(mode === 'gray'
805
465
  ? (grayComputed.find(g => g.step.label === '500')?.hex ?? GRAY_FALLBACK)
806
466
  : baseColor);
807
- // Derive locked anchor indices from curve shape — no writes to state.
808
467
  run(() => {
809
468
  if (anchorToBase) {
810
469
  const x500 = stepIndexToX(4);
@@ -817,14 +476,9 @@
817
476
  lockedSaturationIdx = null;
818
477
  }
819
478
  });
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
- */
479
+ // Keep the locked lightness anchor y in sync with baseColor. Idempotent.
480
+ // During a baseColor drag the curve edit merges into the same history entry;
481
+ // on undo/redo the curve already has the correct y, so this is a no-op.
828
482
  run(() => {
829
483
  if (anchorToBase && lockedLightnessIdx !== null && baseColor) {
830
484
  const targetY = hexToOklch(baseColor).l * 100;
@@ -865,27 +519,13 @@
865
519
  gradientCssValue = '';
866
520
  }
867
521
  });
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
- }) : []);
522
+ let grayScales = $derived(mode === 'gray' ? scales : []);
875
523
  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));
524
+ // Chromatic vs gray distinction is only for the `derivedHex` ("Ag" preview /
525
+ // border-color base); `effectiveColor` always uses `gray500Hex` for non-override derivation.
526
+ let derivedHexForBase = $derived((step: Step, scaleTitle: string) => computeDerivedColor(step, baseColor, scaleTitle));
527
+ let derivedHexForGray = $derived((step: Step, scaleTitle: string) => computeDerivedColor(step, gray500Hex, scaleTitle));
887
528
  let effectiveHexAny = $derived((k: string, step: Step, scaleTitle: string) => effectiveColor(k, step, scaleTitle, curveVersion));
888
- // --- Reactive editing state ---
889
529
 
890
530
  let isEditingBase = $derived(isBaseEdit(editing));
891
531
  let editingColor = $derived(isEditingBase
@@ -915,8 +555,7 @@
915
555
  ? `${editingStepInfo.scale} \u203A ${editingStepInfo.step}`
916
556
  : null);
917
557
  $effect(() => {
918
- // Re-snap whenever any of the inputs change. Touch each so the effect
919
- // tracks them explicitly (resnapScales() reads them indirectly).
558
+ // Touch each input so the effect tracks them (resnapScales reads indirectly).
920
559
  void baseColor;
921
560
  void scaleCurves;
922
561
  void lightnessCurve;
@@ -1073,86 +712,6 @@
1073
712
 
1074
713
  </div>
1075
714
 
1076
- <button class="derived-toggle" type="button" onclick={() => showDerived = !showDerived}>
1077
- <i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
1078
- <span>Text, Surfaces &amp; Borders</span>
1079
- </button>
1080
-
1081
- {#if showDerived}
1082
- <div class="scales-row">
1083
- {#each scales.filter(s => s.isText) as scale}
1084
- <OverridesPanel
1085
- {scale}
1086
- editorOpen={scaleEditorOpen[scale.title] ?? false}
1087
- snapped={snappedScales.has(scale.title)}
1088
- supportsSnap={true}
1089
- {cssNamespace}
1090
- {scaleCurves}
1091
- {curveOffset}
1092
- {defaultScaleCurves}
1093
- {overrides}
1094
- {editingKey}
1095
- {snapPickerKey}
1096
- {copiedKey}
1097
- {copiedLabelKey}
1098
- {paletteComputed}
1099
- derivedHexFor={derivedHexForBase}
1100
- effectiveHexFor={effectiveHexAny}
1101
- stepKeyFor={stepKey}
1102
- scaleCurveKeyFor={getScaleCurveKey}
1103
- onToggleSnap={toggleSnapAll}
1104
- onClearScaleOverrides={clearScaleOverrides}
1105
- onToggleEditor={toggleScaleEditor}
1106
- onResetOverride={resetOverride}
1107
- onOverrideClick={handleOverrideClick}
1108
- onSnappedClick={handleSnappedClick}
1109
- onSelectSnapValue={selectSnapValue}
1110
- onCopyHex={copyHex}
1111
- onCopyVarName={copyVarName}
1112
- onSetScaleCurve={setScaleCurve}
1113
- onOffsetChange={handleOffset}
1114
- />
1115
- {/each}
1116
- </div>
1117
-
1118
- <!-- Surfaces & Borders — per-scale editors -->
1119
- <div class="scales-row">
1120
- {#each scales.filter(s => !s.isText) as scale}
1121
- <OverridesPanel
1122
- {scale}
1123
- editorOpen={scaleEditorOpen[scale.title] ?? false}
1124
- snapped={snappedScales.has(scale.title)}
1125
- supportsSnap={true}
1126
- {cssNamespace}
1127
- {scaleCurves}
1128
- {curveOffset}
1129
- {defaultScaleCurves}
1130
- {overrides}
1131
- {editingKey}
1132
- {snapPickerKey}
1133
- {copiedKey}
1134
- {copiedLabelKey}
1135
- {paletteComputed}
1136
- derivedHexFor={derivedHexForBase}
1137
- effectiveHexFor={effectiveHexAny}
1138
- stepKeyFor={stepKey}
1139
- scaleCurveKeyFor={getScaleCurveKey}
1140
- onToggleSnap={toggleSnapAll}
1141
- onClearScaleOverrides={clearScaleOverrides}
1142
- onToggleEditor={toggleScaleEditor}
1143
- onResetOverride={resetOverride}
1144
- onOverrideClick={handleOverrideClick}
1145
- onSnappedClick={handleSnappedClick}
1146
- onSelectSnapValue={selectSnapValue}
1147
- onCopyHex={copyHex}
1148
- onCopyVarName={copyVarName}
1149
- onSetScaleCurve={setScaleCurve}
1150
- onOffsetChange={handleOffset}
1151
- />
1152
- {/each}
1153
- </div>
1154
- {/if}
1155
-
1156
715
  {:else}
1157
716
  <!-- Gray mode: palette + text row -->
1158
717
  <div class="scales-row">
@@ -1234,6 +793,41 @@
1234
793
  </div>
1235
794
  </div>
1236
795
  </div>
796
+ {/if}
797
+
798
+ {#snippet overridesPanel(scale: Scale, canSnap: boolean, derivedHexFor: (step: Step, scaleTitle: string) => string)}
799
+ <OverridesPanel
800
+ {scale}
801
+ editorOpen={scaleEditorOpen[scale.title] ?? false}
802
+ snapped={canSnap && snappedScales.has(scale.title)}
803
+ supportsSnap={canSnap}
804
+ {cssNamespace}
805
+ {scaleCurves}
806
+ {curveOffset}
807
+ {defaultScaleCurves}
808
+ {overrides}
809
+ {editingKey}
810
+ {snapPickerKey}
811
+ {copiedKey}
812
+ {copiedLabelKey}
813
+ {paletteComputed}
814
+ {derivedHexFor}
815
+ effectiveHexFor={effectiveHexAny}
816
+ stepKeyFor={stepKey}
817
+ scaleCurveKeyFor={getScaleCurveKey}
818
+ onToggleSnap={toggleSnapAll}
819
+ onClearScaleOverrides={clearScaleOverrides}
820
+ onToggleEditor={toggleScaleEditor}
821
+ onResetOverride={resetOverride}
822
+ onOverrideClick={handleOverrideClick}
823
+ onSnappedClick={handleSnappedClick}
824
+ onSelectSnapValue={selectSnapValue}
825
+ onCopyHex={copyHex}
826
+ onCopyVarName={copyVarName}
827
+ onSetScaleCurve={setScaleCurve}
828
+ onOffsetChange={handleOffset}
829
+ />
830
+ {/snippet}
1237
831
 
1238
832
  <button class="derived-toggle" type="button" onclick={() => showDerived = !showDerived}>
1239
833
  <i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
@@ -1241,78 +835,18 @@
1241
835
  </button>
1242
836
 
1243
837
  {#if showDerived}
1244
- <div class="scales-row">
1245
- {#each grayScales.filter(s => s.isText) as scale}
1246
- <OverridesPanel
1247
- {scale}
1248
- editorOpen={scaleEditorOpen[scale.title] ?? false}
1249
- snapped={snappedScales.has(scale.title)}
1250
- supportsSnap={true}
1251
- {cssNamespace}
1252
- {scaleCurves}
1253
- {curveOffset}
1254
- {defaultScaleCurves}
1255
- {overrides}
1256
- {editingKey}
1257
- {snapPickerKey}
1258
- {copiedKey}
1259
- {copiedLabelKey}
1260
- {paletteComputed}
1261
- derivedHexFor={derivedHexForGray}
1262
- effectiveHexFor={effectiveHexAny}
1263
- stepKeyFor={stepKey}
1264
- scaleCurveKeyFor={getScaleCurveKey}
1265
- onToggleSnap={toggleSnapAll}
1266
- onClearScaleOverrides={clearScaleOverrides}
1267
- onToggleEditor={toggleScaleEditor}
1268
- onResetOverride={resetOverride}
1269
- onOverrideClick={handleOverrideClick}
1270
- onSnappedClick={handleSnappedClick}
1271
- onSelectSnapValue={selectSnapValue}
1272
- onCopyHex={copyHex}
1273
- onCopyVarName={copyVarName}
1274
- onSetScaleCurve={setScaleCurve}
1275
- onOffsetChange={handleOffset}
1276
- />
1277
- {/each}
1278
- </div>
1279
- <!-- Surfaces & Borders for gray mode -->
1280
- <div class="scales-row">
1281
- {#each grayScales.filter(s => !s.isText) as scale}
1282
- <OverridesPanel
1283
- {scale}
1284
- editorOpen={scaleEditorOpen[scale.title] ?? false}
1285
- snapped={false}
1286
- supportsSnap={false}
1287
- {cssNamespace}
1288
- {scaleCurves}
1289
- {curveOffset}
1290
- {defaultScaleCurves}
1291
- {overrides}
1292
- {editingKey}
1293
- {snapPickerKey}
1294
- {copiedKey}
1295
- {copiedLabelKey}
1296
- {paletteComputed}
1297
- derivedHexFor={derivedHexForGray}
1298
- effectiveHexFor={effectiveHexAny}
1299
- stepKeyFor={stepKey}
1300
- scaleCurveKeyFor={getScaleCurveKey}
1301
- onToggleSnap={toggleSnapAll}
1302
- onClearScaleOverrides={clearScaleOverrides}
1303
- onToggleEditor={toggleScaleEditor}
1304
- onResetOverride={resetOverride}
1305
- onOverrideClick={handleOverrideClick}
1306
- onSnappedClick={handleSnappedClick}
1307
- onSelectSnapValue={selectSnapValue}
1308
- onCopyHex={copyHex}
1309
- onCopyVarName={copyVarName}
1310
- onSetScaleCurve={setScaleCurve}
1311
- onOffsetChange={handleOffset}
1312
- />
1313
- {/each}
1314
- </div>
1315
- {/if}
838
+ {@const activeScales = mode === 'gray' ? grayScales : scales}
839
+ {@const derivedHexFor = mode === 'gray' ? derivedHexForGray : derivedHexForBase}
840
+ <div class="scales-row">
841
+ {#each activeScales.filter(s => s.isText) as scale}
842
+ {@render overridesPanel(scale, true, derivedHexFor)}
843
+ {/each}
844
+ </div>
845
+ <div class="scales-row">
846
+ {#each activeScales.filter(s => !s.isText) as scale}
847
+ {@render overridesPanel(scale, mode !== 'gray', derivedHexFor)}
848
+ {/each}
849
+ </div>
1316
850
  {/if}
1317
851
 
1318
852
  <!-- Color Edit Panel (non-base edits) -->
@@ -1342,7 +876,7 @@
1342
876
  padding: var(--ui-space-16) var(--ui-space-16) var(--ui-space-24);
1343
877
  background: none;
1344
878
  border: none;
1345
- border-bottom: 1px solid var(--ui-border-faint);
879
+ border-bottom: 1px solid var(--ui-border-low);
1346
880
  font-family: var(--ui-font-sans);
1347
881
  min-width: 0;
1348
882
  }
@@ -1363,7 +897,7 @@
1363
897
  font-size: var(--ui-font-size-md);
1364
898
  color: var(--ui-text-tertiary);
1365
899
  background: none;
1366
- border: 1px solid var(--ui-border-subtle);
900
+ border: 1px solid var(--ui-border-low);
1367
901
  border-radius: var(--ui-radius-sm);
1368
902
  padding: var(--ui-space-2) var(--ui-space-6);
1369
903
  cursor: pointer;
@@ -1371,7 +905,7 @@
1371
905
 
1372
906
  .edit-toggle:hover {
1373
907
  color: var(--ui-text-primary);
1374
- border-color: var(--ui-border-medium);
908
+ border-color: var(--ui-border-high);
1375
909
  }
1376
910
 
1377
911
  .derived-toggle {
@@ -1474,7 +1008,7 @@
1474
1008
  width: 100%;
1475
1009
  height: 2rem;
1476
1010
  border-radius: var(--ui-radius-sm);
1477
- border: 1px solid var(--ui-border-faint);
1011
+ border: 1px solid var(--ui-border-low);
1478
1012
  }
1479
1013
 
1480
1014
  /* Step hex values */
@@ -1530,12 +1064,12 @@
1530
1064
  }
1531
1065
 
1532
1066
  .swatch.gray-swatch:hover {
1533
- border-color: var(--ui-border-medium);
1067
+ border-color: var(--ui-border-high);
1534
1068
  }
1535
1069
 
1536
1070
  .swatch.gray-swatch.active {
1537
- border-color: var(--ui-border-strong);
1538
- outline: 2px solid var(--ui-border-medium);
1071
+ border-color: var(--ui-border-higher);
1072
+ outline: 2px solid var(--ui-border-high);
1539
1073
  outline-offset: 1px;
1540
1074
  }
1541
1075