@motion-proto/live-tokens 0.1.1 → 0.3.2

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 (224) hide show
  1. package/README.md +168 -21
  2. package/dist-plugin/index.cjs +823 -336
  3. package/dist-plugin/index.d.cts +9 -7
  4. package/dist-plugin/index.d.ts +9 -7
  5. package/dist-plugin/index.js +822 -335
  6. package/package.json +46 -20
  7. package/src/assets/newspaper.webp +0 -0
  8. package/src/assets/offering.webp +0 -0
  9. package/src/component-editor/BadgeEditor.svelte +170 -0
  10. package/src/component-editor/CalloutEditor.svelte +103 -0
  11. package/src/component-editor/CardEditor.svelte +184 -0
  12. package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
  13. package/src/component-editor/CornerBadgeEditor.svelte +207 -0
  14. package/src/component-editor/DialogEditor.svelte +172 -0
  15. package/src/component-editor/ImageEditor.svelte +72 -0
  16. package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
  17. package/src/component-editor/NotificationEditor.svelte +160 -0
  18. package/src/component-editor/ProgressBarEditor.svelte +124 -0
  19. package/src/component-editor/RadioButtonEditor.svelte +140 -0
  20. package/src/component-editor/SectionDividerEditor.svelte +263 -0
  21. package/src/component-editor/SegmentedControlEditor.svelte +154 -0
  22. package/src/component-editor/StandardButtonsEditor.svelte +178 -0
  23. package/src/component-editor/TabBarEditor.svelte +137 -0
  24. package/src/component-editor/TableEditor.svelte +128 -0
  25. package/src/component-editor/TooltipEditor.svelte +122 -0
  26. package/src/component-editor/editorTokens.test.ts +93 -0
  27. package/src/component-editor/groupKeySlots.test.ts +67 -0
  28. package/src/component-editor/groupKeySnapshot.test.ts +52 -0
  29. package/src/component-editor/index.ts +5 -0
  30. package/src/component-editor/registry.ts +246 -0
  31. package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
  32. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
  33. package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
  34. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
  35. package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
  36. package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
  37. package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
  38. package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
  39. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
  40. package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
  41. package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
  42. package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
  43. package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
  44. package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
  45. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
  46. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
  47. package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
  48. package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
  49. package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
  50. package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
  51. package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
  52. package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
  53. package/src/component-editor/scaffolding/componentSources.ts +9 -0
  54. package/src/component-editor/scaffolding/defaultSections.ts +16 -0
  55. package/src/component-editor/scaffolding/editorContext.ts +44 -0
  56. package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
  57. package/src/component-editor/scaffolding/siblings.ts +33 -0
  58. package/src/component-editor/scaffolding/types.ts +39 -0
  59. package/src/components/Badge.svelte +231 -42
  60. package/src/components/Button.svelte +324 -124
  61. package/src/components/Callout.svelte +145 -0
  62. package/src/components/Card.svelte +123 -25
  63. package/src/components/CollapsibleSection.svelte +213 -35
  64. package/src/components/CornerBadge.svelte +224 -0
  65. package/src/components/Dialog.svelte +137 -114
  66. package/src/components/Image.svelte +43 -0
  67. package/src/components/InlineEditActions.svelte +74 -14
  68. package/src/components/Notification.svelte +184 -163
  69. package/src/components/ProgressBar.svelte +216 -22
  70. package/src/components/RadioButton.svelte +110 -40
  71. package/src/components/SectionDivider.svelte +428 -74
  72. package/src/components/SegmentedControl.svelte +203 -0
  73. package/src/components/TabBar.svelte +146 -21
  74. package/src/components/Table.svelte +102 -0
  75. package/src/components/Tooltip.svelte +45 -19
  76. package/src/components/types.ts +51 -0
  77. package/src/data/google-fonts.json +75 -0
  78. package/src/lib/ColumnsOverlay.svelte +20 -7
  79. package/src/lib/LiveEditorOverlay.svelte +257 -78
  80. package/src/lib/columnsOverlay.ts +21 -17
  81. package/src/lib/componentConfig.test.ts +204 -0
  82. package/src/lib/componentConfigKeys.ts +19 -0
  83. package/src/lib/componentConfigService.ts +88 -0
  84. package/src/lib/copyPopover.ts +30 -0
  85. package/src/lib/cssVarSync.ts +59 -7
  86. package/src/lib/editorConfigStore.ts +0 -10
  87. package/src/lib/editorCore.ts +402 -0
  88. package/src/lib/editorKeybindings.ts +52 -0
  89. package/src/lib/editorPersistence.ts +106 -0
  90. package/src/lib/editorRenderer.ts +74 -0
  91. package/src/lib/editorStore.test.ts +328 -0
  92. package/src/lib/editorStore.ts +412 -0
  93. package/src/lib/editorTypes.ts +100 -0
  94. package/src/lib/editorViewStore.ts +55 -0
  95. package/src/lib/files/versionedFileResource.ts +140 -0
  96. package/src/lib/fontLoader.ts +130 -0
  97. package/src/lib/fontMigration.ts +140 -0
  98. package/src/lib/fontParse.ts +168 -0
  99. package/src/lib/index.ts +48 -30
  100. package/src/lib/lazyConfig.test.ts +54 -0
  101. package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
  102. package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
  103. package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
  104. package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
  105. package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
  106. package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
  107. package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
  108. package/src/lib/migrations/index.ts +93 -0
  109. package/src/lib/migrations/migrations.test.ts +341 -0
  110. package/src/lib/navLinkTypes.ts +1 -0
  111. package/src/lib/overlayState.ts +3 -0
  112. package/src/lib/paletteDerivation.ts +300 -0
  113. package/src/lib/parentRouteStore.ts +42 -0
  114. package/src/lib/parsers/globalRootBlock.ts +32 -0
  115. package/src/lib/presetService.ts +94 -0
  116. package/src/lib/router.ts +42 -10
  117. package/src/lib/scrollSection.ts +45 -0
  118. package/src/lib/slices/columns.ts +59 -0
  119. package/src/lib/slices/components.ts +362 -0
  120. package/src/lib/slices/domainVars.ts +15 -0
  121. package/src/lib/slices/fonts.ts +30 -0
  122. package/src/lib/slices/gradients.ts +153 -0
  123. package/src/lib/slices/overlays.ts +132 -0
  124. package/src/lib/slices/palettes.ts +26 -0
  125. package/src/lib/slices/shadows.ts +123 -0
  126. package/src/lib/storage.ts +88 -0
  127. package/src/lib/themeInit.ts +74 -0
  128. package/src/lib/themeService.ts +101 -0
  129. package/src/lib/themeTypes.ts +146 -0
  130. package/src/lib/tokenRegistry.ts +148 -0
  131. package/src/pages/ComponentEditorPage.svelte +384 -0
  132. package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
  133. package/src/pages/Editor.svelte +98 -0
  134. package/src/pages/Editor.svelte.d.ts +2 -0
  135. package/src/pages/EditorShell.svelte +348 -0
  136. package/src/styles/_padding.scss +34 -0
  137. package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  138. package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  139. package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  140. package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  141. package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  142. package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
  143. package/src/styles/fonts.css +22 -10
  144. package/src/styles/form-controls.css +14 -16
  145. package/src/styles/tokens.css +1322 -0
  146. package/src/styles/ui-editor.css +126 -0
  147. package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
  148. package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
  149. package/src/ui/EditorViewSwitcher.svelte +180 -0
  150. package/src/ui/FontStackEditor.svelte +360 -0
  151. package/src/ui/GradientEditor.svelte +461 -0
  152. package/src/ui/GradientStopPicker.svelte +74 -0
  153. package/src/ui/PaletteEditor.svelte +1590 -0
  154. package/src/ui/PaletteEditor.test.ts +108 -0
  155. package/src/ui/PresetFileManager.svelte +567 -0
  156. package/src/ui/ProjectFontsSection.svelte +645 -0
  157. package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
  158. package/src/{showcase → ui}/TextTab.svelte +27 -27
  159. package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
  160. package/src/ui/Toggle.svelte +108 -0
  161. package/src/ui/UICopyPopover.svelte +78 -0
  162. package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
  163. package/src/ui/UIFontFamilySelector.svelte +309 -0
  164. package/src/ui/UIFontSizeSelector.svelte +165 -0
  165. package/src/ui/UIFontWeightSelector.svelte +52 -0
  166. package/src/ui/UILineHeightSelector.svelte +47 -0
  167. package/src/ui/UILinkToggle.svelte +60 -0
  168. package/src/ui/UIOptionItem.svelte +74 -0
  169. package/src/ui/UIOptionList.svelte +27 -0
  170. package/src/ui/UIPaddingSelector.svelte +661 -0
  171. package/src/ui/UIPaletteSelector.svelte +1084 -0
  172. package/src/ui/UIRadio.svelte +72 -0
  173. package/src/ui/UIRadioGroup.svelte +59 -0
  174. package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
  175. package/src/ui/UITokenSelector.svelte +509 -0
  176. package/src/ui/UIVariantSelector.svelte +145 -0
  177. package/src/ui/VariablesTab.svelte +252 -0
  178. package/src/ui/index.ts +31 -0
  179. package/src/ui/keepInViewport.ts +84 -0
  180. package/src/ui/palette/GradientStopEditor.svelte +482 -0
  181. package/src/ui/palette/OverridesPanel.svelte +526 -0
  182. package/src/ui/palette/PaletteBase.svelte +165 -0
  183. package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
  184. package/src/ui/palette/paletteEditorState.ts +89 -0
  185. package/src/ui/sections/ColumnsSection.svelte +273 -0
  186. package/src/ui/sections/GradientsSection.svelte +147 -0
  187. package/src/ui/sections/OverlaysSection.svelte +670 -0
  188. package/src/ui/sections/ShadowsSection.svelte +1250 -0
  189. package/src/ui/sections/TokenScaleTable.svelte +332 -0
  190. package/src/ui/sections/tokenScales.ts +81 -0
  191. package/src/ui/variantScales.ts +108 -0
  192. package/src/components/DetailNav.svelte +0 -78
  193. package/src/components/Toggle.svelte +0 -86
  194. package/src/lib/tokenInit.ts +0 -29
  195. package/src/lib/tokenService.ts +0 -144
  196. package/src/lib/tokenTypes.ts +0 -45
  197. package/src/pages/Admin.svelte +0 -100
  198. package/src/pages/ShowcasePage.svelte +0 -144
  199. package/src/showcase/BackupBrowser.svelte +0 -617
  200. package/src/showcase/ComponentsTab.svelte +0 -105
  201. package/src/showcase/PaletteEditor.svelte +0 -2579
  202. package/src/showcase/PaletteSelector.svelte +0 -627
  203. package/src/showcase/TokenMap.svelte +0 -54
  204. package/src/showcase/VariablesTab.svelte +0 -2655
  205. package/src/showcase/VisualsTab.svelte +0 -231
  206. package/src/showcase/demos/BadgeDemo.svelte +0 -56
  207. package/src/showcase/demos/CardDemo.svelte +0 -50
  208. package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
  209. package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
  210. package/src/showcase/demos/DialogDemo.svelte +0 -42
  211. package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
  212. package/src/showcase/demos/NotificationDemo.svelte +0 -147
  213. package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
  214. package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
  215. package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
  216. package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
  217. package/src/showcase/demos/TabBarDemo.svelte +0 -58
  218. package/src/showcase/demos/TooltipDemo.svelte +0 -52
  219. package/src/showcase/editor.css +0 -93
  220. package/src/showcase/index.ts +0 -17
  221. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  222. package/src/styles/fonts/Domine/OFL.txt +0 -97
  223. package/src/styles/fonts/Domine/README.txt +0 -66
  224. /package/src/{showcase → ui}/curveEngine.ts +0 -0
@@ -1,2579 +0,0 @@
1
- <script lang="ts">
2
- import { onMount, onDestroy, tick } from 'svelte';
3
- import Button from '../components/Button.svelte';
4
- import { hexToOklch, oklchToHex, gamutClamp } from '../lib/oklch';
5
- import { type CurveAnchor, makeAnchor, sampleCurve, lightnessCurveConfig, saturationCurveConfig, textLightnessCurveConfig } from './curveEngine';
6
- import BezierCurveEditor from './BezierCurveEditor.svelte';
7
- import ColorEditPanel from './ColorEditPanel.svelte';
8
- import Toggle from '../components/Toggle.svelte';
9
- import type { PaletteConfig, GradientStyle, GradientStop } from '../lib/tokenTypes';
10
- import { editorConfigs, loadedConfigs, configsLoadedFromFile } from '../lib/editorConfigStore';
11
- import { setCssVar as setCssVarSync } from '../lib/cssVarSync';
12
-
13
- export let label: string;
14
- export let initialColor: string = '#808080';
15
- export let mode: 'chromatic' | 'gray' = 'chromatic';
16
- export let cssNamespace: string | null = null;
17
- export let saveSignal: number = 0;
18
- export let emptySelector: boolean = false;
19
-
20
- // --- Empty selector state ---
21
- let emptyMode: 'solid' | 'gradient' = 'solid';
22
- let emptyStep: string = '850';
23
-
24
- // --- Gradient state ---
25
- let gradientStyle: GradientStyle = 'linear';
26
- let gradientAngle: number = 180;
27
- let gradientReverse: boolean = false;
28
- let anchorToBase: boolean = true;
29
- let lockedLightnessIdx: number | null = null;
30
- let lockedSaturationIdx: number | null = null;
31
- let lastAnchorToBase: boolean = false;
32
- let gradientSize: 'page' | 'window' = 'page';
33
- let gradientStops: GradientStop[] = [
34
- { position: 0, paletteLabel: '800' },
35
- { position: 100, paletteLabel: '950' },
36
- ];
37
- let draggingStopIndex: number | null = null;
38
- let selectedStopIndex: number = 0;
39
-
40
- function stopColor(stop: GradientStop, pc: typeof paletteComputed): string {
41
- const ps = pc?.find(p => p.label === stop.paletteLabel);
42
- return ps ? ps.effective : '#000000';
43
- }
44
-
45
- let gradientColorStops = '';
46
- let gradientCssValue = '';
47
- let gradientBarPreview = '';
48
-
49
- // Must run after paletteComputed is defined — see reactive block below
50
-
51
- function addGradientStop(position: number) {
52
- // Find nearest palette color by interpolating between surrounding stops
53
- const nearest = paletteComputed.reduce((prev, curr) => {
54
- const prevDist = Math.abs(parseInt(prev.label) - 500);
55
- const currDist = Math.abs(parseInt(curr.label) - 500);
56
- return currDist < prevDist ? curr : prev;
57
- });
58
- gradientStops = [...gradientStops, { position, paletteLabel: nearest.label }];
59
- selectedStopIndex = gradientStops.length - 1;
60
- }
61
-
62
- function removeGradientStop(index: number) {
63
- if (gradientStops.length <= 2) return;
64
- gradientStops = gradientStops.filter((_, i) => i !== index);
65
- if (selectedStopIndex >= gradientStops.length) selectedStopIndex = gradientStops.length - 1;
66
- }
67
-
68
- function handleStopHandleMouseDown(e: MouseEvent, i: number) {
69
- selectedStopIndex = i;
70
- draggingStopIndex = i;
71
- const bar = (e.currentTarget as HTMLElement).parentElement!;
72
- const rect = bar.getBoundingClientRect();
73
- function onMove(me: MouseEvent) {
74
- if (draggingStopIndex === null) return;
75
- const newPos = Math.round(Math.max(0, Math.min(100, ((me.clientX - rect.left) / rect.width) * 100)));
76
- gradientStops[draggingStopIndex].position = newPos;
77
- gradientStops = gradientStops;
78
- }
79
- function onUp() {
80
- draggingStopIndex = null;
81
- window.removeEventListener('mousemove', onMove);
82
- window.removeEventListener('mouseup', onUp);
83
- }
84
- window.addEventListener('mousemove', onMove);
85
- window.addEventListener('mouseup', onUp);
86
- }
87
-
88
- function handleStopBarMouseDown(e: MouseEvent) {
89
- const bar = (e.currentTarget as HTMLElement);
90
- const rect = bar.getBoundingClientRect();
91
- const pos = Math.round(((e.clientX - rect.left) / rect.width) * 100);
92
-
93
- // Check if clicking near an existing stop
94
- const nearIdx = gradientStops.findIndex(s => Math.abs(s.position - pos) < 4);
95
- if (nearIdx >= 0) {
96
- selectedStopIndex = nearIdx;
97
- draggingStopIndex = nearIdx;
98
- } else {
99
- addGradientStop(Math.max(0, Math.min(100, pos)));
100
- draggingStopIndex = gradientStops.length - 1;
101
- }
102
-
103
- function onMove(me: MouseEvent) {
104
- if (draggingStopIndex === null) return;
105
- const newPos = Math.round(Math.max(0, Math.min(100, ((me.clientX - rect.left) / rect.width) * 100)));
106
- gradientStops[draggingStopIndex].position = newPos;
107
- gradientStops = gradientStops;
108
- }
109
- function onUp() {
110
- draggingStopIndex = null;
111
- window.removeEventListener('mousemove', onMove);
112
- window.removeEventListener('mouseup', onUp);
113
- }
114
- window.addEventListener('mousemove', onMove);
115
- window.addEventListener('mouseup', onUp);
116
- }
117
-
118
- const STORAGE_KEY = `palette-editor-${label}`;
119
-
120
- let baseColor = initialColor;
121
-
122
- // --- Gray mode ---
123
-
124
- interface GrayStep {
125
- label: string;
126
- hue: number;
127
- saturation: number;
128
- lightness: number;
129
- }
130
-
131
- const graySteps: GrayStep[] = [
132
- { label: '100', hue: 240, saturation: 5, lightness: 92 },
133
- { label: '200', hue: 220, saturation: 13, lightness: 84 },
134
- { label: '300', hue: 216, saturation: 12, lightness: 72 },
135
- { label: '400', hue: 240, saturation: 5, lightness: 61 },
136
- { label: '500', hue: 240, saturation: 5, lightness: 50 },
137
- { label: '600', hue: 240, saturation: 5, lightness: 42 },
138
- { label: '700', hue: 240, saturation: 5, lightness: 34 },
139
- { label: '800', hue: 240, saturation: 10, lightness: 25 },
140
- { label: '850', hue: 229, saturation: 20, lightness: 18 },
141
- { label: '900', hue: 240, saturation: 30, lightness: 10 },
142
- { label: '950', hue: 229, saturation: 34, lightness: 3 },
143
- ];
144
-
145
- let tintHue = 240;
146
- let grayEditorOpen = false;
147
- let showDerived = false;
148
-
149
- // --- Palette curve editors (lightness + saturation) ---
150
- let paletteEditorOpen = false;
151
-
152
- // Default curve anchors (used for initial state and reset)
153
- const DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
154
- const DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
155
- const DEFAULT_GRAY_LIGHTNESS = () => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
156
- const DEFAULT_GRAY_SATURATION = () => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
157
-
158
- let lightnessCurve: CurveAnchor[] = DEFAULT_PALETTE_LIGHTNESS();
159
- let saturationCurve: CurveAnchor[] = DEFAULT_PALETTE_SATURATION();
160
- let grayLightnessCurve: CurveAnchor[] = DEFAULT_GRAY_LIGHTNESS();
161
- let graySaturationCurve: CurveAnchor[] = DEFAULT_GRAY_SATURATION();
162
-
163
- function setLightnessCurve(a: CurveAnchor[]) { lightnessCurve = a; }
164
- function setSaturationCurve(a: CurveAnchor[]) { saturationCurve = a; }
165
- function setGrayLightnessCurve(a: CurveAnchor[]) { grayLightnessCurve = a; }
166
- function setGraySaturationCurve(a: CurveAnchor[]) { graySaturationCurve = a; }
167
-
168
- const gradientStyleOptions: { value: GradientStyle; icon: string; title: string }[] = [
169
- { value: 'linear', icon: '/', title: 'Linear' },
170
- { value: 'radial', icon: '\u25CB', title: 'Radial' },
171
- { value: 'conic', icon: '\u25D4', title: 'Conic' },
172
- ];
173
-
174
- const gradientSizeOptions: { value: 'page' | 'window'; label: string; title: string }[] = [
175
- { value: 'page', label: 'Page', title: 'Gradient stretches over the full scrollable page' },
176
- { value: 'window', label: 'Window', title: 'Gradient stays fixed to the viewport' },
177
- ];
178
-
179
- // --- Curve offset + clipboard (shared across all curve editors) ---
180
-
181
- let curveOffset: Record<string, number> = { lightness: 0, saturation: 0 };
182
-
183
- function handleOffset(key: string, value: number) {
184
- curveOffset[key] = value;
185
- curveOffset = curveOffset;
186
- }
187
-
188
- // Gray step index to curve x-position
189
- function grayStepToX(index: number): number {
190
- return graySteps.length > 1 ? (index / (graySteps.length - 1)) * 100 : 50;
191
- }
192
-
193
- // Base chroma for gray tinting (editable via the color panel's chroma slider)
194
- const DEFAULT_TINT_CHROMA = 0.04;
195
- let tintChroma = DEFAULT_TINT_CHROMA;
196
-
197
- // Snapshots for cancel in gray base editing (avoids lossy hex round-trip)
198
- let snapshotTintHue: number | null = null;
199
- let snapshotTintChroma: number | null = null;
200
-
201
- function computeGrayColor(index: number, hue: number, chroma: number = tintChroma): string {
202
- const xPos = grayStepToX(index);
203
- const lOff = curveOffset['gray-lightness'] ?? 0;
204
- const sOff = curveOffset['gray-saturation'] ?? 0;
205
-
206
- const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
207
- const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
208
- const targetC = chroma * satMul;
209
-
210
- const clamped = gamutClamp(targetL, targetC, hue);
211
- return oklchToHex(clamped.l, clamped.c, clamped.h);
212
- }
213
-
214
- function grayStepKey(label: string): string {
215
- return `gray-${label}`;
216
- }
217
-
218
- // Reactive map of computed gray colors
219
- $: grayComputed = (() => {
220
- const _gl = grayLightnessCurve, _gs = graySaturationCurve, _co = curveOffset, _tc = tintChroma, _th = tintHue;
221
- return graySteps.map((step, index) => ({
222
- step,
223
- index,
224
- key: grayStepKey(step.label),
225
- hex: computeGrayColor(index, _th, _tc),
226
- }));
227
- })();
228
-
229
- $: grayEffective = (() => {
230
- const _ed = editingDraft, _ek = editingKey, _ov = overrides;
231
- return grayComputed.map(g => ({
232
- ...g,
233
- effective: (_ek === g.key && _ed !== null) ? _ed : (g.key in _ov) ? _ov[g.key] : g.hex,
234
- }));
235
- })();
236
-
237
- // Gray-500 hex — always the computed (curve-derived) value so derived
238
- // scales (surfaces, borders, text) update in realtime when tint changes.
239
- $: gray500Hex = mode === 'gray'
240
- ? (grayComputed.find(g => g.step.label === '500')?.hex ?? '#808080')
241
- : baseColor;
242
-
243
-
244
- // --- Chromatic palette steps ---
245
-
246
- const paletteStepLightness = [
247
- { label: '100', lightness: 95 },
248
- { label: '200', lightness: 88 },
249
- { label: '300', lightness: 78 },
250
- { label: '400', lightness: 68 },
251
- { label: '500', lightness: 57 },
252
- { label: '600', lightness: 49 },
253
- { label: '700', lightness: 41 },
254
- { label: '800', lightness: 32 },
255
- { label: '850', lightness: 25 },
256
- { label: '900', lightness: 17 },
257
- { label: '950', lightness: 8 },
258
- ];
259
-
260
- function paletteStepKey(label: string): string {
261
- return `Palette-${label}`;
262
- }
263
-
264
- function stepIndexToX(index: number): number {
265
- return (index / (paletteStepLightness.length - 1)) * 100;
266
- }
267
-
268
- // --- Locked anchor management ---
269
-
270
- let injectedLightness = false;
271
- let injectedSaturation = false;
272
-
273
- function injectLockedAnchor(curve: CurveAnchor[], x: number, y: number): { curve: CurveAnchor[], idx: number, injected: boolean } {
274
- const existing = curve.findIndex(a => Math.abs(a.x - x) < 0.5);
275
- if (existing >= 0) {
276
- if (curve[existing].x === x && Math.abs(curve[existing].y - y) < 0.01) return { curve, idx: existing, injected: false };
277
- return { curve: curve.map((a, i) => i === existing ? { ...a, x, y } : a), idx: existing, injected: false };
278
- }
279
- let insertAt = curve.findIndex(a => a.x > x);
280
- if (insertAt < 0) insertAt = curve.length;
281
- return { curve: [...curve.slice(0, insertAt), makeAnchor(x, y, 15), ...curve.slice(insertAt)], idx: insertAt, injected: true };
282
- }
283
-
284
- function removeLockedAnchor(curve: CurveAnchor[], idx: number | null): CurveAnchor[] {
285
- if (idx === null || idx === 0 || idx === curve.length - 1) return curve;
286
- return curve.filter((_, i) => i !== idx);
287
- }
288
-
289
- $: if (anchorToBase !== lastAnchorToBase) {
290
- lastAnchorToBase = anchorToBase;
291
- if (anchorToBase) {
292
- const x500 = stepIndexToX(4);
293
- const lResult = injectLockedAnchor(lightnessCurve, x500, hexToOklch(baseColor).l * 100);
294
- if (lResult.curve !== lightnessCurve) lightnessCurve = lResult.curve;
295
- lockedLightnessIdx = lResult.idx;
296
- injectedLightness = lResult.injected;
297
- const sResult = injectLockedAnchor(saturationCurve, x500, 100);
298
- if (sResult.curve !== saturationCurve) saturationCurve = sResult.curve;
299
- lockedSaturationIdx = sResult.idx;
300
- injectedSaturation = sResult.injected;
301
- } else {
302
- if (injectedLightness) lightnessCurve = removeLockedAnchor(lightnessCurve, lockedLightnessIdx);
303
- if (injectedSaturation) saturationCurve = removeLockedAnchor(saturationCurve, lockedSaturationIdx);
304
- lockedLightnessIdx = null;
305
- lockedSaturationIdx = null;
306
- injectedLightness = false;
307
- injectedSaturation = false;
308
- }
309
- }
310
-
311
- // Keep locked lightness anchor y in sync with base color
312
- $: if (anchorToBase && lockedLightnessIdx !== null && baseColor) {
313
- const targetY = hexToOklch(baseColor).l * 100;
314
- if (lightnessCurve[lockedLightnessIdx] && Math.abs(lightnessCurve[lockedLightnessIdx].y - targetY) > 0.01) {
315
- lightnessCurve = lightnessCurve.map((a, i) => i === lockedLightnessIdx ? { ...a, y: targetY } : a);
316
- }
317
- }
318
-
319
- function computePaletteColor(index: number, base: string): string {
320
- const { c: baseC, h } = hexToOklch(base);
321
- const xPos = stepIndexToX(index);
322
-
323
- const targetL = Math.max(0, Math.min(100, sampleCurve(lightnessCurve, xPos) + (curveOffset['lightness'] ?? 0))) / 100;
324
- const satMul = Math.max(0, Math.min(2, (sampleCurve(saturationCurve, xPos) + (curveOffset['saturation'] ?? 0)) / 100));
325
- const targetC = baseC * satMul;
326
-
327
- const clamped = gamutClamp(targetL, targetC, h);
328
- return oklchToHex(clamped.l, clamped.c, clamped.h);
329
- }
330
-
331
- $: paletteComputed = (() => {
332
- const _lc = lightnessCurve, _sc = saturationCurve, _co = curveOffset, _ed = editingDraft, _ek = editingKey, _ov = overrides, _ab = anchorToBase;
333
- return paletteStepLightness.map((ps, index) => {
334
- const k = paletteStepKey(ps.label);
335
- const hex = computePaletteColor(index, baseColor);
336
- const effective = (_ek === k && _ed !== null) ? _ed : (k in _ov) ? _ov[k] : hex;
337
- return {
338
- label: ps.label,
339
- lightness: ps.lightness,
340
- index,
341
- key: k,
342
- hex,
343
- effective,
344
- };
345
- });
346
- })();
347
-
348
- // Gradient reactives — must follow paletteComputed
349
- $: {
350
- const pc = paletteComputed;
351
- const sorted = [...gradientStops].sort((a, b) => gradientReverse ? b.position - a.position : a.position - b.position);
352
- gradientColorStops = sorted.map(s => `${stopColor(s, pc)} ${s.position}%`).join(', ');
353
- gradientBarPreview = `linear-gradient(to right, ${gradientColorStops})`;
354
- if (emptySelector && emptyMode === 'gradient') {
355
- switch (gradientStyle) {
356
- case 'radial': gradientCssValue = `radial-gradient(circle, ${gradientColorStops})`; break;
357
- case 'conic': gradientCssValue = `conic-gradient(from ${gradientAngle}deg, ${gradientColorStops})`; break;
358
- default: gradientCssValue = `linear-gradient(${gradientAngle}deg, ${gradientColorStops})`;
359
- }
360
- } else {
361
- gradientCssValue = '';
362
- }
363
- }
364
-
365
- function handlePaletteClick(ps: { label: string; lightness: number; index: number }) {
366
- const k = paletteStepKey(ps.label);
367
- if (editingKey === k) {
368
- confirmEdit();
369
- return;
370
- }
371
- const current = (k in overrides) ? overrides[k] : computePaletteColor(ps.index, baseColor);
372
- editingDraft = current;
373
- editingSnapshot = current;
374
- editingKey = k;
375
- }
376
-
377
- // --- Scale types ---
378
-
379
- interface Step {
380
- name: string;
381
- position: number;
382
- lightness?: number;
383
- saturation?: number;
384
- }
385
-
386
- interface Scale {
387
- title: string;
388
- isText: boolean;
389
- steps: Step[];
390
- }
391
-
392
- const scales: Scale[] = [
393
- {
394
- title: 'Surfaces',
395
- isText: false,
396
- steps: [
397
- { name: 'lowest', position: -1 },
398
- { name: 'lower', position: -2/3 },
399
- { name: 'low', position: -1/3 },
400
- { name: 'default', position: 0 },
401
- { name: 'high', position: 1/3 },
402
- { name: 'higher', position: 2/3 },
403
- { name: 'highest', position: 1 },
404
- ]
405
- },
406
- {
407
- title: 'Borders',
408
- isText: false,
409
- steps: [
410
- { name: 'faint', position: -1 },
411
- { name: 'subtle', position: -0.5 },
412
- { name: 'default', position: 0 },
413
- { name: 'medium', position: 0.5 },
414
- { name: 'strong', position: 1 },
415
- ]
416
- },
417
- {
418
- title: 'Text',
419
- isText: true,
420
- steps: [
421
- { name: 'primary', position: 0 },
422
- { name: 'secondary', position: 0 },
423
- { name: 'tertiary', position: 0 },
424
- { name: 'muted', position: 0 },
425
- { name: 'disabled', position: 0 },
426
- ]
427
- }
428
- ];
429
-
430
- // Scales to render in gray mode (varies by namespace)
431
- $: grayScales = mode === 'gray' ? scales.filter(scale => {
432
- if (scale.title === 'Surfaces') return true;
433
- if (scale.title === 'Borders') return true;
434
- if (scale.title === 'Text') return true;
435
- return false;
436
- }) : [];
437
-
438
- // --- Per-scale curve state (Surfaces & Borders) ---
439
-
440
- const defaultScaleCurves: Record<string, { lightness: () => CurveAnchor[]; saturation: () => CurveAnchor[] }> = {
441
- Surfaces: {
442
- lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
443
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)],
444
- },
445
- Borders: {
446
- lightness: () => [makeAnchor(0, 25, 5), makeAnchor(100, 80, 5)],
447
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)],
448
- },
449
- Text: {
450
- lightness: () => [makeAnchor(0, 120, 30), makeAnchor(100, 55, 30)],
451
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 15, 30)],
452
- },
453
- };
454
-
455
- let scaleCurves: Record<string, { lightness: CurveAnchor[]; saturation: CurveAnchor[] }> = {
456
- Surfaces: { lightness: defaultScaleCurves.Surfaces.lightness(), saturation: defaultScaleCurves.Surfaces.saturation() },
457
- Borders: { lightness: defaultScaleCurves.Borders.lightness(), saturation: defaultScaleCurves.Borders.saturation() },
458
- Text: { lightness: defaultScaleCurves.Text.lightness(), saturation: defaultScaleCurves.Text.saturation() },
459
- };
460
-
461
- let scaleEditorOpen: Record<string, boolean> = { Surfaces: false, Borders: false, Text: false };
462
-
463
- function setScaleCurve(title: string, channel: 'lightness' | 'saturation', a: CurveAnchor[]) {
464
- scaleCurves[title] = { ...scaleCurves[title], [channel]: a };
465
- scaleCurves = scaleCurves;
466
- }
467
-
468
- function getScaleCurveKey(scaleTitle: string, channel: 'lightness' | 'saturation'): string {
469
- return `${scaleTitle}-${channel}`;
470
- }
471
-
472
- interface ScaleConfig {
473
- lightnessLow: number;
474
- lightnessHigh: number;
475
- saturation: number;
476
- }
477
-
478
- function configForScale(title: string): ScaleConfig {
479
- return { lightnessLow: 0, lightnessHigh: 100, saturation: 100 };
480
- }
481
-
482
- let overrides: Record<string, string> = {};
483
- let editingKey: string | null = null;
484
- let editingSnapshot: string | null = null;
485
- let editingDraft: string | null = null;
486
-
487
- function stepKey(scaleTitle: string, stepName: string): string {
488
- return `${scaleTitle}-${stepName}`;
489
- }
490
-
491
- $: curveVersion = JSON.stringify(scaleCurves) + JSON.stringify(curveOffset) + gray500Hex;
492
-
493
- function derivedHex(step: Step, base: string, scaleTitle: string, _version?: string): string {
494
- return computeDerivedColor(step, base, configForScale(scaleTitle), scaleTitle);
495
- }
496
-
497
- // --- Reactive editing state ---
498
-
499
- const BASE_KEY = '__base__';
500
-
501
- $: isEditingBase = editingKey === BASE_KEY;
502
-
503
- $: editingColor = isEditingBase
504
- ? (mode === 'gray' ? gray500Hex : baseColor)
505
- : editingDraft;
506
-
507
- $: editingStepInfo = (() => {
508
- if (!editingKey || isEditingBase) return null;
509
- if (mode === 'gray') {
510
- const gs = graySteps.find(s => grayStepKey(s.label) === editingKey);
511
- if (gs) return { scale: 'Gray', step: gs.label };
512
- }
513
- const ps = paletteStepLightness.find(p => paletteStepKey(p.label) === editingKey);
514
- if (ps) return { scale: 'Palette', step: ps.label };
515
- for (const scale of scales) {
516
- for (const step of scale.steps) {
517
- if (stepKey(scale.title, step.name) === editingKey) {
518
- return { scale: scale.title, step: step.name };
519
- }
520
- }
521
- }
522
- return null;
523
- })();
524
-
525
- $: panelOpen = editingKey !== null && (isEditingBase || (editingDraft !== null && editingStepInfo !== null));
526
-
527
- $: editPanelTitle = isEditingBase
528
- ? 'Base Color'
529
- : editingStepInfo
530
- ? `${editingStepInfo.scale} \u203A ${editingStepInfo.step}`
531
- : null;
532
-
533
- // --- Compute derived color via OKLCH ---
534
-
535
- function scaleStepToX(step: Step, scale: Scale): number {
536
- const idx = scale.steps.indexOf(step);
537
- return scale.steps.length > 1 ? (idx / (scale.steps.length - 1)) * 100 : 50;
538
- }
539
-
540
- function computeDerivedColor(step: Step, base: string, config: ScaleConfig, scaleTitle: string): string {
541
- const { l: baseL, c: baseC, h: baseH } = hexToOklch(base);
542
- const scale = scales.find(s => s.title === scaleTitle)!;
543
- const xPos = scaleStepToX(step, scale);
544
-
545
- const lCurve = scaleCurves[scaleTitle]?.lightness ?? [];
546
- const sCurve = scaleCurves[scaleTitle]?.saturation ?? [];
547
- const lKey = getScaleCurveKey(scaleTitle, 'lightness');
548
- const sKey = getScaleCurveKey(scaleTitle, 'saturation');
549
- const lOff = curveOffset[lKey] ?? 0;
550
- const sOff = curveOffset[sKey] ?? 0;
551
-
552
- let targetL: number;
553
- if (scale.isText) {
554
- // Text: lightness curve is a multiplier (100 = 1x base lightness)
555
- const lMul = Math.max(0, Math.min(2, (sampleCurve(lCurve, xPos) + lOff) / 100));
556
- targetL = Math.max(0, Math.min(1, baseL * lMul));
557
- } else {
558
- targetL = Math.max(0, Math.min(100, sampleCurve(lCurve, xPos) + lOff)) / 100;
559
- }
560
-
561
- const satMul = Math.max(0, Math.min(2, (sampleCurve(sCurve, xPos) + sOff) / 100));
562
- const targetC = baseC * satMul;
563
-
564
- const clamped = gamutClamp(targetL, targetC, baseH);
565
- return oklchToHex(clamped.l, clamped.c, clamped.h);
566
- }
567
-
568
- // --- Interaction handlers ---
569
-
570
- function handleColorChange(hex: string) {
571
- if (isEditingBase) {
572
- baseColor = hex;
573
- } else if (editingKey) {
574
- editingDraft = hex;
575
- if (editingKey in overrides) {
576
- overrides[editingKey] = hex;
577
- overrides = overrides;
578
- }
579
- }
580
- }
581
-
582
- function resetOverride(k: string) {
583
- if (!(k in overrides)) return;
584
- const { [k]: _, ...rest } = overrides;
585
- overrides = rest;
586
- }
587
-
588
- function handleOverrideClick(k: string, step: Step, scaleTitle: string) {
589
- if (editingKey === k) {
590
- confirmEdit();
591
- return;
592
- }
593
- const current = (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, configForScale(scaleTitle), scaleTitle);
594
- editingDraft = current;
595
- editingSnapshot = current;
596
- editingKey = k;
597
- }
598
-
599
- function handleGrayClick(gStep: GrayStep, index: number) {
600
- const k = grayStepKey(gStep.label);
601
- if (editingKey === k) {
602
- confirmEdit();
603
- return;
604
- }
605
- const current = (k in overrides) ? overrides[k] : computeGrayColor(index, tintHue, tintChroma);
606
- editingDraft = current;
607
- editingSnapshot = current;
608
- editingKey = k;
609
- }
610
-
611
- function confirmEdit() {
612
- if (editingKey) {
613
- // Promote draft to override only if it differs from computed
614
- if (editingDraft !== null) {
615
- const computed = computedValueForKey(editingKey);
616
- if (computed !== null && editingDraft !== computed) {
617
- overrides = { ...overrides, [editingKey]: editingDraft };
618
- } else if (computed !== null && editingKey in overrides && editingDraft === computed) {
619
- // Was an override but now matches computed — remove it
620
- const { [editingKey]: _, ...rest } = overrides;
621
- overrides = rest;
622
- }
623
- }
624
-
625
- for (const scale of scales.filter(s => !s.isText)) {
626
- if (snappedScales.has(scale.title) && scale.steps.some(s => stepKey(scale.title, s.name) === editingKey)) {
627
- const assigned = snapScaleToPalette(scale);
628
- overrides = { ...overrides, ...assigned };
629
- break;
630
- }
631
- }
632
- }
633
- editingKey = null;
634
- editingSnapshot = null;
635
- editingDraft = null;
636
- snapshotTintHue = null;
637
- snapshotTintChroma = null;
638
- }
639
-
640
- function cancelEdit() {
641
- if (editingSnapshot !== null && editingKey) {
642
- if (isEditingBase) {
643
- if (mode === 'gray' && snapshotTintHue !== null && snapshotTintChroma !== null) {
644
- tintHue = snapshotTintHue;
645
- tintChroma = snapshotTintChroma;
646
- } else {
647
- baseColor = editingSnapshot;
648
- }
649
- } else if (editingKey in overrides) {
650
- overrides[editingKey] = editingSnapshot;
651
- overrides = overrides;
652
- }
653
- }
654
- editingKey = null;
655
- editingSnapshot = null;
656
- editingDraft = null;
657
- snapshotTintHue = null;
658
- snapshotTintChroma = null;
659
- }
660
-
661
- function removeOverride(k: string) {
662
- const { [k]: _, ...rest } = overrides;
663
- overrides = rest;
664
- if (editingKey === k) { editingKey = null; editingDraft = null; }
665
- editingSnapshot = null;
666
- }
667
-
668
- function computedValueForKey(key: string): string | null {
669
- const ps = paletteStepLightness.find(p => paletteStepKey(p.label) === key);
670
- if (ps) {
671
- const idx = paletteStepLightness.indexOf(ps);
672
- return computePaletteColor(idx, baseColor);
673
- }
674
- const gi = graySteps.findIndex(g => grayStepKey(g.label) === key);
675
- if (gi >= 0) return computeGrayColor(gi, tintHue, tintChroma);
676
- for (const scale of scales) {
677
- for (const step of scale.steps) {
678
- if (stepKey(scale.title, step.name) === key) {
679
- return computeDerivedColor(step, gray500Hex, configForScale(scale.title), scale.title);
680
- }
681
- }
682
- }
683
- return null;
684
- }
685
-
686
- function effectiveColor(k: string, step: Step, scaleTitle: string, _version?: string): string {
687
- if (editingKey === k && editingDraft !== null) return editingDraft;
688
- return (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, configForScale(scaleTitle), scaleTitle);
689
- }
690
-
691
- // --- Brightness/Saturation gradient helpers for scale editor ---
692
-
693
- function lightnessGrad(base: string): string {
694
- const { c, h } = hexToOklch(base);
695
- const points: string[] = [];
696
- for (let i = 0; i <= 8; i++) {
697
- const l = i / 8;
698
- const clamped = gamutClamp(l, c, h);
699
- points.push(`${oklchToHex(clamped.l, clamped.c, clamped.h)} ${Math.round((i / 8) * 100)}%`);
700
- }
701
- return `linear-gradient(to right, ${points.join(', ')})`;
702
- }
703
-
704
- function saturationGrad(base: string): string {
705
- const { l, c, h } = hexToOklch(base);
706
- const midL = Math.max(0.3, Math.min(0.7, l));
707
- const points: string[] = [];
708
- for (let i = 0; i <= 8; i++) {
709
- const scale = (i / 8) * 2;
710
- const targetC = c * scale;
711
- const clamped = gamutClamp(midL, targetC, h);
712
- points.push(`${oklchToHex(clamped.l, clamped.c, clamped.h)} ${Math.round((i / 8) * 100)}%`);
713
- }
714
- return `linear-gradient(to right, ${points.join(', ')})`;
715
- }
716
-
717
- let copiedKey: string | null = null;
718
- function copyHex(k: string, hex: string) {
719
- navigator.clipboard.writeText(hex);
720
- copiedKey = k;
721
- setTimeout(() => { copiedKey = null; }, 1000);
722
- }
723
-
724
- let copiedLabelKey: string | null = null;
725
- function copyVarName(k: string, varName: string) {
726
- navigator.clipboard.writeText(varName);
727
- copiedLabelKey = k;
728
- setTimeout(() => { copiedLabelKey = null; }, 1000);
729
- }
730
-
731
- // --- Snap-all: constrain an entire scale to unique palette steps ---
732
-
733
- let snappedScales: Set<string> = new Set();
734
-
735
- function snapScaleToPalette(scale: Scale): Record<string, string> {
736
- const cfg = configForScale(scale.title);
737
- const n = scale.steps.length;
738
-
739
- const stepL = scale.steps.map(step => {
740
- const derived = computeDerivedColor(step, baseColor, cfg, scale.title);
741
- return hexToOklch(derived).l;
742
- });
743
-
744
- const palL = paletteComputed.map(ps => hexToOklch(ps.hex).l);
745
-
746
- const palDarkFirst = [...paletteComputed].reverse();
747
- const palLDarkFirst = [...palL].reverse();
748
-
749
- let bestStart = 0;
750
- let bestCost = Infinity;
751
- for (let start = 0; start <= palDarkFirst.length - n; start++) {
752
- let cost = 0;
753
- for (let i = 0; i < n; i++) {
754
- const d = stepL[i] - palLDarkFirst[start + i];
755
- cost += d * d;
756
- }
757
- if (cost < bestCost) {
758
- bestCost = cost;
759
- bestStart = start;
760
- }
761
- }
762
-
763
- const assigned: Record<string, string> = {};
764
- for (let i = 0; i < n; i++) {
765
- const k = stepKey(scale.title, scale.steps[i].name);
766
- assigned[k] = palDarkFirst[bestStart + i].hex;
767
- }
768
- return assigned;
769
- }
770
-
771
- function toggleSnapAll(scale: Scale) {
772
- if (snappedScales.has(scale.title)) {
773
- snappedScales.delete(scale.title);
774
- snappedScales = snappedScales;
775
- snapPickerKey = null;
776
- const next = { ...overrides };
777
- for (const step of scale.steps) {
778
- delete next[stepKey(scale.title, step.name)];
779
- }
780
- overrides = next;
781
- if (editingKey && scale.steps.some(s => stepKey(scale.title, s.name) === editingKey)) {
782
- editingKey = null;
783
- editingSnapshot = null;
784
- }
785
- } else {
786
- snappedScales.add(scale.title);
787
- snappedScales = snappedScales;
788
- const assigned = snapScaleToPalette(scale);
789
- overrides = { ...overrides, ...assigned };
790
- }
791
- }
792
-
793
- function clearPaletteOverrides() {
794
- const next = { ...overrides };
795
- if (mode === 'gray') {
796
- for (const step of graySteps) delete next[grayStepKey(step.label)];
797
- } else {
798
- for (const ps of paletteStepLightness) delete next[paletteStepKey(ps.label)];
799
- }
800
- overrides = next;
801
- }
802
-
803
- function scaleHasOverrides(scale: Scale): boolean {
804
- return scale.steps.some(s => stepKey(scale.title, s.name) in overrides);
805
- }
806
-
807
- function clearScaleOverrides(scale: Scale) {
808
- // Also unsnap the scale so resnapScales() won't re-add overrides
809
- snappedScales.delete(scale.title);
810
- snappedScales = snappedScales;
811
- snapPickerKey = null;
812
- const next = { ...overrides };
813
- for (const step of scale.steps) {
814
- delete next[stepKey(scale.title, step.name)];
815
- }
816
- overrides = next;
817
- if (editingKey && scale.steps.some(s => stepKey(scale.title, s.name) === editingKey)) {
818
- editingKey = null;
819
- editingSnapshot = null;
820
- }
821
- }
822
-
823
- let snapPickerKey: string | null = null;
824
-
825
- function handleDocClick(e: MouseEvent) {
826
- if (!snapPickerKey) return;
827
- const target = e.target as HTMLElement;
828
- if (target.closest('.override-slot-wrapper')) return;
829
- snapPickerKey = null;
830
- }
831
- onMount(() => document.addEventListener('click', handleDocClick, true));
832
- onDestroy(() => document.removeEventListener('click', handleDocClick, true));
833
-
834
- function selectSnapValue(k: string, paletteHex: string, scaleTitle: string) {
835
- overrides = { ...overrides, [k]: paletteHex };
836
- snapPickerKey = null;
837
- }
838
-
839
- function handleSnappedClick(k: string) {
840
- if (snapPickerKey === k) {
841
- snapPickerKey = null;
842
- } else {
843
- snapPickerKey = k;
844
- }
845
- }
846
-
847
- function resnapScales() {
848
- if (snappedScales.size === 0) return;
849
- let changed = false;
850
- const next = { ...overrides };
851
- for (const scale of scales.filter(s => !s.isText)) {
852
- if (!snappedScales.has(scale.title)) continue;
853
- const assigned = snapScaleToPalette(scale);
854
- for (const [k, hex] of Object.entries(assigned)) {
855
- if (next[k] !== hex) {
856
- next[k] = hex;
857
- changed = true;
858
- }
859
- }
860
- }
861
- if (changed) overrides = next;
862
- }
863
-
864
- $: baseColor, scaleCurves, lightnessCurve, saturationCurve, curveOffset, snappedScales, resnapScales();
865
-
866
- // --- Live CSS variable output ---
867
-
868
- function scaleToCssVar(scaleTitle: string, stepName: string): string | null {
869
- if (cssNamespace === null) return null;
870
- if (scaleTitle === 'Surfaces') {
871
- const suffix = stepName === 'default' ? '' : `-${stepName}`;
872
- if (cssNamespace === 'neutral') return `--surface-neutral${suffix}`;
873
- return cssNamespace ? `--surface-${cssNamespace}${suffix}` : `--surface-neutral${suffix}`;
874
- }
875
- if (scaleTitle === 'Borders') {
876
- const suffix = stepName === 'default' ? '' : `-${stepName}`;
877
- if (cssNamespace === 'neutral') return `--border-neutral${suffix}`;
878
- return cssNamespace ? `--border-${cssNamespace}${suffix}` : `--border-neutral${suffix}`;
879
- }
880
- if (scaleTitle === 'Text') {
881
- if (!cssNamespace || cssNamespace === 'neutral') return `--text-${stepName}`;
882
- if (cssNamespace === 'primary' && stepName === 'primary') return '--text-primary-color';
883
- return stepName === 'primary' ? `--text-${cssNamespace}` : `--text-${cssNamespace}-${stepName}`;
884
- }
885
- return null;
886
- }
887
-
888
- let appliedCssVars: string[] = [];
889
-
890
- function setCssVar(name: string, value: string) {
891
- setCssVarSync(name, value);
892
- if (!appliedCssVars.includes(name)) appliedCssVars.push(name);
893
- }
894
-
895
- // Chromatic mode: set --color-{namespace}-* palette ramp + semantic surface/border/text CSS variables
896
- $: if (cssNamespace !== null && mode === 'chromatic') {
897
- const _cv = curveVersion;
898
- const _ov = overrides;
899
- const _bc = baseColor;
900
- // Palette color ramp (100–950)
901
- const _pc = paletteComputed;
902
- for (const ps of _pc) {
903
- setCssVar(`--color-${cssNamespace}-${ps.label}`, ps.effective);
904
- }
905
- // Semantic scales (surfaces, borders, text)
906
- for (const scale of scales) {
907
- for (const step of scale.steps) {
908
- const k = stepKey(scale.title, step.name);
909
- const hex = (k in _ov) ? _ov[k] : computeDerivedColor(step, _bc, configForScale(scale.title), scale.title);
910
- const varName = scaleToCssVar(scale.title, step.name);
911
- if (varName) setCssVar(varName, hex);
912
- }
913
- }
914
- }
915
-
916
- // Gray mode: set --color-{namespace}-* variables + semantic scales (surfaces, borders, text)
917
- $: if (cssNamespace !== null && mode === 'gray') {
918
- const _ge = grayEffective;
919
- const _cv = curveVersion;
920
- const _ov = overrides;
921
- const _gs = grayScales;
922
- const _base = gray500Hex;
923
- for (const g of _ge) {
924
- setCssVar(`--color-${cssNamespace}-${g.step.label}`, g.effective);
925
- }
926
- for (const scale of _gs) {
927
- for (const step of scale.steps) {
928
- const k = stepKey(scale.title, step.name);
929
- const hex = (k in _ov) ? _ov[k] : computeDerivedColor(step, _base, configForScale(scale.title), scale.title);
930
- const varName = scaleToCssVar(scale.title, step.name);
931
- if (varName) setCssVar(varName, hex);
932
- }
933
- }
934
- }
935
-
936
- // Empty selector: set --empty to the selected palette step color or gradient
937
- $: if (emptySelector) {
938
- if (emptyMode === 'solid') {
939
- const _pc = paletteComputed;
940
- const selected = _pc.find(ps => ps.label === emptyStep);
941
- if (selected) {
942
- setCssVar('--empty', selected.effective);
943
- }
944
- setCssVar('--empty-attachment', 'scroll');
945
- } else {
946
- const _grad = gradientCssValue;
947
- if (_grad) {
948
- setCssVar('--empty', _grad);
949
- }
950
- setCssVar('--empty-attachment', gradientSize === 'window' ? 'fixed' : 'scroll');
951
- }
952
- }
953
-
954
- // --- Save / Load configuration ---
955
-
956
- function buildConfig(): PaletteConfig {
957
- return {
958
- baseColor,
959
- tintHue,
960
- tintChroma,
961
- lightnessCurve,
962
- saturationCurve,
963
- grayLightnessCurve,
964
- graySaturationCurve,
965
- scaleCurves,
966
- curveOffset,
967
- overrides,
968
- snappedScales: [...snappedScales],
969
- anchorToBase,
970
- ...(emptySelector ? { emptyMode, emptyStep, gradientStyle, gradientAngle, gradientReverse, gradientStops, gradientSize } : {}),
971
- };
972
- }
973
-
974
- function saveConfig() {
975
- const config = buildConfig();
976
- localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
977
- // Also push to the shared store so save/load flows can collect all configs
978
- editorConfigs.update(m => ({ ...m, [label]: config }));
979
- }
980
-
981
- function applyConfig(config: PaletteConfig) {
982
- // Clear any in-progress editing
983
- editingKey = null;
984
- editingSnapshot = null;
985
- editingDraft = null;
986
-
987
- baseColor = config.baseColor;
988
- tintHue = config.tintHue;
989
- tintChroma = config.tintChroma ?? DEFAULT_TINT_CHROMA;
990
- lightnessCurve = config.lightnessCurve;
991
- saturationCurve = config.saturationCurve;
992
- grayLightnessCurve = config.grayLightnessCurve;
993
- graySaturationCurve = config.graySaturationCurve;
994
- scaleCurves = config.scaleCurves;
995
- curveOffset = config.curveOffset;
996
- overrides = config.overrides;
997
- snappedScales = new Set(config.snappedScales);
998
- lastAnchorToBase = false; // reset so reactive re-fires
999
- anchorToBase = config.anchorToBase ?? true;
1000
- if (config.emptyMode) emptyMode = config.emptyMode;
1001
- if (config.emptyStep) emptyStep = config.emptyStep;
1002
- if (config.gradientStyle) gradientStyle = config.gradientStyle;
1003
- if (config.gradientAngle !== undefined) gradientAngle = config.gradientAngle;
1004
- if (config.gradientReverse !== undefined) gradientReverse = config.gradientReverse;
1005
- if (config.gradientStops) gradientStops = config.gradientStops;
1006
- if (config.gradientSize) gradientSize = config.gradientSize;
1007
- }
1008
-
1009
- function loadFromLocalStorage(): boolean {
1010
- const raw = localStorage.getItem(STORAGE_KEY);
1011
- if (!raw) return false;
1012
- try {
1013
- applyConfig(JSON.parse(raw));
1014
- return true;
1015
- } catch {
1016
- return false;
1017
- }
1018
- }
1019
-
1020
- /**
1021
- * Fetch variables.css from disk (cache-busted), parse :root variables,
1022
- * and set the base color to match the CSS 500 step. Curves generate
1023
- * the full palette and derived scales — no overrides are created.
1024
- */
1025
- // Called directly by VariablesTab when a token file is loaded.
1026
- export function loadConfig(config: PaletteConfig) {
1027
- applyConfig(config);
1028
- localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
1029
- editorConfigs.update(m => ({ ...m, [label]: config }));
1030
- }
1031
-
1032
- // Initial load from token file on mount (set by tokenInit before mount)
1033
- $: if ($loadedConfigs && $loadedConfigs[label]) {
1034
- loadConfig($loadedConfigs[label]);
1035
- }
1036
-
1037
- let initialized = false;
1038
- let mountComplete = false;
1039
-
1040
- onMount(async () => {
1041
- loadFromLocalStorage();
1042
- // Push initial config to the shared store
1043
- editorConfigs.update(m => ({ ...m, [label]: buildConfig() }));
1044
- initialized = true;
1045
- // Wait for the initial reactive flush to complete before marking mount done.
1046
- // Any auto-persist run after this point is from a deliberate user edit.
1047
- await tick();
1048
- mountComplete = true;
1049
- });
1050
-
1051
- // Auto-persist to localStorage so state survives HMR / remounts
1052
- $: if (initialized) {
1053
- // Explicit dep references for Svelte reactivity tracking
1054
- const _deps = [baseColor, tintHue, tintChroma, lightnessCurve, saturationCurve,
1055
- grayLightnessCurve, graySaturationCurve, scaleCurves,
1056
- curveOffset, overrides, snappedScales,
1057
- emptyMode, emptyStep, gradientStyle, gradientAngle, gradientReverse, gradientStops, gradientSize, anchorToBase];
1058
- const config = buildConfig();
1059
- localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
1060
- editorConfigs.update(m => ({ ...m, [label]: config }));
1061
- // After mount completes, any reactive run means the user deliberately edited
1062
- if (mountComplete) {
1063
- configsLoadedFromFile.set(true);
1064
- }
1065
- }
1066
-
1067
- $: if (saveSignal > 0) {
1068
- saveConfig();
1069
- }
1070
- </script>
1071
-
1072
- <div class="palette-editor" style="--editor-base: {mode === 'gray' ? gray500Hex : baseColor}">
1073
- <div class="editor-top">
1074
- <div class="editor-primary">
1075
- {#if mode === 'chromatic'}
1076
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1077
- <div
1078
- class="header-swatch"
1079
- class:active={isEditingBase}
1080
- style="background: {baseColor}"
1081
- on:click={() => { if (editingKey === BASE_KEY) { confirmEdit(); } else { editingSnapshot = baseColor; editingKey = BASE_KEY; } }}
1082
- role="button"
1083
- tabindex="0"
1084
- on:keydown={(e) => e.key === 'Enter' && (editingKey === BASE_KEY ? confirmEdit() : (editingSnapshot = baseColor, editingKey = BASE_KEY))}
1085
- ></div>
1086
- {:else}
1087
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1088
- <div
1089
- class="header-swatch"
1090
- class:active={isEditingBase}
1091
- style="background: {gray500Hex}"
1092
- on:click={() => { if (editingKey === BASE_KEY) { confirmEdit(); } else { editingSnapshot = gray500Hex; snapshotTintHue = tintHue; snapshotTintChroma = tintChroma; editingKey = BASE_KEY; } }}
1093
- role="button"
1094
- tabindex="0"
1095
- on:keydown={(e) => e.key === 'Enter' && (editingKey === BASE_KEY ? confirmEdit() : (editingSnapshot = gray500Hex, snapshotTintHue = tintHue, snapshotTintChroma = tintChroma, editingKey = BASE_KEY))}
1096
- ></div>
1097
- {/if}
1098
- <div class="primary-info">
1099
- <span class="editor-label">{label}</span>
1100
- {#if mode === 'chromatic'}
1101
- <button
1102
- class="base-hex clickable-hex"
1103
- type="button"
1104
- on:click={() => copyHex(BASE_KEY, baseColor)}
1105
- >{copiedKey === BASE_KEY ? 'copied!' : baseColor}</button>
1106
- {:else}
1107
- <button
1108
- class="base-hex clickable-hex"
1109
- type="button"
1110
- on:click={() => copyHex('gray-500', gray500Hex)}
1111
- >{copiedKey === 'gray-500' ? 'copied!' : gray500Hex}</button>
1112
- {/if}
1113
- </div>
1114
- </div>
1115
- </div>
1116
-
1117
- {#if isEditingBase && panelOpen && editingColor}
1118
- <ColorEditPanel
1119
- color={editingColor}
1120
- title={editPanelTitle}
1121
- showRemoveOverride={false}
1122
- mode={mode === 'gray' ? 'hue-chroma' : 'hsl'}
1123
- hue={tintHue}
1124
- chroma={tintChroma}
1125
- onHueChromaChange={(h, c) => { tintHue = h; tintChroma = c; }}
1126
- onColorChange={handleColorChange}
1127
- onConfirm={confirmEdit}
1128
- onCancel={cancelEdit}
1129
- onRemoveOverride={() => {}}
1130
- >
1131
- <span slot="actions" class:hidden={mode !== 'chromatic'}>
1132
- <Toggle bind:checked={anchorToBase} label="Lock base color to position 500" />
1133
- </span>
1134
- </ColorEditPanel>
1135
- {/if}
1136
-
1137
- {#if mode === 'chromatic'}
1138
- <!-- Palette + Text row -->
1139
- <div class="scales-row">
1140
- <div class="scale-section">
1141
- <div class="scale-header">
1142
- <h4 class="scale-title">Palette</h4>
1143
- {#if emptySelector}
1144
- <label class="empty-mode-toggle">
1145
- <input
1146
- type="checkbox"
1147
- checked={emptyMode === 'gradient'}
1148
- on:change={(e) => { emptyMode = e.currentTarget.checked ? 'gradient' : 'solid'; }}
1149
- />
1150
- <span>Gradient</span>
1151
- </label>
1152
- {/if}
1153
- <button class="edit-toggle" type="button" on:click={clearPaletteOverrides}>Clear Overrides</button>
1154
- <button
1155
- class="edit-toggle"
1156
- type="button"
1157
- on:click={() => paletteEditorOpen = !paletteEditorOpen}
1158
- >{paletteEditorOpen ? 'Close' : 'Edit'}</button>
1159
- </div>
1160
- <div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}">
1161
- <div class="step-column">
1162
- <button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-white'} type="button" on:click={() => copyVarName('palette-white', `--color-${cssNamespace}-white`)}>
1163
- {copiedLabelKey === 'palette-white' ? 'copied!' : 'white'}
1164
- </button>
1165
- <div class="swatch gray-swatch bookend" style="background: #ffffff"></div>
1166
- </div>
1167
- {#each paletteComputed as ps}
1168
- <div class="step-column">
1169
- <button class="step-label copyable-label" class:copied={copiedLabelKey === ps.key} type="button" on:click={() => copyVarName(ps.key, `--color-${cssNamespace}-${ps.label}`)}>
1170
- {copiedLabelKey === ps.key ? 'copied!' : ps.label}
1171
- </button>
1172
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1173
- <div
1174
- class="swatch gray-swatch"
1175
- class:active={editingKey === ps.key}
1176
- class:overridden={ps.key in overrides}
1177
- style="background: {ps.effective}"
1178
- on:click={() => handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
1179
- role="button"
1180
- tabindex="0"
1181
- on:keydown={(e) => e.key === 'Enter' && handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
1182
- >
1183
- {#if ps.key in overrides}
1184
- <span class="override-dot" title="Palette override"></span>
1185
- {/if}
1186
- {#if emptySelector && emptyMode === 'solid'}
1187
- <input
1188
- type="checkbox"
1189
- class="empty-check"
1190
- checked={emptyStep === ps.label}
1191
- on:click|stopPropagation={() => { emptyStep = ps.label; }}
1192
- on:keydown|stopPropagation
1193
- title="Page background"
1194
- />
1195
- {/if}
1196
- </div>
1197
- <button
1198
- class="step-hex"
1199
- class:copied={copiedKey === ps.key}
1200
- type="button"
1201
- on:click={() => copyHex(ps.key, ps.effective)}
1202
- >{copiedKey === ps.key ? 'copied!' : ps.effective}</button>
1203
- </div>
1204
- {/each}
1205
- <div class="step-column">
1206
- <button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-black'} type="button" on:click={() => copyVarName('palette-black', `--color-${cssNamespace}-black`)}>
1207
- {copiedLabelKey === 'palette-black' ? 'copied!' : 'black'}
1208
- </button>
1209
- <div class="swatch gray-swatch bookend" style="background: #000000"></div>
1210
- </div>
1211
- {#if paletteEditorOpen}
1212
- <div class="curve-grid-span" style="grid-column: 2 / {paletteStepLightness.length + 2}">
1213
- {#each [
1214
- { key: 'lightness', anchors: lightnessCurve, cfg: lightnessCurveConfig, defaults: DEFAULT_PALETTE_LIGHTNESS(), set: setLightnessCurve, lockedIdx: lockedLightnessIdx },
1215
- { key: 'saturation', anchors: saturationCurve, cfg: saturationCurveConfig, defaults: DEFAULT_PALETTE_SATURATION(), set: setSaturationCurve, lockedIdx: lockedSaturationIdx },
1216
- ] as curve (curve.key)}
1217
- <BezierCurveEditor
1218
- anchors={curve.anchors}
1219
- cfg={curve.cfg}
1220
- stepCount={paletteStepLightness.length}
1221
- defaultAnchors={curve.defaults}
1222
- offset={curveOffset[curve.key] ?? 0}
1223
- lockedAnchorIndex={curve.lockedIdx}
1224
- onAnchorsChange={curve.set}
1225
- onOffsetChange={(v) => handleOffset(curve.key, v)}
1226
- />
1227
- {/each}
1228
- </div>
1229
- {/if}
1230
- </div>
1231
-
1232
- {#if emptySelector && emptyMode === 'gradient'}
1233
- <div class="gradient-controls">
1234
- <div class="gradient-row">
1235
- <span class="gradient-label">Style:</span>
1236
- <div class="gradient-style-buttons">
1237
- {#each gradientStyleOptions as opt}
1238
- <button
1239
- class="style-btn"
1240
- class:active={gradientStyle === opt.value}
1241
- type="button"
1242
- title={opt.title}
1243
- on:click={() => { gradientStyle = opt.value; }}
1244
- >{opt.icon}</button>
1245
- {/each}
1246
- </div>
1247
- </div>
1248
-
1249
- <div class="gradient-row">
1250
- <span class="gradient-label">Angle:</span>
1251
- <input
1252
- class="gradient-angle-input"
1253
- type="number"
1254
- min="0"
1255
- max="360"
1256
- bind:value={gradientAngle}
1257
- />
1258
- <span class="gradient-unit">deg</span>
1259
- <input
1260
- class="gradient-angle-slider"
1261
- type="range"
1262
- min="0"
1263
- max="360"
1264
- bind:value={gradientAngle}
1265
- />
1266
- </div>
1267
-
1268
- <div class="gradient-row">
1269
- <span class="gradient-label">Size:</span>
1270
- <div class="gradient-style-buttons">
1271
- {#each gradientSizeOptions as opt}
1272
- <button
1273
- class="style-btn size-btn"
1274
- class:active={gradientSize === opt.value}
1275
- type="button"
1276
- title={opt.title}
1277
- on:click={() => { gradientSize = opt.value; }}
1278
- >{opt.label}</button>
1279
- {/each}
1280
- </div>
1281
- </div>
1282
-
1283
- <div class="gradient-row">
1284
- <label class="gradient-checkbox-label">
1285
- <input type="checkbox" bind:checked={gradientReverse} />
1286
- Reverse
1287
- </label>
1288
- </div>
1289
-
1290
- <!-- Gradient stop bar -->
1291
- <div class="gradient-stop-bar-wrapper">
1292
- <div class="gradient-stop-handles">
1293
- {#each gradientStops as stop, i}
1294
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1295
- <div
1296
- class="gradient-stop-handle"
1297
- class:selected={selectedStopIndex === i}
1298
- style="left: {stop.position}%; --stop-color: {stopColor(stop, paletteComputed)}"
1299
- on:mousedown|stopPropagation={(e) => handleStopHandleMouseDown(e, i)}
1300
- role="button"
1301
- tabindex="0"
1302
- on:keydown={(e) => {
1303
- if (e.key === 'Delete' || e.key === 'Backspace') removeGradientStop(i);
1304
- }}
1305
- >
1306
- <div class="stop-swatch" style="background: {stopColor(stop, paletteComputed)}"></div>
1307
- <div class="stop-arrow"></div>
1308
- </div>
1309
- {/each}
1310
- </div>
1311
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1312
- <div
1313
- class="gradient-stop-bar"
1314
- style="background: {gradientBarPreview}"
1315
- on:mousedown={handleStopBarMouseDown}
1316
- role="slider"
1317
- tabindex="0"
1318
- aria-label="Gradient stops"
1319
- aria-valuenow={gradientStops[selectedStopIndex]?.position ?? 0}
1320
- aria-valuemin="0"
1321
- aria-valuemax="100"
1322
- ></div>
1323
- </div>
1324
-
1325
- <!-- Selected stop controls -->
1326
- {#if gradientStops[selectedStopIndex]}
1327
- <div class="gradient-row stop-controls">
1328
- <span class="gradient-label">Color:</span>
1329
- <select
1330
- class="gradient-select"
1331
- bind:value={gradientStops[selectedStopIndex].paletteLabel}
1332
- on:change={() => { gradientStops = gradientStops; }}
1333
- >
1334
- {#each paletteComputed as ps}
1335
- <option value={ps.label}>{ps.label}</option>
1336
- {/each}
1337
- </select>
1338
- <div class="stop-color-preview" style="background: {stopColor(gradientStops[selectedStopIndex], paletteComputed)}"></div>
1339
- <span class="gradient-label">Pos:</span>
1340
- <input
1341
- class="gradient-pos-input"
1342
- type="number"
1343
- min="0"
1344
- max="100"
1345
- bind:value={gradientStops[selectedStopIndex].position}
1346
- on:change={() => { gradientStops = gradientStops; }}
1347
- />
1348
- <span class="gradient-unit">%</span>
1349
- {#if gradientStops.length > 2}
1350
- <button
1351
- class="stop-remove-btn"
1352
- type="button"
1353
- title="Remove stop"
1354
- on:click={() => removeGradientStop(selectedStopIndex)}
1355
- >&times;</button>
1356
- {/if}
1357
- </div>
1358
- {/if}
1359
- </div>
1360
- {/if}
1361
- </div>
1362
-
1363
- </div>
1364
-
1365
- <button class="derived-toggle" type="button" on:click={() => showDerived = !showDerived}>
1366
- <i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
1367
- <span>Text, Surfaces &amp; Borders</span>
1368
- </button>
1369
-
1370
- {#if showDerived}
1371
- <div class="scales-row">
1372
- {#each scales.filter(s => s.isText) as scale}
1373
- {@const textEditorOpen = scaleEditorOpen[scale.title] ?? false}
1374
- <div class="scale-section">
1375
- <div class="scale-header">
1376
- <h4 class="scale-title">{scale.title}</h4>
1377
- <button
1378
- class="edit-toggle"
1379
- class:active={snappedScales.has(scale.title)}
1380
- type="button"
1381
- on:click={() => toggleSnapAll(scale)}
1382
- >{snappedScales.has(scale.title) ? 'Unsnap' : 'Snap All'}</button>
1383
- <button class="edit-toggle" type="button" on:click={() => clearScaleOverrides(scale)}>Clear Overrides</button>
1384
- <button
1385
- class="edit-toggle"
1386
- type="button"
1387
- on:click={() => { scaleEditorOpen[scale.title] = !scaleEditorOpen[scale.title]; scaleEditorOpen = scaleEditorOpen; }}
1388
- >{textEditorOpen ? 'Close' : 'Edit'}</button>
1389
- </div>
1390
- <div class="swatch-grid" style="--swatch-cols: {scale.steps.length}; --swatch-gap: var(--space-8)">
1391
- {#each scale.steps as step}
1392
- {@const k = stepKey(scale.title, step.name)}
1393
- {@const hex = effectiveColor(k, step, scale.title, curveVersion)}
1394
- <div class="step-column">
1395
- <button class="step-label copyable-label" class:copied={copiedLabelKey === k} type="button" on:click={() => { const v = scaleToCssVar(scale.title, step.name); if (v) copyVarName(k, v); }}>
1396
- {copiedLabelKey === k ? 'copied!' : step.name}
1397
- </button>
1398
- <div
1399
- class="swatch derived text-swatch"
1400
- class:dimmed={k in overrides}
1401
- class:clickable={k in overrides}
1402
- on:click={() => resetOverride(k)}
1403
- role={k in overrides ? 'button' : undefined}
1404
- tabindex={k in overrides ? 0 : undefined}
1405
- on:keydown={(e) => k in overrides && e.key === 'Enter' && resetOverride(k)}
1406
- >
1407
- <span style="color: {derivedHex(step, baseColor, scale.title, curveVersion)}">Ag</span>
1408
- </div>
1409
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1410
- <div
1411
- class="swatch override-slot text-swatch"
1412
- class:active={editingKey === k}
1413
- class:populated={k in overrides}
1414
- class:matching={k in overrides && overrides[k] === derivedHex(step, baseColor, scale.title, curveVersion)}
1415
- on:click={() => handleOverrideClick(k, step, scale.title)}
1416
- role="button"
1417
- tabindex="0"
1418
- on:keydown={(e) => e.key === 'Enter' && handleOverrideClick(k, step, scale.title)}
1419
- >
1420
- {#if k in overrides}
1421
- <span style="color: {overrides[k]}">Ag</span>
1422
- {/if}
1423
- </div>
1424
- <button
1425
- class="step-hex"
1426
- class:copied={copiedKey === k}
1427
- type="button"
1428
- on:click={() => copyHex(k, hex)}
1429
- >{copiedKey === k ? 'copied!' : hex}</button>
1430
- </div>
1431
- {/each}
1432
- {#if textEditorOpen}
1433
- <div class="curve-grid-span" style="grid-column: 1 / -1">
1434
- {#each [
1435
- { key: getScaleCurveKey(scale.title, 'lightness'), anchors: scaleCurves[scale.title].lightness, cfg: textLightnessCurveConfig, defaults: defaultScaleCurves[scale.title].lightness(), set: setScaleCurve.bind(null, scale.title, 'lightness') },
1436
- { key: getScaleCurveKey(scale.title, 'saturation'), anchors: scaleCurves[scale.title].saturation, cfg: saturationCurveConfig, defaults: defaultScaleCurves[scale.title].saturation(), set: setScaleCurve.bind(null, scale.title, 'saturation') },
1437
- ] as curve (curve.key)}
1438
- <BezierCurveEditor
1439
- anchors={curve.anchors}
1440
- cfg={curve.cfg}
1441
- stepCount={scale.steps.length}
1442
- defaultAnchors={curve.defaults}
1443
- offset={curveOffset[curve.key] ?? 0}
1444
- onAnchorsChange={curve.set}
1445
- onOffsetChange={(v) => handleOffset(curve.key, v)}
1446
- />
1447
- {/each}
1448
- </div>
1449
- {/if}
1450
- </div>
1451
- </div>
1452
- {/each}
1453
- </div>
1454
-
1455
- <!-- Surfaces & Borders — per-scale editors -->
1456
- <div class="scales-row">
1457
- {#each scales.filter(s => !s.isText) as scale}
1458
- {@const cfg = configForScale(scale.title)}
1459
- {@const editorOpen = scaleEditorOpen[scale.title] ?? false}
1460
- <div class="scale-section">
1461
- <div class="scale-header">
1462
- <h4 class="scale-title">{scale.title}</h4>
1463
- <button
1464
- class="edit-toggle"
1465
- class:active={snappedScales.has(scale.title)}
1466
- type="button"
1467
- on:click={() => toggleSnapAll(scale)}
1468
- >{snappedScales.has(scale.title) ? 'Unsnap' : 'Snap All'}</button>
1469
- <button class="edit-toggle" type="button" on:click={() => clearScaleOverrides(scale)}>Clear Overrides</button>
1470
- <button
1471
- class="edit-toggle"
1472
- type="button"
1473
- on:click={() => { scaleEditorOpen[scale.title] = !scaleEditorOpen[scale.title]; scaleEditorOpen = scaleEditorOpen; }}
1474
- >{editorOpen ? 'Close' : 'Edit'}</button>
1475
- </div>
1476
- <div class="swatch-grid" style="--swatch-cols: {scale.steps.length}; --swatch-gap: var(--space-8)">
1477
- {#each scale.steps as step}
1478
- {@const k = stepKey(scale.title, step.name)}
1479
- {@const hex = effectiveColor(k, step, scale.title, curveVersion)}
1480
- <div class="step-column">
1481
- <button class="step-label copyable-label" class:copied={copiedLabelKey === k} type="button" on:click={() => { const v = scaleToCssVar(scale.title, step.name); if (v) copyVarName(k, v); }}>
1482
- {copiedLabelKey === k ? 'copied!' : step.name}
1483
- </button>
1484
- <div
1485
- class="swatch derived"
1486
- class:border-preview={scale.title === 'Borders'}
1487
- class:dimmed={k in overrides}
1488
- class:clickable={k in overrides}
1489
- style={scale.title === 'Borders'
1490
- ? `border: 3px solid ${derivedHex(step, baseColor, scale.title, curveVersion)}`
1491
- : `background: ${derivedHex(step, baseColor, scale.title, curveVersion)}`}
1492
- on:click={() => resetOverride(k)}
1493
- role={k in overrides ? 'button' : undefined}
1494
- tabindex={k in overrides ? 0 : undefined}
1495
- on:keydown={(e) => k in overrides && e.key === 'Enter' && resetOverride(k)}
1496
- ></div>
1497
- <div class="override-slot-wrapper">
1498
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1499
- <div
1500
- class="swatch override-slot"
1501
- class:border-preview={scale.title === 'Borders'}
1502
- class:active={editingKey === k || snapPickerKey === k}
1503
- class:populated={k in overrides}
1504
- class:matching={k in overrides && overrides[k] === derivedHex(step, baseColor, scale.title, curveVersion)}
1505
- on:click={() => snappedScales.has(scale.title) ? handleSnappedClick(k) : handleOverrideClick(k, step, scale.title)}
1506
- role="button"
1507
- tabindex="0"
1508
- on:keydown={(e) => e.key === 'Enter' && (snappedScales.has(scale.title) ? handleSnappedClick(k) : handleOverrideClick(k, step, scale.title))}
1509
- >
1510
- {#if k in overrides}
1511
- {#if scale.title === 'Borders'}
1512
- <div class="override-fill border-fill" style="border: 3px solid {overrides[k]}"></div>
1513
- {:else}
1514
- <div class="override-fill" style="background: {overrides[k]}"></div>
1515
- {/if}
1516
- {#if snappedScales.has(scale.title)}
1517
- {@const plabel = paletteComputed.find(ps => ps.hex === overrides[k])?.label ?? null}
1518
- {#if plabel}
1519
- <span class="palette-step-label">{plabel}</span>
1520
- {/if}
1521
- {/if}
1522
- {/if}
1523
- </div>
1524
- {#if snapPickerKey === k}
1525
- <div class="snap-picker">
1526
- {#each paletteComputed as ps}
1527
- <button
1528
- class="snap-picker-item"
1529
- class:selected={overrides[k] === ps.hex}
1530
- type="button"
1531
- on:click={() => selectSnapValue(k, ps.hex, scale.title)}
1532
- >
1533
- <span class="snap-picker-swatch" style="background: {ps.hex}"></span>
1534
- <span class="snap-picker-label">{ps.label}</span>
1535
- </button>
1536
- {/each}
1537
- </div>
1538
- {/if}
1539
- </div>
1540
- <button
1541
- class="step-hex"
1542
- class:copied={copiedKey === k}
1543
- type="button"
1544
- on:click={() => copyHex(k, hex)}
1545
- >{copiedKey === k ? 'copied!' : hex}</button>
1546
- </div>
1547
- {/each}
1548
- {#if editorOpen}
1549
- <div class="curve-grid-span" style="grid-column: 1 / -1">
1550
- {#each [
1551
- { key: getScaleCurveKey(scale.title, 'lightness'), anchors: scaleCurves[scale.title].lightness, cfg: lightnessCurveConfig, defaults: defaultScaleCurves[scale.title].lightness(), set: setScaleCurve.bind(null, scale.title, 'lightness') },
1552
- { key: getScaleCurveKey(scale.title, 'saturation'), anchors: scaleCurves[scale.title].saturation, cfg: saturationCurveConfig, defaults: defaultScaleCurves[scale.title].saturation(), set: setScaleCurve.bind(null, scale.title, 'saturation') },
1553
- ] as curve (curve.key)}
1554
- <BezierCurveEditor
1555
- anchors={curve.anchors}
1556
- cfg={curve.cfg}
1557
- stepCount={scale.steps.length}
1558
- defaultAnchors={curve.defaults}
1559
- offset={curveOffset[curve.key] ?? 0}
1560
- onAnchorsChange={curve.set}
1561
- onOffsetChange={(v) => handleOffset(curve.key, v)}
1562
- />
1563
- {/each}
1564
- </div>
1565
- {/if}
1566
- </div>
1567
- </div>
1568
- {/each}
1569
- </div>
1570
- {/if}
1571
-
1572
- {:else}
1573
- <!-- Gray mode: palette + text row -->
1574
- <div class="scales-row">
1575
- <div class="scale-section">
1576
- <div class="scale-header">
1577
- <h4 class="scale-title">{label}</h4>
1578
- <button class="edit-toggle" type="button" on:click={clearPaletteOverrides}>Clear Overrides</button>
1579
- <button
1580
- class="edit-toggle"
1581
- type="button"
1582
- on:click={() => grayEditorOpen = !grayEditorOpen}
1583
- >{grayEditorOpen ? 'Close' : 'Edit'}</button>
1584
- </div>
1585
- <div class="swatch-grid" style="--swatch-cols: {graySteps.length + 2}">
1586
- <div class="step-column">
1587
- <button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-white'} type="button" on:click={() => copyVarName('gray-white', `--color-${cssNamespace}-white`)}>
1588
- {copiedLabelKey === 'gray-white' ? 'copied!' : 'white'}
1589
- </button>
1590
- <div class="swatch gray-swatch bookend" style="background: #ffffff"></div>
1591
- </div>
1592
- {#each grayEffective as g}
1593
- <div class="step-column">
1594
- <button class="step-label copyable-label" class:copied={copiedLabelKey === g.key} type="button" on:click={() => copyVarName(g.key, `--color-${cssNamespace}-${g.step.label}`)}>
1595
- {copiedLabelKey === g.key ? 'copied!' : g.step.label}
1596
- </button>
1597
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1598
- <div
1599
- class="swatch gray-swatch"
1600
- class:active={editingKey === g.key}
1601
- class:overridden={g.key in overrides}
1602
- style="background: {g.effective}"
1603
- on:click={() => handleGrayClick(g.step, g.index)}
1604
- role="button"
1605
- tabindex="0"
1606
- on:keydown={(e) => e.key === 'Enter' && handleGrayClick(g.step, g.index)}
1607
- >
1608
- {#if g.key in overrides}
1609
- <span class="override-dot" title="Palette override"></span>
1610
- {/if}
1611
- </div>
1612
- <button
1613
- class="step-hex"
1614
- class:copied={copiedKey === g.key}
1615
- type="button"
1616
- on:click={() => copyHex(g.key, g.effective)}
1617
- >{copiedKey === g.key ? 'copied!' : g.effective}</button>
1618
- </div>
1619
- {/each}
1620
- <div class="step-column">
1621
- <button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-black'} type="button" on:click={() => copyVarName('gray-black', `--color-${cssNamespace}-black`)}>
1622
- {copiedLabelKey === 'gray-black' ? 'copied!' : 'black'}
1623
- </button>
1624
- <div class="swatch gray-swatch bookend" style="background: #000000"></div>
1625
- </div>
1626
- {#if grayEditorOpen}
1627
- <div class="curve-grid-span" style="grid-column: 2 / {graySteps.length + 2}">
1628
- {#each [
1629
- { key: 'gray-lightness', anchors: grayLightnessCurve, cfg: lightnessCurveConfig, defaults: DEFAULT_GRAY_LIGHTNESS(), set: setGrayLightnessCurve },
1630
- { key: 'gray-saturation', anchors: graySaturationCurve, cfg: saturationCurveConfig, defaults: DEFAULT_GRAY_SATURATION(), set: setGraySaturationCurve },
1631
- ] as curve (curve.key)}
1632
- <BezierCurveEditor
1633
- anchors={curve.anchors}
1634
- cfg={curve.cfg}
1635
- stepCount={graySteps.length}
1636
- defaultAnchors={curve.defaults}
1637
- offset={curveOffset[curve.key] ?? 0}
1638
- onAnchorsChange={curve.set}
1639
- onOffsetChange={(v) => handleOffset(curve.key, v)}
1640
- />
1641
- {/each}
1642
- </div>
1643
- {/if}
1644
- </div>
1645
- </div>
1646
- </div>
1647
-
1648
- <button class="derived-toggle" type="button" on:click={() => showDerived = !showDerived}>
1649
- <i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
1650
- <span>Text, Surfaces &amp; Borders</span>
1651
- </button>
1652
-
1653
- {#if showDerived}
1654
- <div class="scales-row">
1655
- {#each grayScales.filter(s => s.isText) as scale}
1656
- {@const textEditorOpen = scaleEditorOpen[scale.title] ?? false}
1657
- <div class="scale-section">
1658
- <div class="scale-header">
1659
- <h4 class="scale-title">{scale.title}</h4>
1660
- <button
1661
- class="edit-toggle"
1662
- class:active={snappedScales.has(scale.title)}
1663
- type="button"
1664
- on:click={() => toggleSnapAll(scale)}
1665
- >{snappedScales.has(scale.title) ? 'Unsnap' : 'Snap All'}</button>
1666
- <button class="edit-toggle" type="button" on:click={() => clearScaleOverrides(scale)}>Clear Overrides</button>
1667
- <button
1668
- class="edit-toggle"
1669
- type="button"
1670
- on:click={() => { scaleEditorOpen[scale.title] = !scaleEditorOpen[scale.title]; scaleEditorOpen = scaleEditorOpen; }}
1671
- >{textEditorOpen ? 'Close' : 'Edit'}</button>
1672
- </div>
1673
- <div class="swatch-grid" style="--swatch-cols: {scale.steps.length}; --swatch-gap: var(--space-8)">
1674
- {#each scale.steps as step}
1675
- {@const k = stepKey(scale.title, step.name)}
1676
- {@const hex = effectiveColor(k, step, scale.title, curveVersion)}
1677
- <div class="step-column">
1678
- <button class="step-label copyable-label" class:copied={copiedLabelKey === k} type="button" on:click={() => { const v = scaleToCssVar(scale.title, step.name); if (v) copyVarName(k, v); }}>
1679
- {copiedLabelKey === k ? 'copied!' : step.name}
1680
- </button>
1681
- <div
1682
- class="swatch derived text-swatch"
1683
- class:dimmed={k in overrides}
1684
- class:clickable={k in overrides}
1685
- on:click={() => resetOverride(k)}
1686
- role={k in overrides ? 'button' : undefined}
1687
- tabindex={k in overrides ? 0 : undefined}
1688
- on:keydown={(e) => k in overrides && e.key === 'Enter' && resetOverride(k)}
1689
- >
1690
- <span style="color: {derivedHex(step, gray500Hex, scale.title, curveVersion)}">Ag</span>
1691
- </div>
1692
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1693
- <div
1694
- class="swatch override-slot text-swatch"
1695
- class:active={editingKey === k}
1696
- class:populated={k in overrides}
1697
- class:matching={k in overrides && overrides[k] === derivedHex(step, gray500Hex, scale.title, curveVersion)}
1698
- on:click={() => handleOverrideClick(k, step, scale.title)}
1699
- role="button"
1700
- tabindex="0"
1701
- on:keydown={(e) => e.key === 'Enter' && handleOverrideClick(k, step, scale.title)}
1702
- >
1703
- {#if k in overrides}
1704
- <span style="color: {overrides[k]}">Ag</span>
1705
- {/if}
1706
- </div>
1707
- <button
1708
- class="step-hex"
1709
- class:copied={copiedKey === k}
1710
- type="button"
1711
- on:click={() => copyHex(k, hex)}
1712
- >{copiedKey === k ? 'copied!' : hex}</button>
1713
- </div>
1714
- {/each}
1715
- {#if textEditorOpen}
1716
- <div class="curve-grid-span" style="grid-column: 1 / -1">
1717
- {#each [
1718
- { key: getScaleCurveKey(scale.title, 'lightness'), anchors: scaleCurves[scale.title].lightness, cfg: textLightnessCurveConfig, defaults: defaultScaleCurves[scale.title].lightness(), set: setScaleCurve.bind(null, scale.title, 'lightness') },
1719
- { key: getScaleCurveKey(scale.title, 'saturation'), anchors: scaleCurves[scale.title].saturation, cfg: saturationCurveConfig, defaults: defaultScaleCurves[scale.title].saturation(), set: setScaleCurve.bind(null, scale.title, 'saturation') },
1720
- ] as curve (curve.key)}
1721
- <BezierCurveEditor
1722
- anchors={curve.anchors}
1723
- cfg={curve.cfg}
1724
- stepCount={scale.steps.length}
1725
- defaultAnchors={curve.defaults}
1726
- offset={curveOffset[curve.key] ?? 0}
1727
- onAnchorsChange={curve.set}
1728
- onOffsetChange={(v) => handleOffset(curve.key, v)}
1729
- />
1730
- {/each}
1731
- </div>
1732
- {/if}
1733
- </div>
1734
- </div>
1735
- {/each}
1736
- </div>
1737
- <!-- Surfaces & Borders for gray mode -->
1738
- <div class="scales-row">
1739
- {#each grayScales.filter(s => !s.isText) as scale}
1740
- {@const editorOpen = scaleEditorOpen[scale.title] ?? false}
1741
- <div class="scale-section">
1742
- <div class="scale-header">
1743
- <h4 class="scale-title">{scale.title}</h4>
1744
- <button class="edit-toggle" type="button" on:click={() => clearScaleOverrides(scale)}>Clear Overrides</button>
1745
- <button
1746
- class="edit-toggle"
1747
- type="button"
1748
- on:click={() => { scaleEditorOpen[scale.title] = !scaleEditorOpen[scale.title]; scaleEditorOpen = scaleEditorOpen; }}
1749
- >{editorOpen ? 'Close' : 'Edit'}</button>
1750
- </div>
1751
- <div class="swatch-grid" style="--swatch-cols: {scale.steps.length}; --swatch-gap: var(--space-8)">
1752
- {#each scale.steps as step}
1753
- {@const k = stepKey(scale.title, step.name)}
1754
- {@const hex = effectiveColor(k, step, scale.title, curveVersion)}
1755
- <div class="step-column">
1756
- <button class="step-label copyable-label" class:copied={copiedLabelKey === k} type="button" on:click={() => { const v = scaleToCssVar(scale.title, step.name); if (v) copyVarName(k, v); }}>
1757
- {copiedLabelKey === k ? 'copied!' : step.name}
1758
- </button>
1759
- <div
1760
- class="swatch derived"
1761
- class:border-preview={scale.title === 'Borders'}
1762
- class:dimmed={k in overrides}
1763
- class:clickable={k in overrides}
1764
- style={scale.title === 'Borders'
1765
- ? `border: 3px solid ${derivedHex(step, gray500Hex, scale.title, curveVersion)}`
1766
- : `background: ${derivedHex(step, gray500Hex, scale.title, curveVersion)}`}
1767
- on:click={() => resetOverride(k)}
1768
- role={k in overrides ? 'button' : undefined}
1769
- tabindex={k in overrides ? 0 : undefined}
1770
- on:keydown={(e) => k in overrides && e.key === 'Enter' && resetOverride(k)}
1771
- ></div>
1772
- <div class="override-slot-wrapper">
1773
- <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
1774
- <div
1775
- class="swatch override-slot"
1776
- class:border-preview={scale.title === 'Borders'}
1777
- class:active={editingKey === k}
1778
- class:populated={k in overrides}
1779
- class:matching={k in overrides && overrides[k] === derivedHex(step, gray500Hex, scale.title, curveVersion)}
1780
- on:click={() => handleOverrideClick(k, step, scale.title)}
1781
- role="button"
1782
- tabindex="0"
1783
- on:keydown={(e) => e.key === 'Enter' && handleOverrideClick(k, step, scale.title)}
1784
- >
1785
- {#if k in overrides}
1786
- {#if scale.title === 'Borders'}
1787
- <div class="override-fill border-fill" style="border: 3px solid {overrides[k]}"></div>
1788
- {:else}
1789
- <div class="override-fill" style="background: {overrides[k]}"></div>
1790
- {/if}
1791
- {/if}
1792
- </div>
1793
- </div>
1794
- <button
1795
- class="step-hex"
1796
- class:copied={copiedKey === k}
1797
- type="button"
1798
- on:click={() => copyHex(k, hex)}
1799
- >{copiedKey === k ? 'copied!' : hex}</button>
1800
- </div>
1801
- {/each}
1802
- {#if editorOpen}
1803
- <div class="curve-grid-span" style="grid-column: 1 / -1">
1804
- {#each [
1805
- { key: getScaleCurveKey(scale.title, 'lightness'), anchors: scaleCurves[scale.title].lightness, cfg: lightnessCurveConfig, defaults: defaultScaleCurves[scale.title].lightness(), set: setScaleCurve.bind(null, scale.title, 'lightness') },
1806
- { key: getScaleCurveKey(scale.title, 'saturation'), anchors: scaleCurves[scale.title].saturation, cfg: saturationCurveConfig, defaults: defaultScaleCurves[scale.title].saturation(), set: setScaleCurve.bind(null, scale.title, 'saturation') },
1807
- ] as curve (curve.key)}
1808
- <BezierCurveEditor
1809
- anchors={curve.anchors}
1810
- cfg={curve.cfg}
1811
- stepCount={scale.steps.length}
1812
- defaultAnchors={curve.defaults}
1813
- offset={curveOffset[curve.key] ?? 0}
1814
- onAnchorsChange={curve.set}
1815
- onOffsetChange={(v) => handleOffset(curve.key, v)}
1816
- />
1817
- {/each}
1818
- </div>
1819
- {/if}
1820
- </div>
1821
- </div>
1822
- {/each}
1823
- </div>
1824
- {/if}
1825
- {/if}
1826
-
1827
- <!-- Color Edit Panel (non-base edits) -->
1828
- {#if !isEditingBase && panelOpen && editingColor}
1829
- <ColorEditPanel
1830
- color={editingColor}
1831
- title={editPanelTitle}
1832
- showRemoveOverride={!!editingKey}
1833
- mode={'hsl'}
1834
- hue={tintHue}
1835
- chroma={tintChroma}
1836
- onHueChromaChange={(h, c) => { tintHue = h; tintChroma = c; }}
1837
- onColorChange={handleColorChange}
1838
- onConfirm={confirmEdit}
1839
- onCancel={cancelEdit}
1840
- onRemoveOverride={() => editingKey && removeOverride(editingKey)}
1841
- />
1842
- {/if}
1843
- </div>
1844
-
1845
- <style>
1846
- .palette-editor {
1847
- display: flex;
1848
- flex-direction: column;
1849
- gap: var(--space-20);
1850
- padding: var(--space-16) var(--space-16) var(--space-24);
1851
- background: none;
1852
- border: none;
1853
- border-bottom: 1px solid var(--ui-border-faint);
1854
- font-family: var(--ui-font-sans);
1855
- min-width: 0;
1856
- }
1857
-
1858
- .palette-editor:last-child {
1859
- border-bottom: none;
1860
- }
1861
-
1862
- .editor-top {
1863
- display: flex;
1864
- align-items: flex-start;
1865
- gap: var(--space-16);
1866
- flex-wrap: wrap;
1867
- }
1868
-
1869
- .editor-primary {
1870
- display: flex;
1871
- align-items: center;
1872
- gap: var(--space-8);
1873
- flex-shrink: 0;
1874
- }
1875
-
1876
- .primary-info {
1877
- display: flex;
1878
- flex-direction: column;
1879
- gap: var(--space-2);
1880
- }
1881
-
1882
- .header-swatch {
1883
- width: 4rem;
1884
- height: 4rem;
1885
- border-radius: var(--radius-md);
1886
- border: 2px solid var(--ui-border-default);
1887
- flex-shrink: 0;
1888
- cursor: pointer;
1889
- }
1890
-
1891
- .header-swatch:hover {
1892
- border-color: var(--ui-border-strong);
1893
- }
1894
-
1895
- .header-swatch.active {
1896
- border-color: var(--ui-border-strong);
1897
- outline: 2px solid var(--ui-border-medium);
1898
- outline-offset: 1px;
1899
- }
1900
-
1901
-
1902
- .editor-label {
1903
- font-size: var(--font-lg);
1904
- font-weight: var(--font-weight-semibold);
1905
- color: var(--ui-text-primary);
1906
- }
1907
-
1908
- .base-hex {
1909
- font-size: var(--font-xs);
1910
- color: var(--ui-text-secondary);
1911
- font-family: var(--ui-font-mono);
1912
- }
1913
-
1914
- .clickable-hex {
1915
- background: none;
1916
- border: none;
1917
- cursor: pointer;
1918
- padding: var(--space-2) var(--space-4);
1919
- border-radius: var(--radius-sm);
1920
- font-size: var(--font-xs);
1921
- color: var(--ui-text-secondary);
1922
- font-family: var(--ui-font-mono);
1923
- }
1924
-
1925
- .clickable-hex:hover {
1926
- background: var(--ui-surface-highest);
1927
- color: var(--ui-text-primary);
1928
- }
1929
-
1930
- /* Scale header with edit button */
1931
-
1932
- .scale-header {
1933
- display: flex;
1934
- align-items: center;
1935
- gap: var(--space-8);
1936
- }
1937
-
1938
- .edit-toggle {
1939
- font-size: var(--font-md);
1940
- color: var(--ui-text-tertiary);
1941
- background: none;
1942
- border: 1px solid var(--ui-border-subtle);
1943
- border-radius: var(--radius-sm);
1944
- padding: var(--space-2) var(--space-6);
1945
- cursor: pointer;
1946
- }
1947
-
1948
- .edit-toggle:hover {
1949
- color: var(--ui-text-primary);
1950
- border-color: var(--ui-border-medium);
1951
- }
1952
-
1953
- .edit-toggle.active {
1954
- color: var(--ui-text-primary);
1955
- border-color: var(--ui-border-medium);
1956
- background: var(--ui-surface-high);
1957
- }
1958
-
1959
- .hidden {
1960
- display: none;
1961
- }
1962
-
1963
- .derived-toggle {
1964
- display: flex;
1965
- align-items: center;
1966
- gap: var(--space-8);
1967
- padding: var(--space-6) var(--space-4);
1968
- background: none;
1969
- border: none;
1970
- color: var(--ui-text-tertiary);
1971
- font-size: var(--font-sm);
1972
- font-weight: var(--font-weight-semibold);
1973
- cursor: pointer;
1974
- transition: color var(--transition-fast);
1975
- text-transform: uppercase;
1976
- letter-spacing: 0.04em;
1977
- }
1978
-
1979
- .derived-toggle:hover {
1980
- color: var(--ui-text-secondary);
1981
- }
1982
-
1983
- .derived-toggle i {
1984
- font-size: var(--font-xs);
1985
- width: 0.75rem;
1986
- text-align: center;
1987
- }
1988
-
1989
- /* Scale layout */
1990
-
1991
- .scales-row {
1992
- display: flex;
1993
- gap: 3rem;
1994
- flex-wrap: wrap;
1995
- min-width: 0;
1996
- }
1997
-
1998
- .scale-section {
1999
- display: flex;
2000
- flex-direction: column;
2001
- gap: var(--space-6);
2002
- min-width: 0;
2003
- max-width: 100%;
2004
- }
2005
-
2006
- .scale-title {
2007
- font-size: var(--font-md);
2008
- font-weight: var(--font-weight-semibold);
2009
- color: var(--ui-text-tertiary);
2010
- margin: 0;
2011
- text-transform: uppercase;
2012
- letter-spacing: 0.05em;
2013
- }
2014
-
2015
- /* Step columns */
2016
-
2017
- .step-column {
2018
- display: flex;
2019
- flex-direction: column;
2020
- align-items: stretch;
2021
- justify-self: stretch;
2022
- gap: var(--space-2);
2023
- width: auto;
2024
- min-width: 0;
2025
- overflow: visible;
2026
- }
2027
-
2028
- .step-label {
2029
- font-size: var(--font-sm);
2030
- color: var(--ui-text-secondary);
2031
- text-align: center;
2032
- line-height: 1;
2033
- height: var(--font-xs);
2034
- display: flex;
2035
- align-items: flex-end;
2036
- }
2037
-
2038
- .step-label.copyable-label {
2039
- background: none;
2040
- border: none;
2041
- padding: 0;
2042
- cursor: pointer;
2043
- font: inherit;
2044
- font-size: var(--font-sm);
2045
- justify-content: center;
2046
- transition: color var(--transition-fast);
2047
- }
2048
-
2049
- .step-label.copyable-label:hover {
2050
- color: var(--ui-text-primary);
2051
- }
2052
-
2053
- .step-label.copyable-label.copied {
2054
- color: var(--ui-text-accent, var(--ui-text-primary));
2055
- }
2056
-
2057
- /* Swatches */
2058
-
2059
- .swatch {
2060
- width: 100%;
2061
- height: 2rem;
2062
- border-radius: var(--radius-sm);
2063
- border: 1px solid var(--ui-border-faint);
2064
- }
2065
-
2066
- .swatch.text-swatch {
2067
- display: flex;
2068
- align-items: center;
2069
- justify-content: center;
2070
- background: none;
2071
- font-size: var(--font-md);
2072
- font-weight: var(--font-weight-bold);
2073
- }
2074
-
2075
- .swatch.border-preview {
2076
- background: none;
2077
- border-radius: var(--radius-md);
2078
- }
2079
-
2080
- .override-fill.border-fill {
2081
- background: none;
2082
- border-radius: var(--radius-sm);
2083
- }
2084
-
2085
- .swatch.derived.clickable {
2086
- cursor: pointer;
2087
- }
2088
-
2089
- .swatch.derived.clickable:hover {
2090
- outline: 2px solid var(--ui-border-medium);
2091
- outline-offset: 1px;
2092
- }
2093
-
2094
- .swatch.derived.dimmed {
2095
- position: relative;
2096
- }
2097
-
2098
- .swatch.derived.dimmed::after {
2099
- content: '';
2100
- position: absolute;
2101
- inset: 0;
2102
- background: linear-gradient(
2103
- to bottom left,
2104
- transparent calc(50% - 0.5px),
2105
- white calc(50% - 0.5px),
2106
- white calc(50% + 0.5px),
2107
- transparent calc(50% + 0.5px)
2108
- );
2109
- border-radius: inherit;
2110
- }
2111
-
2112
- /* Override slot */
2113
- .override-slot {
2114
- border-style: dashed;
2115
- border-color: var(--ui-border-subtle);
2116
- cursor: pointer;
2117
- position: relative;
2118
- overflow: hidden;
2119
- }
2120
-
2121
- .override-slot:hover {
2122
- border-color: var(--ui-border-medium);
2123
- }
2124
-
2125
- .override-slot.active {
2126
- border-color: var(--ui-border-strong);
2127
- outline: 1px solid var(--ui-border-medium);
2128
- outline-offset: 1px;
2129
- }
2130
-
2131
- .override-slot.populated {
2132
- border-color: var(--ui-border-medium);
2133
- }
2134
-
2135
- .override-slot.matching::after {
2136
- content: '';
2137
- position: absolute;
2138
- inset: 0;
2139
- background: linear-gradient(
2140
- to bottom left,
2141
- transparent calc(50% - 0.5px),
2142
- white calc(50% - 0.5px),
2143
- white calc(50% + 0.5px),
2144
- transparent calc(50% + 0.5px)
2145
- );
2146
- border-radius: inherit;
2147
- pointer-events: none;
2148
- }
2149
-
2150
- .override-fill {
2151
- position: absolute;
2152
- inset: 0;
2153
- border-radius: inherit;
2154
- }
2155
-
2156
- .palette-step-label {
2157
- position: absolute;
2158
- inset: 0;
2159
- display: flex;
2160
- align-items: center;
2161
- justify-content: center;
2162
- font-size: var(--font-md);
2163
- font-weight: var(--font-weight-semibold);
2164
- color: white;
2165
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
2166
- pointer-events: none;
2167
- z-index: 1;
2168
- }
2169
-
2170
- .override-slot-wrapper {
2171
- position: relative;
2172
- }
2173
-
2174
- .snap-picker {
2175
- position: absolute;
2176
- top: 100%;
2177
- left: 50%;
2178
- transform: translateX(-50%);
2179
- z-index: 10;
2180
- margin-top: var(--space-4);
2181
- background: var(--ui-surface-lowest);
2182
- border: 1px solid var(--ui-border-medium);
2183
- border-radius: var(--radius-md);
2184
- padding: var(--space-4);
2185
- display: flex;
2186
- flex-direction: column;
2187
- gap: 1px;
2188
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
2189
- min-width: 5.5rem;
2190
- }
2191
-
2192
- .snap-picker-item {
2193
- display: flex;
2194
- align-items: center;
2195
- gap: var(--space-6);
2196
- padding: var(--space-2) var(--space-6);
2197
- border: none;
2198
- background: none;
2199
- border-radius: var(--radius-sm);
2200
- cursor: pointer;
2201
- color: var(--ui-text-secondary);
2202
- font-size: var(--font-md);
2203
- font-family: var(--ui-font-mono);
2204
- white-space: nowrap;
2205
- }
2206
-
2207
- .snap-picker-item:hover {
2208
- background: var(--ui-surface-high);
2209
- color: var(--ui-text-primary);
2210
- }
2211
-
2212
- .snap-picker-item.selected {
2213
- background: var(--ui-surface-highest);
2214
- color: var(--ui-text-primary);
2215
- font-weight: var(--font-weight-semibold);
2216
- }
2217
-
2218
- .snap-picker-swatch {
2219
- display: inline-block;
2220
- width: 1rem;
2221
- height: 1rem;
2222
- border-radius: var(--radius-sm);
2223
- border: 1px solid var(--ui-border-faint);
2224
- flex-shrink: 0;
2225
- }
2226
-
2227
- .snap-picker-label {
2228
- line-height: 1;
2229
- }
2230
-
2231
- /* Step hex values */
2232
-
2233
- .step-hex {
2234
- font-size: var(--font-xs);
2235
- color: var(--ui-text-secondary);
2236
- font-family: var(--ui-font-mono);
2237
- cursor: pointer;
2238
- padding: 1px var(--space-2);
2239
- border-radius: var(--radius-sm);
2240
- white-space: nowrap;
2241
- background: none;
2242
- border: none;
2243
- text-align: center;
2244
- min-width: 0;
2245
- overflow: hidden;
2246
- text-overflow: ellipsis;
2247
- }
2248
-
2249
- .step-hex:hover {
2250
- background: var(--ui-surface-highest);
2251
- color: var(--ui-text-primary);
2252
- }
2253
-
2254
- .step-hex.copied {
2255
- color: var(--ui-text-accent);
2256
- }
2257
-
2258
- /* Swatch grid */
2259
-
2260
- .swatch-grid {
2261
- display: grid;
2262
- grid-template-columns: repeat(var(--swatch-cols), minmax(0, 1fr));
2263
- gap: var(--space-4) var(--swatch-gap, var(--space-4));
2264
- align-items: start;
2265
- justify-content: start;
2266
- min-width: 0;
2267
- max-width: calc(var(--swatch-cols) * 4rem + (var(--swatch-cols) - 1) * var(--swatch-gap, var(--space-4)));
2268
- }
2269
-
2270
- .curve-grid-span {
2271
- display: flex;
2272
- flex-direction: column;
2273
- gap: var(--space-8);
2274
- }
2275
-
2276
- .swatch.gray-swatch {
2277
- width: 100%;
2278
- height: calc(4rem + var(--space-2));
2279
- cursor: pointer;
2280
- position: relative;
2281
- }
2282
-
2283
- .swatch.gray-swatch:hover {
2284
- border-color: var(--ui-border-medium);
2285
- }
2286
-
2287
- .swatch.gray-swatch.active {
2288
- border-color: var(--ui-border-strong);
2289
- outline: 2px solid var(--ui-border-medium);
2290
- outline-offset: 1px;
2291
- }
2292
-
2293
- .override-dot {
2294
- position: absolute;
2295
- top: 3px;
2296
- right: 3px;
2297
- width: 6px;
2298
- height: 6px;
2299
- border-radius: 50%;
2300
- background: var(--ui-text-primary);
2301
- border: 1px solid rgba(255, 255, 255, 0.6);
2302
- }
2303
-
2304
- .empty-mode-toggle {
2305
- display: flex;
2306
- align-items: center;
2307
- gap: var(--space-6);
2308
- font-size: var(--font-md);
2309
- color: var(--ui-text-secondary);
2310
- cursor: pointer;
2311
- user-select: none;
2312
- }
2313
-
2314
- .empty-mode-toggle input {
2315
- margin: 0;
2316
- cursor: pointer;
2317
- }
2318
-
2319
- .empty-check {
2320
- -webkit-appearance: none;
2321
- appearance: none;
2322
- position: absolute;
2323
- bottom: 3px;
2324
- right: 3px;
2325
- margin: 0;
2326
- cursor: pointer;
2327
- width: 14px;
2328
- height: 14px;
2329
- background: white;
2330
- border: 1px solid rgba(0, 0, 0, 0.3);
2331
- border-radius: 2px;
2332
- opacity: 0.5;
2333
- }
2334
-
2335
- .empty-check:checked {
2336
- opacity: 1;
2337
- }
2338
-
2339
- .empty-check:checked::after {
2340
- content: '\2713';
2341
- display: flex;
2342
- align-items: center;
2343
- justify-content: center;
2344
- width: 100%;
2345
- height: 100%;
2346
- font-size: var(--font-md);
2347
- font-weight: bold;
2348
- color: black;
2349
- line-height: 1;
2350
- }
2351
-
2352
- /* Gradient controls */
2353
- .gradient-controls {
2354
- margin-top: var(--space-8);
2355
- padding: var(--space-12);
2356
- background: var(--ui-surface-low);
2357
- border: 1px solid var(--ui-border-faint);
2358
- border-radius: var(--radius-lg);
2359
- display: flex;
2360
- flex-direction: column;
2361
- gap: var(--space-8);
2362
- }
2363
-
2364
- .gradient-row {
2365
- display: flex;
2366
- align-items: center;
2367
- gap: var(--space-8);
2368
- }
2369
-
2370
- .gradient-label {
2371
- font-size: var(--font-md);
2372
- color: var(--ui-text-secondary);
2373
- min-width: 36px;
2374
- flex-shrink: 0;
2375
- }
2376
-
2377
- .gradient-style-buttons {
2378
- display: flex;
2379
- gap: var(--space-2);
2380
- }
2381
-
2382
- .style-btn {
2383
- width: 28px;
2384
- height: 28px;
2385
- border: 1px solid var(--ui-border-subtle);
2386
- border-radius: var(--radius-md);
2387
- background: var(--ui-surface-lowest);
2388
- color: var(--ui-text-secondary);
2389
- cursor: pointer;
2390
- font-size: var(--font-md);
2391
- display: flex;
2392
- align-items: center;
2393
- justify-content: center;
2394
- padding: 0;
2395
- }
2396
-
2397
- .style-btn.active {
2398
- border-color: var(--ui-text-secondary);
2399
- background: var(--ui-surface-high);
2400
- color: var(--ui-text-primary);
2401
- }
2402
-
2403
- .style-btn:hover {
2404
- border-color: var(--ui-border-medium);
2405
- }
2406
-
2407
- .size-btn {
2408
- width: auto;
2409
- padding: 0 8px;
2410
- }
2411
-
2412
- .gradient-angle-input,
2413
- .gradient-pos-input {
2414
- width: 52px;
2415
- padding: 2px 6px;
2416
- font-size: var(--font-md);
2417
- background: var(--ui-surface-lowest);
2418
- border: 1px solid var(--ui-border-subtle);
2419
- border-radius: var(--radius-md);
2420
- color: var(--ui-text-primary);
2421
- text-align: center;
2422
- }
2423
-
2424
- .gradient-angle-slider {
2425
- flex: 1;
2426
- min-width: 60px;
2427
- height: 4px;
2428
- accent-color: var(--ui-text-secondary);
2429
- }
2430
-
2431
- .gradient-unit {
2432
- font-size: var(--font-md);
2433
- color: var(--ui-text-tertiary);
2434
- }
2435
-
2436
- .gradient-checkbox-label {
2437
- display: flex;
2438
- align-items: center;
2439
- gap: var(--space-6);
2440
- font-size: var(--font-md);
2441
- color: var(--ui-text-secondary);
2442
- cursor: pointer;
2443
- user-select: none;
2444
- }
2445
-
2446
- .gradient-checkbox-label input {
2447
- margin: 0;
2448
- cursor: pointer;
2449
- }
2450
-
2451
- .gradient-select {
2452
- padding: 2px 6px;
2453
- font-size: var(--font-md);
2454
- background: var(--ui-surface-lowest);
2455
- border: 1px solid var(--ui-border-subtle);
2456
- border-radius: var(--radius-md);
2457
- color: var(--ui-text-primary);
2458
- }
2459
-
2460
- .stop-color-preview {
2461
- width: 20px;
2462
- height: 20px;
2463
- border-radius: var(--radius-sm);
2464
- border: 1px solid var(--ui-border-subtle);
2465
- flex-shrink: 0;
2466
- }
2467
-
2468
- .gradient-stop-bar-wrapper {
2469
- padding: 0;
2470
- display: flex;
2471
- flex-direction: column;
2472
- gap: 0;
2473
- }
2474
-
2475
- .gradient-stop-handles {
2476
- position: relative;
2477
- height: 28px;
2478
- }
2479
-
2480
- .gradient-stop-bar {
2481
- position: relative;
2482
- height: 24px;
2483
- border-radius: var(--radius-md);
2484
- border: 1px solid var(--ui-border-subtle);
2485
- cursor: crosshair;
2486
- }
2487
-
2488
- .gradient-stop-handle {
2489
- position: absolute;
2490
- top: 0;
2491
- transform: translateX(-50%);
2492
- display: flex;
2493
- flex-direction: column;
2494
- align-items: center;
2495
- cursor: grab;
2496
- z-index: 1;
2497
- }
2498
-
2499
- .gradient-stop-handle.selected {
2500
- z-index: 2;
2501
- }
2502
-
2503
- .stop-swatch {
2504
- width: 16px;
2505
- height: 16px;
2506
- border-radius: var(--radius-sm);
2507
- border: 2px solid var(--ui-border-medium);
2508
- flex-shrink: 0;
2509
- }
2510
-
2511
- .gradient-stop-handle.selected .stop-swatch {
2512
- border-color: var(--ui-text-primary);
2513
- }
2514
-
2515
- .gradient-stop-handle:hover .stop-swatch {
2516
- border-color: var(--ui-text-secondary);
2517
- }
2518
-
2519
- .stop-arrow {
2520
- width: 0;
2521
- height: 0;
2522
- border-left: 4px solid transparent;
2523
- border-right: 4px solid transparent;
2524
- border-top: 6px solid var(--ui-border-medium);
2525
- }
2526
-
2527
- .gradient-stop-handle.selected .stop-arrow {
2528
- border-top-color: var(--ui-text-primary);
2529
- }
2530
-
2531
- .gradient-stop-handle:hover .stop-arrow {
2532
- border-top-color: var(--ui-text-secondary);
2533
- }
2534
-
2535
- .stop-controls {
2536
- flex-wrap: wrap;
2537
- }
2538
-
2539
- .stop-remove-btn {
2540
- width: 20px;
2541
- height: 20px;
2542
- border: 1px solid var(--ui-border-subtle);
2543
- border-radius: var(--radius-md);
2544
- background: var(--ui-surface-lowest);
2545
- color: var(--ui-text-tertiary);
2546
- cursor: pointer;
2547
- font-size: var(--font-md);
2548
- display: flex;
2549
- align-items: center;
2550
- justify-content: center;
2551
- padding: 0;
2552
- margin-left: auto;
2553
- }
2554
-
2555
- .stop-remove-btn:hover {
2556
- border-color: var(--ui-border-strong);
2557
- color: var(--ui-text-primary);
2558
- }
2559
-
2560
- /* Narrow desktop: tighten palette editor spacing */
2561
- @media (max-width: 1280px) {
2562
- .palette-editor {
2563
- padding: var(--space-12) var(--space-12) var(--space-20);
2564
- }
2565
- .scales-row {
2566
- gap: var(--space-24);
2567
- }
2568
- .header-swatch {
2569
- width: 3rem;
2570
- height: 3rem;
2571
- }
2572
- }
2573
-
2574
- @media (max-width: 1024px) {
2575
- .scales-row {
2576
- gap: var(--space-16);
2577
- }
2578
- }
2579
- </style>