@motion-proto/live-tokens 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist-plugin/index.cjs +444 -0
- package/dist-plugin/index.d.cts +12 -0
- package/dist-plugin/index.d.ts +12 -0
- package/dist-plugin/index.js +407 -0
- package/package.json +86 -0
- package/src/components/Badge.svelte +82 -0
- package/src/components/Button.svelte +333 -0
- package/src/components/Card.svelte +83 -0
- package/src/components/CollapsibleSection.svelte +82 -0
- package/src/components/DetailNav.svelte +78 -0
- package/src/components/Dialog.svelte +269 -0
- package/src/components/InlineEditActions.svelte +73 -0
- package/src/components/Notification.svelte +308 -0
- package/src/components/ProgressBar.svelte +99 -0
- package/src/components/RadioButton.svelte +87 -0
- package/src/components/SectionDivider.svelte +121 -0
- package/src/components/TabBar.svelte +92 -0
- package/src/components/Toggle.svelte +86 -0
- package/src/components/Tooltip.svelte +64 -0
- package/src/lib/ColumnsOverlay.svelte +120 -0
- package/src/lib/LiveEditorOverlay.svelte +467 -0
- package/src/lib/columnsOverlay.ts +26 -0
- package/src/lib/cssVarSync.ts +72 -0
- package/src/lib/editorConfig.ts +9 -0
- package/src/lib/editorConfigStore.ts +14 -0
- package/src/lib/index.ts +51 -0
- package/src/lib/oklch.ts +129 -0
- package/src/lib/pageSource.ts +6 -0
- package/src/lib/tokenInit.ts +29 -0
- package/src/lib/tokenService.ts +144 -0
- package/src/lib/tokenTypes.ts +45 -0
- package/src/pages/Admin.svelte +100 -0
- package/src/pages/ShowcasePage.svelte +146 -0
- package/src/showcase/BackupBrowser.svelte +617 -0
- package/src/showcase/BezierCurveEditor.svelte +648 -0
- package/src/showcase/ColorEditPanel.svelte +498 -0
- package/src/showcase/ComponentsTab.svelte +107 -0
- package/src/showcase/EditorDialog.svelte +137 -0
- package/src/showcase/PaletteEditor.svelte +2579 -0
- package/src/showcase/PaletteSelector.svelte +627 -0
- package/src/showcase/SurfacesTab.svelte +409 -0
- package/src/showcase/TextTab.svelte +205 -0
- package/src/showcase/TokenFileManager.svelte +683 -0
- package/src/showcase/TokenMap.svelte +54 -0
- package/src/showcase/VariablesTab.svelte +2657 -0
- package/src/showcase/VisualsTab.svelte +233 -0
- package/src/showcase/curveEngine.ts +190 -0
- package/src/showcase/demos/BadgeDemo.svelte +58 -0
- package/src/showcase/demos/CardDemo.svelte +52 -0
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +194 -0
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +56 -0
- package/src/showcase/demos/DialogDemo.svelte +42 -0
- package/src/showcase/demos/InlineEditActionsDemo.svelte +27 -0
- package/src/showcase/demos/NotificationDemo.svelte +149 -0
- package/src/showcase/demos/ProgressBarDemo.svelte +56 -0
- package/src/showcase/demos/RadioButtonDemo.svelte +58 -0
- package/src/showcase/demos/SectionDividerDemo.svelte +79 -0
- package/src/showcase/demos/StandardButtonsDemo.svelte +457 -0
- package/src/showcase/demos/TabBarDemo.svelte +60 -0
- package/src/showcase/demos/TooltipDemo.svelte +54 -0
- package/src/showcase/editor.css +93 -0
- package/src/showcase/index.ts +17 -0
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +97 -0
- package/src/styles/fonts/Domine/README.txt +66 -0
- package/src/styles/fonts.css +18 -0
- package/src/styles/form-controls.css +190 -0
|
@@ -0,0 +1,2579 @@
|
|
|
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
|
+
>×</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 & 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 & 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>
|