@motion-proto/live-tokens 0.38.0 → 0.40.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/.claude/skills/live-tokens-pick-component/SKILL.md +9 -1
- package/CHANGELOG.md +71 -0
- package/dist-plugin/index.cjs +20 -72
- package/dist-plugin/index.js +20 -72
- package/package.json +1 -1
- package/src/editor/component-editor/IconButtonEditor.svelte +175 -0
- package/src/editor/component-editor/SectionDividerEditor.svelte +1 -1
- package/src/editor/component-editor/registry.ts +11 -0
- package/src/editor/core/palettes/paletteDerivation.ts +28 -83
- package/src/editor/core/store/editorStore.ts +2 -1
- package/src/editor/core/store/gradientSource.ts +49 -9
- package/src/editor/core/themes/migrations/2026-06-05-palette-unification.ts +90 -0
- package/src/editor/core/themes/themeTypes.ts +3 -8
- package/src/editor/docs/content/01-overview.md +3 -2
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/ui/ColorEditPanel.svelte +93 -172
- package/src/editor/ui/PaletteEditor.svelte +46 -207
- package/src/editor/ui/VariablesTab.svelte +2 -2
- package/src/editor/ui/palette/PaletteBase.svelte +29 -37
- package/src/editor/ui/palette/paletteEditorState.ts +12 -25
- package/src/editor/ui/palette/paletteMath.ts +22 -49
- package/src/live-tokens/data/themes/default.json +11 -391
- package/src/live-tokens/data/tokens.generated.css +14 -14
- package/src/system/components/IconButton.svelte +322 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const bubble = createBubbler();
|
|
5
5
|
import { onMount, onDestroy, tick, untrack } from 'svelte';
|
|
6
|
-
import { hexToOklch } from '../core/palettes/oklch';
|
|
6
|
+
import { hexToOklch, oklchToHex, gamutClamp } from '../core/palettes/oklch';
|
|
7
7
|
import { type CurveAnchor, lightnessCurveConfig, saturationCurveConfig } from './curveEngine';
|
|
8
8
|
import ColorEditPanel from './ColorEditPanel.svelte';
|
|
9
9
|
import OverridesPanel from './palette/OverridesPanel.svelte';
|
|
@@ -13,15 +13,14 @@
|
|
|
13
13
|
import PaletteBase from './palette/PaletteBase.svelte';
|
|
14
14
|
import { type EditingState, idleState, BASE_KEY, isEditingBase as isBaseEdit } from './palette/paletteEditorState';
|
|
15
15
|
import {
|
|
16
|
-
type Step, type Scale,
|
|
17
|
-
GRAY_FALLBACK,
|
|
18
|
-
DEFAULT_PALETTE_LIGHTNESS, DEFAULT_PALETTE_SATURATION,
|
|
19
|
-
defaultScaleCurves, defaultScaleCurvesObject,
|
|
20
|
-
paletteStepLightness,
|
|
21
|
-
paletteStepKey,
|
|
16
|
+
type Step, type Scale,
|
|
17
|
+
GRAY_FALLBACK,
|
|
18
|
+
DEFAULT_PALETTE_LIGHTNESS, DEFAULT_PALETTE_SATURATION,
|
|
19
|
+
defaultScaleCurves, defaultScaleCurvesObject, defaultPaletteConfig,
|
|
20
|
+
paletteStepLightness, scales,
|
|
21
|
+
paletteStepKey, stepKey, scaleCurveKey as getScaleCurveKey,
|
|
22
22
|
stepIndexToX,
|
|
23
23
|
injectLockedAnchor, removeLockedAnchor,
|
|
24
|
-
computeGrayColor as computeGrayColorPure,
|
|
25
24
|
computePaletteColor as computePaletteColorPure,
|
|
26
25
|
computeDerivedColor as computeDerivedColorPure,
|
|
27
26
|
snapScaleToPalette as snapScaleToPalettePure,
|
|
@@ -34,7 +33,7 @@
|
|
|
34
33
|
label: string;
|
|
35
34
|
displayLabel?: string | null;
|
|
36
35
|
initialColor?: string;
|
|
37
|
-
|
|
36
|
+
neutral?: boolean;
|
|
38
37
|
cssNamespace?: string | null;
|
|
39
38
|
emptySelector?: boolean;
|
|
40
39
|
}
|
|
@@ -43,39 +42,23 @@
|
|
|
43
42
|
label,
|
|
44
43
|
displayLabel = null,
|
|
45
44
|
initialColor = GRAY_FALLBACK,
|
|
46
|
-
|
|
45
|
+
neutral = false,
|
|
47
46
|
cssNamespace = null,
|
|
48
47
|
emptySelector = false
|
|
49
48
|
}: Props = $props();
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
function defaultPaletteConfig(): PaletteConfig {
|
|
53
|
-
return {
|
|
54
|
-
baseColor: initialColor,
|
|
55
|
-
tintHue: 240,
|
|
56
|
-
tintChroma: DEFAULT_TINT_CHROMA,
|
|
57
|
-
lightnessCurve: DEFAULT_PALETTE_LIGHTNESS(),
|
|
58
|
-
saturationCurve: DEFAULT_PALETTE_SATURATION(),
|
|
59
|
-
grayLightnessCurve: DEFAULT_GRAY_LIGHTNESS(),
|
|
60
|
-
graySaturationCurve: DEFAULT_GRAY_SATURATION(),
|
|
61
|
-
scaleCurves: defaultScaleCurvesObject(),
|
|
62
|
-
curveOffset: { lightness: 0, saturation: 0 },
|
|
63
|
-
overrides: {},
|
|
64
|
-
snappedScales: [],
|
|
65
|
-
anchorToBase: true,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
50
|
+
const seedConfig = () => defaultPaletteConfig({ baseColor: initialColor, neutral });
|
|
68
51
|
|
|
69
52
|
function edit<K extends keyof PaletteConfig>(field: K, value: PaletteConfig[K]): void {
|
|
70
53
|
mutate(`${label}: ${String(field)}`, (s) => {
|
|
71
|
-
if (!s.palettes[label]) s.palettes[label] =
|
|
54
|
+
if (!s.palettes[label]) s.palettes[label] = seedConfig();
|
|
72
55
|
(s.palettes[label] as any)[field] = value;
|
|
73
56
|
});
|
|
74
57
|
}
|
|
75
58
|
|
|
76
59
|
function patchPalette(patch: Partial<PaletteConfig>, historyLabel: string): void {
|
|
77
60
|
mutate(`${label}: ${historyLabel}`, (s) => {
|
|
78
|
-
if (!s.palettes[label]) s.palettes[label] =
|
|
61
|
+
if (!s.palettes[label]) s.palettes[label] = seedConfig();
|
|
79
62
|
Object.assign(s.palettes[label], patch);
|
|
80
63
|
});
|
|
81
64
|
}
|
|
@@ -110,14 +93,11 @@
|
|
|
110
93
|
edit('emptyMode', (e.currentTarget as HTMLInputElement).checked ? 'gradient' : 'solid');
|
|
111
94
|
}
|
|
112
95
|
|
|
113
|
-
let grayEditorOpen = $state(false);
|
|
114
96
|
let showDerived = $state(false);
|
|
115
97
|
let paletteEditorOpen = $state(false);
|
|
116
98
|
|
|
117
99
|
function setLightnessCurve(a: CurveAnchor[]) { edit('lightnessCurve', a); }
|
|
118
100
|
function setSaturationCurve(a: CurveAnchor[]) { edit('saturationCurve', a); }
|
|
119
|
-
function setGrayLightnessCurve(a: CurveAnchor[]) { edit('grayLightnessCurve', a); }
|
|
120
|
-
function setGraySaturationCurve(a: CurveAnchor[]) { edit('graySaturationCurve', a); }
|
|
121
101
|
|
|
122
102
|
function handleOffset(key: string, value: number) {
|
|
123
103
|
edit('curveOffset', { ...curveOffset, [key]: value });
|
|
@@ -128,10 +108,6 @@
|
|
|
128
108
|
let injectedLightness = false;
|
|
129
109
|
let injectedSaturation = false;
|
|
130
110
|
|
|
131
|
-
function computeGrayColor(index: number, hue: number, chroma: number = tintChroma): string {
|
|
132
|
-
return computeGrayColorPure(index, hue, chroma, grayLightnessCurve, graySaturationCurve, curveOffset);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
111
|
function computePaletteColor(index: number, base: string): string {
|
|
136
112
|
return computePaletteColorPure(index, base, lightnessCurve, saturationCurve, curveOffset);
|
|
137
113
|
}
|
|
@@ -180,9 +156,7 @@
|
|
|
180
156
|
|
|
181
157
|
function startBaseEdit() {
|
|
182
158
|
if (editing.kind === 'editingBase') { confirmEdit(); return; }
|
|
183
|
-
editing =
|
|
184
|
-
? { kind: 'editingBase', snapshotHex: gray500Hex, snapshotTintHue: tintHue, snapshotTintChroma: tintChroma }
|
|
185
|
-
: { kind: 'editingBase', snapshotHex: baseColor, snapshotTintHue: null, snapshotTintChroma: null };
|
|
159
|
+
editing = { kind: 'editingBase' };
|
|
186
160
|
openSession();
|
|
187
161
|
}
|
|
188
162
|
|
|
@@ -210,19 +184,22 @@
|
|
|
210
184
|
}
|
|
211
185
|
|
|
212
186
|
function handleColorChange(hex: string) {
|
|
213
|
-
if (isEditingBase) {
|
|
214
|
-
// Gray mode's base is derived from tintHue/tintChroma; raw hex only applies to chromatic.
|
|
215
|
-
if (mode === 'chromatic') edit('baseColor', hex);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
187
|
if (editing.kind === 'editingStep') {
|
|
219
188
|
editing = { ...editing, draft: hex };
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
189
|
+
// Write every drag tick to the store (not just pre-existing overrides) so
|
|
190
|
+
// the live page and the derived-scale preview swatches update in realtime.
|
|
191
|
+
// The open session collapses these to one undo entry; cancel restores the
|
|
192
|
+
// snapshot, and confirmEdit drops the override if the draft equals the
|
|
193
|
+
// computed value, so a no-op edit leaves nothing behind.
|
|
194
|
+
edit('overrides', { ...overrides, [editing.stepKey]: hex });
|
|
223
195
|
}
|
|
224
196
|
}
|
|
225
197
|
|
|
198
|
+
function oklchHex(hue: number, chroma: number, lightness: number): string {
|
|
199
|
+
const c = gamutClamp(lightness / 100, chroma, hue);
|
|
200
|
+
return oklchToHex(c.l, c.c, c.h);
|
|
201
|
+
}
|
|
202
|
+
|
|
226
203
|
function resetOverride(k: string) {
|
|
227
204
|
if (!(k in overrides)) return;
|
|
228
205
|
const { [k]: _, ...rest } = overrides;
|
|
@@ -234,18 +211,7 @@
|
|
|
234
211
|
confirmEdit();
|
|
235
212
|
return;
|
|
236
213
|
}
|
|
237
|
-
const current = (k in overrides) ? overrides[k] : computeDerivedColor(step,
|
|
238
|
-
editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
|
|
239
|
-
openSession();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function handleGrayClick(gStep: GrayStep, index: number) {
|
|
243
|
-
const k = grayStepKey(gStep.label);
|
|
244
|
-
if (editingKey === k) {
|
|
245
|
-
confirmEdit();
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const current = (k in overrides) ? overrides[k] : computeGrayColor(index, tintHue, tintChroma);
|
|
214
|
+
const current = (k in overrides) ? overrides[k] : computeDerivedColor(step, baseColor, scaleTitle);
|
|
249
215
|
editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
|
|
250
216
|
openSession();
|
|
251
217
|
}
|
|
@@ -282,7 +248,7 @@
|
|
|
282
248
|
|
|
283
249
|
function cancelEdit() {
|
|
284
250
|
editing = idleState;
|
|
285
|
-
// Restoring the session snapshot pulls baseColor/
|
|
251
|
+
// Restoring the session snapshot pulls baseColor/overrides/… back to pre-open.
|
|
286
252
|
if (paletteEditScope) { cancelScope(paletteEditScope); paletteEditScope = null; }
|
|
287
253
|
}
|
|
288
254
|
|
|
@@ -298,12 +264,10 @@
|
|
|
298
264
|
const idx = paletteStepLightness.indexOf(ps);
|
|
299
265
|
return computePaletteColor(idx, baseColor);
|
|
300
266
|
}
|
|
301
|
-
const gi = graySteps.findIndex(g => grayStepKey(g.label) === key);
|
|
302
|
-
if (gi >= 0) return computeGrayColor(gi, tintHue, tintChroma);
|
|
303
267
|
for (const scale of scales) {
|
|
304
268
|
for (const step of scale.steps) {
|
|
305
269
|
if (stepKey(scale.title, step.name) === key) {
|
|
306
|
-
return computeDerivedColor(step,
|
|
270
|
+
return computeDerivedColor(step, baseColor, scale.title);
|
|
307
271
|
}
|
|
308
272
|
}
|
|
309
273
|
}
|
|
@@ -312,7 +276,7 @@
|
|
|
312
276
|
|
|
313
277
|
function effectiveColor(k: string, step: Step, scaleTitle: string, _version?: string): string {
|
|
314
278
|
if (editingKey === k && editingDraft !== null) return editingDraft;
|
|
315
|
-
return (k in overrides) ? overrides[k] : computeDerivedColor(step,
|
|
279
|
+
return (k in overrides) ? overrides[k] : computeDerivedColor(step, baseColor, scaleTitle);
|
|
316
280
|
}
|
|
317
281
|
|
|
318
282
|
let copiedKey: string | null = $state(null);
|
|
@@ -356,11 +320,7 @@
|
|
|
356
320
|
|
|
357
321
|
function clearPaletteOverrides() {
|
|
358
322
|
const next = { ...overrides };
|
|
359
|
-
|
|
360
|
-
for (const step of graySteps) delete next[grayStepKey(step.label)];
|
|
361
|
-
} else {
|
|
362
|
-
for (const ps of paletteStepLightness) delete next[paletteStepKey(ps.label)];
|
|
363
|
-
}
|
|
323
|
+
for (const ps of paletteStepLightness) delete next[paletteStepKey(ps.label)];
|
|
364
324
|
edit('overrides', next);
|
|
365
325
|
}
|
|
366
326
|
|
|
@@ -427,12 +387,8 @@
|
|
|
427
387
|
// every mutate-in-place store update (Svelte 5 uses `===` equality) and
|
|
428
388
|
// short-circuit the downstream chain — swatches would freeze.
|
|
429
389
|
let baseColor = $derived($editorState.palettes[label]?.baseColor ?? initialColor);
|
|
430
|
-
let tintHue = $derived($editorState.palettes[label]?.tintHue ?? 240);
|
|
431
|
-
let tintChroma = $derived($editorState.palettes[label]?.tintChroma ?? DEFAULT_TINT_CHROMA);
|
|
432
390
|
let lightnessCurve = $derived($editorState.palettes[label]?.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS());
|
|
433
391
|
let saturationCurve = $derived($editorState.palettes[label]?.saturationCurve ?? DEFAULT_PALETTE_SATURATION());
|
|
434
|
-
let grayLightnessCurve = $derived($editorState.palettes[label]?.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS());
|
|
435
|
-
let graySaturationCurve = $derived($editorState.palettes[label]?.graySaturationCurve ?? DEFAULT_GRAY_SATURATION());
|
|
436
392
|
let scaleCurves = $derived($editorState.palettes[label]?.scaleCurves ?? defaultScaleCurvesObject());
|
|
437
393
|
let curveOffset = $derived($editorState.palettes[label]?.curveOffset ?? { lightness: 0, saturation: 0 });
|
|
438
394
|
let overrides = $derived($editorState.palettes[label]?.overrides ?? {});
|
|
@@ -450,27 +406,6 @@
|
|
|
450
406
|
let gradientSize = $derived($editorState.palettes[label]?.gradientSize ?? 'page');
|
|
451
407
|
let editingKey = $derived(editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? BASE_KEY : editing.stepKey);
|
|
452
408
|
let editingDraft = $derived(editing.kind === 'editingStep' ? editing.draft : null);
|
|
453
|
-
let grayComputed = $derived((() => {
|
|
454
|
-
const _gl = grayLightnessCurve, _gs = graySaturationCurve, _co = curveOffset, _tc = tintChroma, _th = tintHue;
|
|
455
|
-
return graySteps.map((step, index) => ({
|
|
456
|
-
step,
|
|
457
|
-
index,
|
|
458
|
-
key: grayStepKey(step.label),
|
|
459
|
-
hex: computeGrayColor(index, _th, _tc),
|
|
460
|
-
}));
|
|
461
|
-
})());
|
|
462
|
-
let grayEffective = $derived((() => {
|
|
463
|
-
const _ed = editingDraft, _ek = editingKey, _ov = overrides;
|
|
464
|
-
return grayComputed.map(g => ({
|
|
465
|
-
...g,
|
|
466
|
-
effective: (_ek === g.key && _ed !== null) ? _ed : (g.key in _ov) ? _ov[g.key] : g.hex,
|
|
467
|
-
}));
|
|
468
|
-
})());
|
|
469
|
-
// Always use the computed (curve-derived) value so derived scales update in
|
|
470
|
-
// realtime when tint changes.
|
|
471
|
-
let gray500Hex = $derived(mode === 'gray'
|
|
472
|
-
? (grayComputed.find(g => g.step.label === '500')?.hex ?? GRAY_FALLBACK)
|
|
473
|
-
: baseColor);
|
|
474
409
|
// Keep the locked lightness anchor y in sync with baseColor. Idempotent.
|
|
475
410
|
// `lightnessCurve` is read via `untrack` so writing it back via `edit` does
|
|
476
411
|
// not retrigger this effect (Svelte 5 flags the read+write pattern as
|
|
@@ -509,24 +444,17 @@
|
|
|
509
444
|
const stops = sorted.map(s => `${stopColor(s, pc)} ${s.position}%`).join(', ');
|
|
510
445
|
return `linear-gradient(to right, ${stops})`;
|
|
511
446
|
});
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
//
|
|
515
|
-
|
|
447
|
+
// `editingDraft` is folded in so the effective-hex closure passed to
|
|
448
|
+
// OverridesPanel re-derives on every drag tick (the per-step hex text would
|
|
449
|
+
// otherwise lag the live swatch during an override edit).
|
|
450
|
+
let curveVersion = $derived(JSON.stringify(scaleCurves) + JSON.stringify(curveOffset) + baseColor + (editingDraft ?? ''));
|
|
516
451
|
let derivedHexForBase = $derived((step: Step, scaleTitle: string) => computeDerivedColor(step, baseColor, scaleTitle));
|
|
517
|
-
let derivedHexForGray = $derived((step: Step, scaleTitle: string) => computeDerivedColor(step, gray500Hex, scaleTitle));
|
|
518
452
|
let effectiveHexAny = $derived((k: string, step: Step, scaleTitle: string) => effectiveColor(k, step, scaleTitle, curveVersion));
|
|
519
453
|
|
|
520
454
|
let isEditingBase = $derived(isBaseEdit(editing));
|
|
521
|
-
let editingColor = $derived(isEditingBase
|
|
522
|
-
? (mode === 'gray' ? gray500Hex : baseColor)
|
|
523
|
-
: editingDraft);
|
|
455
|
+
let editingColor = $derived(isEditingBase ? baseColor : editingDraft);
|
|
524
456
|
let editingStepInfo = $derived((() => {
|
|
525
457
|
if (!editingKey || isEditingBase) return null;
|
|
526
|
-
if (mode === 'gray') {
|
|
527
|
-
const gs = graySteps.find(s => grayStepKey(s.label) === editingKey);
|
|
528
|
-
if (gs) return { scale: 'Gray', step: gs.label };
|
|
529
|
-
}
|
|
530
458
|
const ps = paletteStepLightness.find(p => paletteStepKey(p.label) === editingKey);
|
|
531
459
|
if (ps) return { scale: 'Palette', step: ps.label };
|
|
532
460
|
for (const scale of scales) {
|
|
@@ -556,15 +484,12 @@
|
|
|
556
484
|
});
|
|
557
485
|
</script>
|
|
558
486
|
|
|
559
|
-
<div class="palette-editor" style="--editor-base: {
|
|
487
|
+
<div class="palette-editor" style="--editor-base: {baseColor}">
|
|
560
488
|
<PaletteBase
|
|
561
489
|
{label}
|
|
562
490
|
{displayLabel}
|
|
563
|
-
{
|
|
491
|
+
{neutral}
|
|
564
492
|
{baseColor}
|
|
565
|
-
{gray500Hex}
|
|
566
|
-
{tintHue}
|
|
567
|
-
{tintChroma}
|
|
568
493
|
{anchorToBase}
|
|
569
494
|
{isEditingBase}
|
|
570
495
|
{panelOpen}
|
|
@@ -574,13 +499,11 @@
|
|
|
574
499
|
onStartEdit={startBaseEdit}
|
|
575
500
|
onConfirm={confirmEdit}
|
|
576
501
|
onCancel={cancelEdit}
|
|
577
|
-
|
|
578
|
-
onTintChange={(h, c) => patchPalette({ tintHue: h, tintChroma: c }, 'tint hue/chroma')}
|
|
502
|
+
onBaseChange={(h, c, l) => edit('baseColor', oklchHex(h, c, l))}
|
|
579
503
|
onAnchorToBaseChange={setAnchorToBase}
|
|
580
504
|
onCopyBaseHex={copyHex}
|
|
581
505
|
/>
|
|
582
506
|
|
|
583
|
-
{#if mode === 'chromatic'}
|
|
584
507
|
<!-- Palette + Text row -->
|
|
585
508
|
<div class="scales-row">
|
|
586
509
|
<div class="scale-section">
|
|
@@ -700,87 +623,6 @@
|
|
|
700
623
|
|
|
701
624
|
</div>
|
|
702
625
|
|
|
703
|
-
{:else}
|
|
704
|
-
<!-- Gray mode: palette + text row -->
|
|
705
|
-
<div class="scales-row">
|
|
706
|
-
<div class="scale-section">
|
|
707
|
-
<div class="scale-header">
|
|
708
|
-
<h4 class="scale-title">{displayLabel ?? label}</h4>
|
|
709
|
-
<UIPillButton size="compact" variant="outline" onclick={clearPaletteOverrides}>Clear Overrides</UIPillButton>
|
|
710
|
-
<UIPillButton size="compact" variant="outline" onclick={() => grayEditorOpen = !grayEditorOpen}>
|
|
711
|
-
{grayEditorOpen ? 'Close' : 'Edit'}
|
|
712
|
-
</UIPillButton>
|
|
713
|
-
</div>
|
|
714
|
-
<div class="swatch-grid" style="--swatch-cols: {graySteps.length + 2}">
|
|
715
|
-
<div class="step-column">
|
|
716
|
-
<button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-white'} type="button" onclick={(e) => copyVarName('gray-white', `--color-${cssNamespace}-white`, e)}>
|
|
717
|
-
{copiedLabelKey === 'gray-white' ? 'copied!' : 'white'}
|
|
718
|
-
</button>
|
|
719
|
-
<div class="swatch gray-swatch bookend" style="background: #ffffff"></div>
|
|
720
|
-
</div>
|
|
721
|
-
{#each grayEffective as g}
|
|
722
|
-
<div class="step-column">
|
|
723
|
-
<button class="step-label copyable-label" class:copied={copiedLabelKey === g.key} type="button" onclick={(e) => copyVarName(g.key, `--color-${cssNamespace}-${g.step.label}`, e)}>
|
|
724
|
-
{copiedLabelKey === g.key ? 'copied!' : g.step.label}
|
|
725
|
-
</button>
|
|
726
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
727
|
-
<div
|
|
728
|
-
class="swatch gray-swatch"
|
|
729
|
-
class:active={editingKey === g.key}
|
|
730
|
-
class:overridden={g.key in overrides}
|
|
731
|
-
style="background: {g.effective}"
|
|
732
|
-
onclick={() => handleGrayClick(g.step, g.index)}
|
|
733
|
-
role="button"
|
|
734
|
-
tabindex="0"
|
|
735
|
-
onkeydown={(e) => e.key === 'Enter' && handleGrayClick(g.step, g.index)}
|
|
736
|
-
>
|
|
737
|
-
{#if g.key in overrides}
|
|
738
|
-
<span class="override-dot" title="Palette override"></span>
|
|
739
|
-
{/if}
|
|
740
|
-
</div>
|
|
741
|
-
<button
|
|
742
|
-
class="step-hex"
|
|
743
|
-
class:copied={copiedKey === g.key}
|
|
744
|
-
type="button"
|
|
745
|
-
onclick={(e) => copyHex(g.key, g.effective, e)}
|
|
746
|
-
>{copiedKey === g.key ? 'copied!' : g.effective}</button>
|
|
747
|
-
</div>
|
|
748
|
-
{/each}
|
|
749
|
-
<div class="step-column">
|
|
750
|
-
<button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-black'} type="button" onclick={(e) => copyVarName('gray-black', `--color-${cssNamespace}-black`, e)}>
|
|
751
|
-
{copiedLabelKey === 'gray-black' ? 'copied!' : 'black'}
|
|
752
|
-
</button>
|
|
753
|
-
<div class="swatch gray-swatch bookend" style="background: #000000"></div>
|
|
754
|
-
</div>
|
|
755
|
-
{#if grayEditorOpen}
|
|
756
|
-
<div class="curve-grid-span" style="grid-column: 2 / {graySteps.length + 2}">
|
|
757
|
-
<ScaleCurveEditor
|
|
758
|
-
curveKey="gray-lightness"
|
|
759
|
-
anchors={grayLightnessCurve}
|
|
760
|
-
cfg={lightnessCurveConfig}
|
|
761
|
-
stepCount={graySteps.length}
|
|
762
|
-
defaults={DEFAULT_GRAY_LIGHTNESS()}
|
|
763
|
-
offset={curveOffset['gray-lightness'] ?? 0}
|
|
764
|
-
onAnchorsChange={setGrayLightnessCurve}
|
|
765
|
-
onOffsetChange={handleOffset}
|
|
766
|
-
/>
|
|
767
|
-
<ScaleCurveEditor
|
|
768
|
-
curveKey="gray-saturation"
|
|
769
|
-
anchors={graySaturationCurve}
|
|
770
|
-
cfg={saturationCurveConfig}
|
|
771
|
-
stepCount={graySteps.length}
|
|
772
|
-
defaults={DEFAULT_GRAY_SATURATION()}
|
|
773
|
-
offset={curveOffset['gray-saturation'] ?? 0}
|
|
774
|
-
onAnchorsChange={setGraySaturationCurve}
|
|
775
|
-
onOffsetChange={handleOffset}
|
|
776
|
-
/>
|
|
777
|
-
</div>
|
|
778
|
-
{/if}
|
|
779
|
-
</div>
|
|
780
|
-
</div>
|
|
781
|
-
</div>
|
|
782
|
-
{/if}
|
|
783
|
-
|
|
784
626
|
{#snippet overridesPanel(scale: Scale, canSnap: boolean, derivedHexFor: (step: Step, scaleTitle: string) => string)}
|
|
785
627
|
<OverridesPanel
|
|
786
628
|
{scale}
|
|
@@ -821,31 +663,28 @@
|
|
|
821
663
|
</button>
|
|
822
664
|
|
|
823
665
|
{#if showDerived}
|
|
824
|
-
{@const activeScales = mode === 'gray' ? grayScales : scales}
|
|
825
|
-
{@const derivedHexFor = mode === 'gray' ? derivedHexForGray : derivedHexForBase}
|
|
826
666
|
<div class="scales-row">
|
|
827
|
-
{#each
|
|
828
|
-
{@render overridesPanel(scale, true,
|
|
667
|
+
{#each scales.filter(s => s.isText) as scale}
|
|
668
|
+
{@render overridesPanel(scale, true, derivedHexForBase)}
|
|
829
669
|
{/each}
|
|
830
670
|
</div>
|
|
831
671
|
<div class="scales-row">
|
|
832
|
-
{#each
|
|
833
|
-
{@render overridesPanel(scale,
|
|
672
|
+
{#each scales.filter(s => !s.isText) as scale}
|
|
673
|
+
{@render overridesPanel(scale, true, derivedHexForBase)}
|
|
834
674
|
{/each}
|
|
835
675
|
</div>
|
|
836
676
|
{/if}
|
|
837
677
|
|
|
838
678
|
<!-- Color Edit Panel (non-base edits) -->
|
|
839
679
|
{#if !isEditingBase && panelOpen && editingColor}
|
|
680
|
+
{@const oc = hexToOklch(editingColor)}
|
|
840
681
|
<ColorEditPanel
|
|
841
|
-
color={editingColor}
|
|
842
682
|
title={editPanelTitle}
|
|
843
683
|
showRemoveOverride={!!editingKey}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
onHueChromaChange={(h, c) =>
|
|
848
|
-
onColorChange={handleColorChange}
|
|
684
|
+
hue={oc.h}
|
|
685
|
+
chroma={oc.c}
|
|
686
|
+
lightness={oc.l * 100}
|
|
687
|
+
onHueChromaChange={(h, c, l) => handleColorChange(oklchHex(h, c, l))}
|
|
849
688
|
onConfirm={confirmEdit}
|
|
850
689
|
onCancel={cancelEdit}
|
|
851
690
|
onRemoveOverride={() => editingKey && removeOverride(editingKey)}
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
<PaletteEditor label="Brand" initialColor="#c93636" cssNamespace="brand" />
|
|
53
53
|
<PaletteEditor label="Accent" initialColor="#f49e0b" cssNamespace="accent" />
|
|
54
54
|
<PaletteEditor label="Background" initialColor="#1a1a2e" cssNamespace="canvas" emptySelector />
|
|
55
|
-
<PaletteEditor
|
|
56
|
-
<PaletteEditor
|
|
55
|
+
<PaletteEditor neutral label="Neutral" initialColor="#70787e" cssNamespace="neutral"/>
|
|
56
|
+
<PaletteEditor neutral label="Alternate" displayLabel="Alternate (neutral)" initialColor="#817b78" cssNamespace="alternate" />
|
|
57
57
|
<PaletteEditor label="Special" initialColor="#8b5cf6" cssNamespace="special" />
|
|
58
58
|
<PaletteEditor label="Info" initialColor="#3077e8" cssNamespace="info" />
|
|
59
59
|
<PaletteEditor label="Success" initialColor="#21c45d" cssNamespace="success" />
|
|
@@ -2,28 +2,31 @@
|
|
|
2
2
|
import ColorEditPanel from '../ColorEditPanel.svelte';
|
|
3
3
|
import Toggle from '../Toggle.svelte';
|
|
4
4
|
import { beginSliderGesture } from '../../core/store/editorStore';
|
|
5
|
+
import { hexToOklch } from '../../core/palettes/oklch';
|
|
6
|
+
|
|
7
|
+
// Full sRGB chroma range (gamutClamp trims per hue/lightness). Neutrals default
|
|
8
|
+
// low but are not capped; their calm character comes from defaults, not a ceiling.
|
|
9
|
+
const CHROMA_MAX = 0.4;
|
|
10
|
+
// Where a typical neutral's chroma sits, marked on the slider as a soft nudge.
|
|
11
|
+
const NEUTRAL_CALM_CHROMA = 0.05;
|
|
5
12
|
|
|
6
13
|
|
|
7
14
|
|
|
8
15
|
|
|
9
16
|
interface Props {
|
|
10
17
|
/**
|
|
11
|
-
* The header swatch + label + base-hex + (when active) the ColorEditPanel
|
|
12
|
-
* for editing the palette's base colour.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
18
|
+
* The header swatch + label + base-hex + (when active) the OKLCH ColorEditPanel
|
|
19
|
+
* for editing the palette's base colour. The picker edits hue/chroma/lightness
|
|
20
|
+
* and maps them straight to the base hex. `neutral` only nudges the picker
|
|
21
|
+
* (a calm-chroma hint on the slider); it does not change behaviour.
|
|
15
22
|
*
|
|
16
23
|
* State (`editing`, scope handle) is owned by the parent — this component
|
|
17
|
-
* fires callbacks (`onStartEdit`, `onConfirm`, `onCancel`, etc.).
|
|
18
|
-
* parent decides whether to apply the chromatic-vs-gray snapshot dance.
|
|
24
|
+
* fires callbacks (`onStartEdit`, `onConfirm`, `onCancel`, etc.).
|
|
19
25
|
*/
|
|
20
26
|
label: string;
|
|
21
27
|
displayLabel?: string | null;
|
|
22
|
-
|
|
28
|
+
neutral?: boolean;
|
|
23
29
|
baseColor: string;
|
|
24
|
-
gray500Hex: string;
|
|
25
|
-
tintHue: number;
|
|
26
|
-
tintChroma: number;
|
|
27
30
|
anchorToBase: boolean;
|
|
28
31
|
isEditingBase: boolean;
|
|
29
32
|
panelOpen: boolean;
|
|
@@ -33,8 +36,7 @@
|
|
|
33
36
|
onStartEdit: () => void;
|
|
34
37
|
onConfirm: () => void;
|
|
35
38
|
onCancel: () => void;
|
|
36
|
-
|
|
37
|
-
onTintChange: (hue: number, chroma: number) => void;
|
|
39
|
+
onBaseChange: (hue: number, chroma: number, lightness: number) => void;
|
|
38
40
|
onAnchorToBaseChange: (next: boolean) => void;
|
|
39
41
|
onCopyBaseHex: (key: string, hex: string, event?: MouseEvent) => void;
|
|
40
42
|
}
|
|
@@ -42,11 +44,8 @@
|
|
|
42
44
|
let {
|
|
43
45
|
label,
|
|
44
46
|
displayLabel = null,
|
|
45
|
-
|
|
47
|
+
neutral = false,
|
|
46
48
|
baseColor,
|
|
47
|
-
gray500Hex,
|
|
48
|
-
tintHue,
|
|
49
|
-
tintChroma,
|
|
50
49
|
anchorToBase,
|
|
51
50
|
isEditingBase,
|
|
52
51
|
panelOpen,
|
|
@@ -56,14 +55,13 @@
|
|
|
56
55
|
onStartEdit,
|
|
57
56
|
onConfirm,
|
|
58
57
|
onCancel,
|
|
59
|
-
|
|
60
|
-
onTintChange,
|
|
58
|
+
onBaseChange,
|
|
61
59
|
onAnchorToBaseChange,
|
|
62
60
|
onCopyBaseHex
|
|
63
61
|
}: Props = $props();
|
|
64
62
|
|
|
65
|
-
let
|
|
66
|
-
let
|
|
63
|
+
let baseOklch = $derived(hexToOklch(baseColor));
|
|
64
|
+
let pickerChromaHint = $derived(neutral ? NEUTRAL_CALM_CHROMA : undefined);
|
|
67
65
|
</script>
|
|
68
66
|
|
|
69
67
|
<div class="editor-top">
|
|
@@ -72,7 +70,7 @@
|
|
|
72
70
|
<div
|
|
73
71
|
class="header-swatch"
|
|
74
72
|
class:active={isEditingBase}
|
|
75
|
-
style="background: {
|
|
73
|
+
style="background: {baseColor}"
|
|
76
74
|
onclick={onStartEdit}
|
|
77
75
|
role="button"
|
|
78
76
|
tabindex="0"
|
|
@@ -83,32 +81,30 @@
|
|
|
83
81
|
<button
|
|
84
82
|
class="base-hex clickable-hex"
|
|
85
83
|
type="button"
|
|
86
|
-
onclick={(e) => onCopyBaseHex(
|
|
87
|
-
>{copiedKey ===
|
|
84
|
+
onclick={(e) => onCopyBaseHex('__base__', baseColor, e)}
|
|
85
|
+
>{copiedKey === '__base__' ? 'copied!' : baseColor}</button>
|
|
88
86
|
</div>
|
|
89
87
|
</div>
|
|
90
88
|
</div>
|
|
91
89
|
|
|
92
90
|
{#if isEditingBase && panelOpen && editingColor}
|
|
93
91
|
<ColorEditPanel
|
|
94
|
-
color={editingColor}
|
|
95
92
|
title={editPanelTitle}
|
|
96
93
|
showRemoveOverride={false}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
hue={baseOklch.h}
|
|
95
|
+
chroma={baseOklch.c}
|
|
96
|
+
lightness={baseOklch.l * 100}
|
|
97
|
+
chromaMax={CHROMA_MAX}
|
|
98
|
+
chromaHint={pickerChromaHint}
|
|
99
|
+
onHueChromaChange={onBaseChange}
|
|
102
100
|
onConfirm={onConfirm}
|
|
103
101
|
onCancel={onCancel}
|
|
104
102
|
onRemoveOverride={() => {}}
|
|
105
103
|
onSliderStart={() => beginSliderGesture(`edit ${label} base`)}
|
|
106
104
|
>
|
|
107
105
|
{#snippet actions()}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
</span>
|
|
111
|
-
{/snippet}
|
|
106
|
+
<Toggle checked={anchorToBase} onchange={(v) => onAnchorToBaseChange(v ?? !anchorToBase)} label="Lock base color to position 500" />
|
|
107
|
+
{/snippet}
|
|
112
108
|
</ColorEditPanel>
|
|
113
109
|
{/if}
|
|
114
110
|
|
|
@@ -183,10 +179,6 @@
|
|
|
183
179
|
color: var(--ui-text-primary);
|
|
184
180
|
}
|
|
185
181
|
|
|
186
|
-
.hidden {
|
|
187
|
-
display: none;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
182
|
@media (max-width: 1280px) {
|
|
191
183
|
.header-swatch {
|
|
192
184
|
width: 3rem;
|
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Editing-state machine for the PaletteEditor.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* discriminated union. The type-checker now enforces field validity per
|
|
7
|
-
* mode — e.g. `snapshotTintHue` only exists when `kind === 'editingBase'`,
|
|
8
|
-
* making it impossible to leak the gray-mode tint snapshot into a
|
|
9
|
-
* step-edit cancel path.
|
|
4
|
+
* A single discriminated union replaces several independent `let` decls so
|
|
5
|
+
* the type-checker enforces field validity per mode.
|
|
10
6
|
*
|
|
11
7
|
* Modes:
|
|
12
8
|
* - `idle` — no edit panel open.
|
|
13
|
-
* - `editingBase` — header swatch is active; the user is dragging the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* live working colour (drives the swatch preview before commit).
|
|
9
|
+
* - `editingBase` — header swatch is active; the user is dragging the base
|
|
10
|
+
* hex. No payload: the swatch renders from `baseColor` and cancel restores
|
|
11
|
+
* via the session scope.
|
|
12
|
+
* - `editingStep` — a non-base swatch is active (palette step / derived-scale
|
|
13
|
+
* step / override slot). `stepKey` is the dot-keyed identifier; `snapshot`
|
|
14
|
+
* is the value at panel-open; `draft` is the live working colour (drives
|
|
15
|
+
* the swatch preview before commit).
|
|
21
16
|
*
|
|
22
17
|
* Locked-anchor state (`lockedLightnessIdx`, `lockedSaturationIdx`) and
|
|
23
18
|
* the per-toggle injected flags (`injectedLightness`, `injectedSaturation`)
|
|
24
19
|
* remain separate `let` decls in the parent because they are anchor-
|
|
25
20
|
* management state, not edit-session state — they persist across
|
|
26
21
|
* idle/edit transitions and have their own lifecycle tied to
|
|
27
|
-
* `setAnchorToBase()`.
|
|
28
|
-
* `editingScale` sketch) would conflate two orthogonal axes; the audit's
|
|
29
|
-
* "scale" mode does not correspond to a user-visible state since curve
|
|
30
|
-
* edits don't open a panel.
|
|
22
|
+
* `setAnchorToBase()`.
|
|
31
23
|
*/
|
|
32
24
|
|
|
33
25
|
import type { Scope } from '../../core/store/editorStore';
|
|
@@ -37,12 +29,7 @@ export const BASE_KEY = '__base__';
|
|
|
37
29
|
|
|
38
30
|
export type EditingState =
|
|
39
31
|
| { kind: 'idle' }
|
|
40
|
-
| {
|
|
41
|
-
kind: 'editingBase';
|
|
42
|
-
snapshotHex: string;
|
|
43
|
-
snapshotTintHue: number | null;
|
|
44
|
-
snapshotTintChroma: number | null;
|
|
45
|
-
}
|
|
32
|
+
| { kind: 'editingBase' }
|
|
46
33
|
| {
|
|
47
34
|
kind: 'editingStep';
|
|
48
35
|
stepKey: string;
|
|
@@ -75,7 +62,7 @@ export function activeKey(s: EditingState): string | null {
|
|
|
75
62
|
return s.stepKey;
|
|
76
63
|
}
|
|
77
64
|
|
|
78
|
-
/** The current draft hex (for editingStep) or null.
|
|
65
|
+
/** The current draft hex (for editingStep) or null. editingBase draws from baseColor directly. */
|
|
79
66
|
export function activeDraft(s: EditingState): string | null {
|
|
80
67
|
return s.kind === 'editingStep' ? s.draft : null;
|
|
81
68
|
}
|