@motion-proto/live-tokens 0.6.2 → 0.8.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 (232) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +854 -226
  3. package/dist-plugin/index.d.cts +2 -1
  4. package/dist-plugin/index.d.ts +2 -1
  5. package/dist-plugin/index.js +852 -225
  6. package/package.json +26 -40
  7. package/src/{styles → app}/site.css +1 -1
  8. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  9. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  10. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  11. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +37 -30
  12. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  13. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  14. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  15. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  16. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  17. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +67 -38
  18. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  19. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  20. package/src/editor/component-editor/SectionDividerEditor.svelte +565 -0
  21. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  22. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +29 -21
  23. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  24. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  25. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  26. package/src/editor/component-editor/editors.d.ts +10 -0
  27. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  28. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +54 -15
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +151 -424
  31. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  32. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  33. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  34. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  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 -12
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  39. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  40. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +85 -0
  41. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  42. package/src/editor/component-editor/scaffolding/StateBlock.svelte +345 -0
  43. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +17 -12
  44. package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +13 -1
  45. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +858 -0
  46. package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +1 -0
  47. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  48. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  49. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +25 -0
  50. package/src/{lib → editor/core/components}/componentConfigKeys.ts +8 -0
  51. package/src/{lib → editor/core/components}/componentConfigService.ts +3 -3
  52. package/src/{lib → editor/core/components}/componentPersist.ts +11 -9
  53. package/src/editor/core/flashStatus.ts +30 -0
  54. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  55. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  56. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  57. package/src/editor/core/manifests/manifestService.ts +171 -0
  58. package/src/editor/core/palettes/familySwap.ts +99 -0
  59. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +71 -2
  60. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +9 -6
  61. package/src/editor/core/productionPulse.ts +37 -0
  62. package/src/{lib → editor/core/routing}/router.ts +1 -1
  63. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  64. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  65. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  66. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  67. package/src/{lib → editor/core/store}/editorStore.ts +222 -28
  68. package/src/{lib → editor/core/store}/editorTypes.ts +56 -13
  69. package/src/editor/core/store/gradientSource.ts +192 -0
  70. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  71. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  72. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  73. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  74. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  75. package/src/{lib → editor/core/themes}/migrations/index.ts +10 -0
  76. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  77. package/src/{lib → editor/core/themes}/slices/components.ts +20 -6
  78. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  79. package/src/{lib → editor/core/themes}/slices/gradients.ts +89 -14
  80. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  81. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  82. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  83. package/src/{lib → editor/core/themes}/themeInit.ts +8 -8
  84. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  85. package/src/{lib → editor/core/themes}/themeTypes.ts +67 -8
  86. package/src/editor/index.ts +69 -0
  87. package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -1
  88. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +80 -129
  89. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  90. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  91. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  92. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  93. package/src/{styles → editor/styles}/ui-editor.css +43 -22
  94. package/src/{styles → editor/styles}/ui-form-controls.css +23 -24
  95. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +119 -68
  96. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  97. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +7 -6
  98. package/src/editor/ui/FileLoadList.svelte +367 -0
  99. package/src/editor/ui/FilePill.svelte +80 -0
  100. package/src/editor/ui/FontStackEditor.svelte +499 -0
  101. package/src/editor/ui/GradientEditor.svelte +690 -0
  102. package/src/{ui → editor/ui}/GradientStopPicker.svelte +12 -4
  103. package/src/editor/ui/ManifestFileManager.svelte +438 -0
  104. package/src/{ui → editor/ui}/PaletteEditor.svelte +180 -673
  105. package/src/editor/ui/ProjectFontsSection.svelte +638 -0
  106. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  107. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  108. package/src/editor/ui/ThemeFileManager.svelte +783 -0
  109. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  110. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -7
  111. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +4 -1
  112. package/src/editor/ui/UIInfoPopover.svelte +243 -0
  113. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  114. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  115. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  116. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  117. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +57 -30
  118. package/src/editor/ui/UIPillButton.svelte +168 -0
  119. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  120. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  121. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  122. package/src/editor/ui/UISquareButton.svelte +172 -0
  123. package/src/{ui → editor/ui}/UITokenSelector.svelte +14 -11
  124. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  125. package/src/{ui → editor/ui}/VariablesTab.svelte +46 -17
  126. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  127. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +24 -47
  128. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +11 -8
  129. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  130. package/src/editor/ui/palette/paletteMath.ts +275 -0
  131. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -18
  132. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +8 -8
  133. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +18 -18
  134. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +23 -23
  135. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  136. package/src/{components → system/components}/Badge.svelte +0 -36
  137. package/src/{components → system/components}/Button.svelte +2 -2
  138. package/src/{components → system/components}/Card.svelte +34 -60
  139. package/src/{components → system/components}/CollapsibleSection.svelte +25 -2
  140. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  141. package/src/{components → system/components}/Dialog.svelte +1 -1
  142. package/src/system/components/FloatingTokenTags.css +275 -0
  143. package/src/system/components/FloatingTokenTags.svelte +543 -0
  144. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  145. package/src/system/components/MenuSelect.svelte +229 -0
  146. package/src/{components → system/components}/Notification.svelte +8 -1
  147. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  148. package/src/system/components/SectionDivider.svelte +560 -0
  149. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  150. package/src/{components → system/components}/TabBar.svelte +81 -65
  151. package/src/{components → system/components}/Table.svelte +17 -3
  152. package/src/{components → system/components}/Tooltip.svelte +6 -4
  153. package/src/system/styles/CONVENTIONS.md +178 -0
  154. package/src/system/styles/fonts.css +20 -0
  155. package/src/system/styles/tokens.css +601 -0
  156. package/src/system/styles/tokens.generated.css +544 -0
  157. package/src/component-editor/ImageEditor.svelte +0 -74
  158. package/src/component-editor/SectionDividerEditor.svelte +0 -265
  159. package/src/component-editor/scaffolding/DividerEditor.svelte +0 -94
  160. package/src/component-editor/scaffolding/GradientCard.svelte +0 -296
  161. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  162. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  163. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  164. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  165. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  166. package/src/components/SectionDivider.svelte +0 -483
  167. package/src/data/google-fonts.json +0 -75
  168. package/src/lib/index.ts +0 -68
  169. package/src/lib/presetService.ts +0 -214
  170. package/src/lib/productionPulse.ts +0 -32
  171. package/src/styles/fonts.css +0 -30
  172. package/src/styles/tokens.css +0 -1324
  173. package/src/ui/FontStackEditor.svelte +0 -361
  174. package/src/ui/GradientEditor.svelte +0 -470
  175. package/src/ui/PresetFileManager.svelte +0 -1116
  176. package/src/ui/ProjectFontsSection.svelte +0 -645
  177. package/src/ui/ThemeFileManager.svelte +0 -1020
  178. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  179. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  180. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  181. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  182. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  183. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  184. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  185. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  186. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  187. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  188. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  189. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  190. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  191. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  192. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  193. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  194. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  195. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  196. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  197. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  198. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  199. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  200. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  201. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  202. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  203. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  204. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  205. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  206. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  207. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  208. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  209. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  210. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  211. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  212. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  213. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  214. /package/src/{ui → editor/ui}/index.ts +0 -0
  215. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  216. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  217. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  218. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  219. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  220. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  221. /package/src/{assets → system/assets}/offering.webp +0 -0
  222. /package/src/{components → system/components}/Callout.svelte +0 -0
  223. /package/src/{components → system/components}/Image.svelte +0 -0
  224. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  225. /package/src/{components → system/components}/types.ts +0 -0
  226. /package/src/{styles → system/styles}/_padding.scss +0 -0
  227. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  228. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  229. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  230. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  231. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  232. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -1,24 +1,34 @@
1
1
  <script lang="ts">
2
- import { run, stopPropagation, createBubbler } from 'svelte/legacy';
2
+ import { stopPropagation, createBubbler } from 'svelte/legacy';
3
3
 
4
4
  const bubble = createBubbler();
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';
5
+ import { onMount, onDestroy, tick, untrack } from 'svelte';
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
+ import UIPillButton from './UIPillButton.svelte';
10
11
  import GradientStopEditor from './palette/GradientStopEditor.svelte';
11
12
  import ScaleCurveEditor from './palette/ScaleCurveEditor.svelte';
12
13
  import PaletteBase from './palette/PaletteBase.svelte';
13
14
  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';
15
+ import {
16
+ type Step, type Scale, type GrayStep, type CurveOffset, type ScaleCurves,
17
+ GRAY_FALLBACK, DEFAULT_TINT_CHROMA,
18
+ DEFAULT_PALETTE_LIGHTNESS, DEFAULT_PALETTE_SATURATION, DEFAULT_GRAY_LIGHTNESS, DEFAULT_GRAY_SATURATION,
19
+ defaultScaleCurves, defaultScaleCurvesObject,
20
+ paletteStepLightness, graySteps, scales,
21
+ paletteStepKey, grayStepKey, stepKey, scaleCurveKey as getScaleCurveKey,
22
+ stepIndexToX,
23
+ injectLockedAnchor, removeLockedAnchor,
24
+ computeGrayColor as computeGrayColorPure,
25
+ computePaletteColor as computePaletteColorPure,
26
+ computeDerivedColor as computeDerivedColorPure,
27
+ snapScaleToPalette as snapScaleToPalettePure,
28
+ } from './palette/paletteMath';
29
+ import type { PaletteConfig, GradientStop } from '../core/themes/themeTypes';
30
+ import { editorState, mutate, setPaletteConfig, beginSliderGesture, beginScope, commitScope, cancelScope, type Scope } from '../core/store/editorStore';
31
+ import { showCopyPopover } from './copyPopover';
22
32
 
23
33
  interface Props {
24
34
  label: string;
@@ -56,14 +66,6 @@
56
66
  };
57
67
  }
58
68
 
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
69
  function edit<K extends keyof PaletteConfig>(field: K, value: PaletteConfig[K]): void {
68
70
  mutate(`${label}: ${String(field)}`, (s) => {
69
71
  if (!s.palettes[label]) s.palettes[label] = defaultPaletteConfig();
@@ -78,156 +80,64 @@
78
80
  });
79
81
  }
80
82
 
81
- // --- Transient UI state (not persisted; not in PaletteConfig) ---
82
- let lockedLightnessIdx: number | null = $state(null);
83
- let lockedSaturationIdx: number | null = $state(null);
83
+ let lockedLightnessIdx: number | null = $derived.by(() => {
84
+ if (!anchorToBase) return null;
85
+ const x500 = stepIndexToX(4);
86
+ const idx = lightnessCurve.findIndex(a => Math.abs(a.x - x500) < 0.5);
87
+ return idx >= 0 ? idx : null;
88
+ });
89
+ let lockedSaturationIdx: number | null = $derived.by(() => {
90
+ if (!anchorToBase) return null;
91
+ const x500 = stepIndexToX(4);
92
+ const idx = saturationCurve.findIndex(a => Math.abs(a.x - x500) < 0.5);
93
+ return idx >= 0 ? idx : null;
94
+ });
84
95
 
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.
96
+ // Held at component scope so inline header-swatch handlers and the function
97
+ // handlers share one handle for commit/cancel.
89
98
  let paletteEditScope: Scope | null = null;
90
99
 
100
+ function openSession() {
101
+ paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
102
+ }
103
+
91
104
  function stopColor(stop: GradientStop, pc: typeof paletteComputed): string {
92
105
  const ps = pc?.find(p => p.label === stop.paletteLabel);
93
106
  return ps ? ps.effective : '#000000';
94
107
  }
95
108
 
96
- let gradientColorStops = $state('');
97
- let gradientCssValue = $state('');
98
- let gradientBarPreview = $state('');
99
-
100
109
  function onEmptyModeChange(e: Event) {
101
110
  edit('emptyMode', (e.currentTarget as HTMLInputElement).checked ? 'gradient' : 'solid');
102
111
  }
103
112
 
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
113
  let grayEditorOpen = $state(false);
128
114
  let showDerived = $state(false);
129
-
130
- // --- Palette curve editors (lightness + saturation) ---
131
115
  let paletteEditorOpen = $state(false);
132
116
 
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
117
  function setLightnessCurve(a: CurveAnchor[]) { edit('lightnessCurve', a); }
140
118
  function setSaturationCurve(a: CurveAnchor[]) { edit('saturationCurve', a); }
141
119
  function setGrayLightnessCurve(a: CurveAnchor[]) { edit('grayLightnessCurve', a); }
142
120
  function setGraySaturationCurve(a: CurveAnchor[]) { edit('graySaturationCurve', a); }
143
121
 
144
- // --- Curve offset + clipboard (shared across all curve editors) ---
145
-
146
122
  function handleOffset(key: string, value: number) {
147
123
  edit('curveOffset', { ...curveOffset, [key]: value });
148
124
  }
149
125
 
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
126
  let editing: EditingState = $state(idleState);
165
127
 
128
+ let injectedLightness = false;
129
+ let injectedSaturation = false;
166
130
 
167
131
  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;
132
+ return computeGrayColorPure(index, hue, chroma, grayLightnessCurve, graySaturationCurve, curveOffset);
210
133
  }
211
134
 
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 };
135
+ function computePaletteColor(index: number, base: string): string {
136
+ return computePaletteColorPure(index, base, lightnessCurve, saturationCurve, curveOffset);
226
137
  }
227
138
 
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);
139
+ function computeDerivedColor(step: Step, base: string, scaleTitle: string): string {
140
+ return computeDerivedColorPure(step, base, scaleTitle, scaleCurves, curveOffset);
231
141
  }
232
142
 
233
143
  /**
@@ -268,26 +178,12 @@
268
178
 
269
179
 
270
180
 
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
181
  function startBaseEdit() {
286
182
  if (editing.kind === 'editingBase') { confirmEdit(); return; }
287
183
  editing = mode === 'gray'
288
184
  ? { kind: 'editingBase', snapshotHex: gray500Hex, snapshotTintHue: tintHue, snapshotTintChroma: tintChroma }
289
185
  : { kind: 'editingBase', snapshotHex: baseColor, snapshotTintHue: null, snapshotTintChroma: null };
290
- paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
186
+ openSession();
291
187
  }
292
188
 
293
189
  function handlePaletteClick(ps: { label: string; lightness: number; index: number }) {
@@ -298,80 +194,9 @@
298
194
  }
299
195
  const current = (k in overrides) ? overrides[k] : computePaletteColor(ps.index, baseColor);
300
196
  editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
301
- paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
197
+ openSession();
302
198
  }
303
199
 
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
200
  let scaleEditorOpen: Record<string, boolean> = $state({ Surfaces: false, Borders: false, Text: false });
376
201
 
377
202
  function toggleScaleEditor(title: string) {
@@ -384,77 +209,9 @@
384
209
  edit('scaleCurves', { ...scaleCurves, [title]: { ...cur, [channel]: a } });
385
210
  }
386
211
 
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
212
  function handleColorChange(hex: string) {
455
213
  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.
214
+ // Gray mode's base is derived from tintHue/tintChroma; raw hex only applies to chromatic.
458
215
  if (mode === 'chromatic') edit('baseColor', hex);
459
216
  return;
460
217
  }
@@ -477,9 +234,9 @@
477
234
  confirmEdit();
478
235
  return;
479
236
  }
480
- const current = (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, configForScale(scaleTitle), scaleTitle);
237
+ const current = (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, scaleTitle);
481
238
  editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
482
- paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
239
+ openSession();
483
240
  }
484
241
 
485
242
  function handleGrayClick(gStep: GrayStep, index: number) {
@@ -490,13 +247,13 @@
490
247
  }
491
248
  const current = (k in overrides) ? overrides[k] : computeGrayColor(index, tintHue, tintChroma);
492
249
  editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
493
- paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
250
+ openSession();
494
251
  }
495
252
 
496
253
  async function confirmEdit() {
497
254
  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).
255
+ // Accumulate override changes into one patch so the session commit sees
256
+ // a single final state (no intermediate reactive round-trips).
500
257
  let nextOverrides = { ...overrides };
501
258
  if (editingDraft !== null) {
502
259
  const computed = computedValueForKey(editingKey);
@@ -525,8 +282,7 @@
525
282
 
526
283
  function cancelEdit() {
527
284
  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.
285
+ // Restoring the session snapshot pulls baseColor/tintHue/overrides/… back to pre-open.
530
286
  if (paletteEditScope) { cancelScope(paletteEditScope); paletteEditScope = null; }
531
287
  }
532
288
 
@@ -547,7 +303,7 @@
547
303
  for (const scale of scales) {
548
304
  for (const step of scale.steps) {
549
305
  if (stepKey(scale.title, step.name) === key) {
550
- return computeDerivedColor(step, gray500Hex, configForScale(scale.title), scale.title);
306
+ return computeDerivedColor(step, gray500Hex, scale.title);
551
307
  }
552
308
  }
553
309
  }
@@ -556,33 +312,7 @@
556
312
 
557
313
  function effectiveColor(k: string, step: Step, scaleTitle: string, _version?: string): string {
558
314
  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(', ')})`;
315
+ return (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, scaleTitle);
586
316
  }
587
317
 
588
318
  let copiedKey: string | null = $state(null);
@@ -601,42 +331,8 @@
601
331
  setTimeout(() => { copiedLabelKey = null; }, 1500);
602
332
  }
603
333
 
604
- // --- Snap-all: constrain an entire scale to unique palette steps ---
605
-
606
334
  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;
335
+ return snapScaleToPalettePure(scale, baseColor, scaleCurves, curveOffset, paletteComputed);
640
336
  }
641
337
 
642
338
  function toggleSnapAll(scale: Scale) {
@@ -668,10 +364,6 @@
668
364
  edit('overrides', next);
669
365
  }
670
366
 
671
- function scaleHasOverrides(scale: Scale): boolean {
672
- return scale.steps.some(s => stepKey(scale.title, s.name) in overrides);
673
- }
674
-
675
367
  function clearScaleOverrides(scale: Scale) {
676
368
  snapPickerKey = null;
677
369
  const nextOverrides = { ...overrides };
@@ -726,63 +418,38 @@
726
418
  if (changed) edit('overrides', next);
727
419
  }
728
420
 
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
421
  export function loadConfig(config: PaletteConfig) {
742
422
  setPaletteConfig(label, config);
743
423
  }
744
424
 
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 ?? [
425
+ // Each field reads `$editorState.palettes[label]` directly. A chained
426
+ // `$derived` of the whole config would return the same object reference on
427
+ // every mutate-in-place store update (Svelte 5 uses `===` equality) and
428
+ // short-circuit the downstream chain swatches would freeze.
429
+ let baseColor = $derived($editorState.palettes[label]?.baseColor ?? initialColor);
430
+ let tintHue = $derived($editorState.palettes[label]?.tintHue ?? 240);
431
+ let tintChroma = $derived($editorState.palettes[label]?.tintChroma ?? DEFAULT_TINT_CHROMA);
432
+ let lightnessCurve = $derived($editorState.palettes[label]?.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS());
433
+ let saturationCurve = $derived($editorState.palettes[label]?.saturationCurve ?? DEFAULT_PALETTE_SATURATION());
434
+ let grayLightnessCurve = $derived($editorState.palettes[label]?.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS());
435
+ let graySaturationCurve = $derived($editorState.palettes[label]?.graySaturationCurve ?? DEFAULT_GRAY_SATURATION());
436
+ let scaleCurves = $derived($editorState.palettes[label]?.scaleCurves ?? defaultScaleCurvesObject());
437
+ let curveOffset = $derived($editorState.palettes[label]?.curveOffset ?? { lightness: 0, saturation: 0 });
438
+ let overrides = $derived($editorState.palettes[label]?.overrides ?? {});
439
+ let snappedScales = $derived(new Set($editorState.palettes[label]?.snappedScales ?? []));
440
+ let anchorToBase = $derived($editorState.palettes[label]?.anchorToBase ?? true);
441
+ let emptyMode = $derived($editorState.palettes[label]?.emptyMode ?? 'solid');
442
+ let emptyStep = $derived($editorState.palettes[label]?.emptyStep ?? '850');
443
+ let gradientStyle = $derived($editorState.palettes[label]?.gradientStyle ?? 'linear');
444
+ let gradientAngle = $derived($editorState.palettes[label]?.gradientAngle ?? 180);
445
+ let gradientReverse = $derived($editorState.palettes[label]?.gradientReverse ?? false);
446
+ let gradientStops = $derived($editorState.palettes[label]?.gradientStops ?? [
774
447
  { position: 0, paletteLabel: '800' },
775
448
  { position: 100, paletteLabel: '950' },
776
449
  ]);
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.
450
+ let gradientSize = $derived($editorState.palettes[label]?.gradientSize ?? 'page');
780
451
  let editingKey = $derived(editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? BASE_KEY : editing.stepKey);
781
452
  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
453
  let grayComputed = $derived((() => {
787
454
  const _gl = grayLightnessCurve, _gs = graySaturationCurve, _co = curveOffset, _tc = tintChroma, _th = tintHue;
788
455
  return graySteps.map((step, index) => ({
@@ -799,39 +466,26 @@
799
466
  effective: (_ek === g.key && _ed !== null) ? _ed : (g.key in _ov) ? _ov[g.key] : g.hex,
800
467
  }));
801
468
  })());
802
- // Gray-500 hex — always the computed (curve-derived) value so derived
803
- // scales (surfaces, borders, text) update in realtime when tint changes.
469
+ // Always use the computed (curve-derived) value so derived scales update in
470
+ // realtime when tint changes.
804
471
  let gray500Hex = $derived(mode === 'gray'
805
472
  ? (grayComputed.find(g => g.step.label === '500')?.hex ?? GRAY_FALLBACK)
806
473
  : 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));
474
+ // Keep the locked lightness anchor y in sync with baseColor. Idempotent.
475
+ // `lightnessCurve` is read via `untrack` so writing it back via `edit` does
476
+ // not retrigger this effect (Svelte 5 flags the read+write pattern as
477
+ // recursive).
478
+ $effect(() => {
479
+ if (!anchorToBase || lockedLightnessIdx === null || !baseColor) return;
480
+ const targetY = hexToOklch(baseColor).l * 100;
481
+ untrack(() => {
482
+ const idx = lockedLightnessIdx;
483
+ if (idx === null) return;
484
+ const curve = lightnessCurve;
485
+ if (curve[idx] && Math.abs(curve[idx].y - targetY) > 0.01) {
486
+ edit('lightnessCurve', curve.map((a, i) => i === idx ? { ...a, y: targetY } : a));
833
487
  }
834
- }
488
+ });
835
489
  });
836
490
  let paletteComputed = $derived((() => {
837
491
  const _bc = baseColor, _lc = lightnessCurve, _sc = saturationCurve, _co = curveOffset, _ed = editingDraft, _ek = editingKey, _ov = overrides, _ab = anchorToBase;
@@ -849,43 +503,19 @@
849
503
  };
850
504
  });
851
505
  })());
852
- // Gradient reactives must follow paletteComputed
853
- run(() => {
506
+ let gradientBarPreview = $derived.by(() => {
854
507
  const pc = paletteComputed;
855
508
  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
- }
509
+ const stops = sorted.map(s => `${stopColor(s, pc)} ${s.position}%`).join(', ');
510
+ return `linear-gradient(to right, ${stops})`;
867
511
  });
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
- }) : []);
512
+ let grayScales = $derived(mode === 'gray' ? scales : []);
875
513
  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));
514
+ // Chromatic vs gray distinction is only for the `derivedHex` ("Ag" preview /
515
+ // border-color base); `effectiveColor` always uses `gray500Hex` for non-override derivation.
516
+ let derivedHexForBase = $derived((step: Step, scaleTitle: string) => computeDerivedColor(step, baseColor, scaleTitle));
517
+ let derivedHexForGray = $derived((step: Step, scaleTitle: string) => computeDerivedColor(step, gray500Hex, scaleTitle));
887
518
  let effectiveHexAny = $derived((k: string, step: Step, scaleTitle: string) => effectiveColor(k, step, scaleTitle, curveVersion));
888
- // --- Reactive editing state ---
889
519
 
890
520
  let isEditingBase = $derived(isBaseEdit(editing));
891
521
  let editingColor = $derived(isEditingBase
@@ -915,8 +545,7 @@
915
545
  ? `${editingStepInfo.scale} \u203A ${editingStepInfo.step}`
916
546
  : null);
917
547
  $effect(() => {
918
- // Re-snap whenever any of the inputs change. Touch each so the effect
919
- // tracks them explicitly (resnapScales() reads them indirectly).
548
+ // Touch each input so the effect tracks them (resnapScales reads indirectly).
920
549
  void baseColor;
921
550
  void scaleCurves;
922
551
  void lightnessCurve;
@@ -967,12 +596,10 @@
967
596
  <span>Gradient</span>
968
597
  </label>
969
598
  {/if}
970
- <button class="edit-toggle" type="button" onclick={clearPaletteOverrides}>Clear Overrides</button>
971
- <button
972
- class="edit-toggle"
973
- type="button"
974
- onclick={() => paletteEditorOpen = !paletteEditorOpen}
975
- >{paletteEditorOpen ? 'Close' : 'Edit'}</button>
599
+ <UIPillButton size="compact" variant="outline" onclick={clearPaletteOverrides}>Clear Overrides</UIPillButton>
600
+ <UIPillButton size="compact" variant="outline" onclick={() => paletteEditorOpen = !paletteEditorOpen}>
601
+ {paletteEditorOpen ? 'Close' : 'Edit'}
602
+ </UIPillButton>
976
603
  </div>
977
604
  <div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}">
978
605
  <div class="step-column">
@@ -1073,98 +700,16 @@
1073
700
 
1074
701
  </div>
1075
702
 
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
703
  {:else}
1157
704
  <!-- Gray mode: palette + text row -->
1158
705
  <div class="scales-row">
1159
706
  <div class="scale-section">
1160
707
  <div class="scale-header">
1161
708
  <h4 class="scale-title">{displayLabel ?? label}</h4>
1162
- <button class="edit-toggle" type="button" onclick={clearPaletteOverrides}>Clear Overrides</button>
1163
- <button
1164
- class="edit-toggle"
1165
- type="button"
1166
- onclick={() => grayEditorOpen = !grayEditorOpen}
1167
- >{grayEditorOpen ? 'Close' : 'Edit'}</button>
709
+ <UIPillButton size="compact" variant="outline" onclick={clearPaletteOverrides}>Clear Overrides</UIPillButton>
710
+ <UIPillButton size="compact" variant="outline" onclick={() => grayEditorOpen = !grayEditorOpen}>
711
+ {grayEditorOpen ? 'Close' : 'Edit'}
712
+ </UIPillButton>
1168
713
  </div>
1169
714
  <div class="swatch-grid" style="--swatch-cols: {graySteps.length + 2}">
1170
715
  <div class="step-column">
@@ -1234,6 +779,41 @@
1234
779
  </div>
1235
780
  </div>
1236
781
  </div>
782
+ {/if}
783
+
784
+ {#snippet overridesPanel(scale: Scale, canSnap: boolean, derivedHexFor: (step: Step, scaleTitle: string) => string)}
785
+ <OverridesPanel
786
+ {scale}
787
+ editorOpen={scaleEditorOpen[scale.title] ?? false}
788
+ snapped={canSnap && snappedScales.has(scale.title)}
789
+ supportsSnap={canSnap}
790
+ {cssNamespace}
791
+ {scaleCurves}
792
+ {curveOffset}
793
+ {defaultScaleCurves}
794
+ {overrides}
795
+ {editingKey}
796
+ {snapPickerKey}
797
+ {copiedKey}
798
+ {copiedLabelKey}
799
+ {paletteComputed}
800
+ {derivedHexFor}
801
+ effectiveHexFor={effectiveHexAny}
802
+ stepKeyFor={stepKey}
803
+ scaleCurveKeyFor={getScaleCurveKey}
804
+ onToggleSnap={toggleSnapAll}
805
+ onClearScaleOverrides={clearScaleOverrides}
806
+ onToggleEditor={toggleScaleEditor}
807
+ onResetOverride={resetOverride}
808
+ onOverrideClick={handleOverrideClick}
809
+ onSnappedClick={handleSnappedClick}
810
+ onSelectSnapValue={selectSnapValue}
811
+ onCopyHex={copyHex}
812
+ onCopyVarName={copyVarName}
813
+ onSetScaleCurve={setScaleCurve}
814
+ onOffsetChange={handleOffset}
815
+ />
816
+ {/snippet}
1237
817
 
1238
818
  <button class="derived-toggle" type="button" onclick={() => showDerived = !showDerived}>
1239
819
  <i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
@@ -1241,78 +821,18 @@
1241
821
  </button>
1242
822
 
1243
823
  {#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}
824
+ {@const activeScales = mode === 'gray' ? grayScales : scales}
825
+ {@const derivedHexFor = mode === 'gray' ? derivedHexForGray : derivedHexForBase}
826
+ <div class="scales-row">
827
+ {#each activeScales.filter(s => s.isText) as scale}
828
+ {@render overridesPanel(scale, true, derivedHexFor)}
829
+ {/each}
830
+ </div>
831
+ <div class="scales-row">
832
+ {#each activeScales.filter(s => !s.isText) as scale}
833
+ {@render overridesPanel(scale, mode !== 'gray', derivedHexFor)}
834
+ {/each}
835
+ </div>
1316
836
  {/if}
1317
837
 
1318
838
  <!-- Color Edit Panel (non-base edits) -->
@@ -1342,7 +862,7 @@
1342
862
  padding: var(--ui-space-16) var(--ui-space-16) var(--ui-space-24);
1343
863
  background: none;
1344
864
  border: none;
1345
- border-bottom: 1px solid var(--ui-border-faint);
865
+ border-bottom: 1px solid var(--ui-border-low);
1346
866
  font-family: var(--ui-font-sans);
1347
867
  min-width: 0;
1348
868
  }
@@ -1357,21 +877,7 @@
1357
877
  display: flex;
1358
878
  align-items: center;
1359
879
  gap: var(--ui-space-8);
1360
- }
1361
-
1362
- .edit-toggle {
1363
- font-size: var(--ui-font-size-md);
1364
- color: var(--ui-text-tertiary);
1365
- background: none;
1366
- border: 1px solid var(--ui-border-subtle);
1367
- border-radius: var(--ui-radius-sm);
1368
- padding: var(--ui-space-2) var(--ui-space-6);
1369
- cursor: pointer;
1370
- }
1371
-
1372
- .edit-toggle:hover {
1373
- color: var(--ui-text-primary);
1374
- border-color: var(--ui-border-medium);
880
+ padding-bottom: 0.5rem;
1375
881
  }
1376
882
 
1377
883
  .derived-toggle {
@@ -1381,17 +887,15 @@
1381
887
  padding: var(--ui-space-6) var(--ui-space-4);
1382
888
  background: none;
1383
889
  border: none;
1384
- color: var(--ui-text-tertiary);
1385
- font-size: var(--ui-font-size-sm);
1386
- font-weight: var(--ui-font-weight-semibold);
890
+ color: var(--ui-text-secondary);
891
+ font-size: var(--ui-font-size-lg);
892
+ font-weight: var(--ui-font-weight-light);
1387
893
  cursor: pointer;
1388
894
  transition: color var(--ui-transition-fast);
1389
- text-transform: uppercase;
1390
- letter-spacing: 0.04em;
1391
895
  }
1392
896
 
1393
897
  .derived-toggle:hover {
1394
- color: var(--ui-text-secondary);
898
+ color: var(--ui-text-primary);
1395
899
  }
1396
900
 
1397
901
  .derived-toggle i {
@@ -1418,12 +922,11 @@
1418
922
  }
1419
923
 
1420
924
  .scale-title {
1421
- font-size: var(--ui-font-size-md);
1422
- font-weight: var(--ui-font-weight-semibold);
1423
- color: var(--ui-text-tertiary);
925
+ font-size: var(--ui-font-size-lg);
926
+ font-weight: var(--ui-font-weight-bold);
927
+ color: var(--ui-text-primary);
1424
928
  margin: 0;
1425
- text-transform: uppercase;
1426
- letter-spacing: 0.05em;
929
+ padding-right: 1rem;
1427
930
  }
1428
931
 
1429
932
  /* Step columns */
@@ -1474,7 +977,7 @@
1474
977
  width: 100%;
1475
978
  height: 2rem;
1476
979
  border-radius: var(--ui-radius-sm);
1477
- border: 1px solid var(--ui-border-faint);
980
+ border: 1px solid var(--ui-border-low);
1478
981
  }
1479
982
 
1480
983
  /* Step hex values */
@@ -1490,7 +993,11 @@
1490
993
  background: none;
1491
994
  border: none;
1492
995
  text-align: center;
996
+ display: block;
997
+ width: 100%;
998
+ box-sizing: border-box;
1493
999
  min-width: 0;
1000
+ margin-top: var(--ui-space-2);
1494
1001
  overflow: hidden;
1495
1002
  text-overflow: ellipsis;
1496
1003
  }
@@ -1530,12 +1037,12 @@
1530
1037
  }
1531
1038
 
1532
1039
  .swatch.gray-swatch:hover {
1533
- border-color: var(--ui-border-medium);
1040
+ border-color: var(--ui-border-high);
1534
1041
  }
1535
1042
 
1536
1043
  .swatch.gray-swatch.active {
1537
- border-color: var(--ui-border-strong);
1538
- outline: 2px solid var(--ui-border-medium);
1044
+ border-color: var(--ui-border-higher);
1045
+ outline: 2px solid var(--ui-border-high);
1539
1046
  outline-offset: 1px;
1540
1047
  }
1541
1048