@motion-proto/live-tokens 0.40.1 → 0.41.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/CHANGELOG.md +105 -0
- package/dist-plugin/{chunk-D77VD4Z6.js → chunk-H4TRUINI.js} +102 -13
- package/dist-plugin/index.cjs +288 -67
- package/dist-plugin/index.js +171 -55
- package/dist-plugin/tokensCssMigrations/index.cjs +102 -13
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +3 -2
- package/src/app/site.css +97 -20
- package/src/editor/core/palettes/colorHarmony.ts +228 -0
- package/src/editor/core/palettes/contrast.ts +128 -0
- package/src/editor/core/palettes/oklch.ts +8 -0
- package/src/editor/core/palettes/paletteDerivation.ts +267 -72
- package/src/editor/core/palettes/recommendText.ts +216 -0
- package/src/editor/core/palettes/solveTextContrast.ts +251 -0
- package/src/editor/core/store/editorCore.ts +2 -1
- package/src/editor/core/store/editorPersistence.ts +18 -1
- package/src/editor/core/store/editorStore.ts +20 -1
- package/src/editor/core/store/editorTypes.ts +4 -0
- package/src/editor/core/store/editorViewStore.ts +7 -3
- package/src/editor/core/themes/migrations/2026-06-05-palette-unification.ts +11 -9
- package/src/editor/core/themes/migrations/2026-07-21-palette-oklch-basis.ts +51 -0
- package/src/editor/core/themes/migrations/2026-07-24-base-anchor-placement.ts +37 -0
- package/src/editor/core/themes/migrations/2026-07-25-background-spot-to-base.ts +40 -0
- package/src/editor/core/themes/themeTypes.ts +23 -3
- package/src/editor/docs/CodeBlock.svelte +1 -1
- package/src/editor/docs/Docs.svelte +30 -13
- package/src/editor/pages/EditorShell.svelte +31 -3
- package/src/editor/ui/ColorEditPanel.svelte +22 -16
- package/src/editor/ui/EditorViewSwitcher.svelte +43 -9
- package/src/editor/ui/FontStackEditor.svelte +0 -1
- package/src/editor/ui/PaletteEditor.svelte +117 -155
- package/src/editor/ui/ProjectFontsSection.svelte +0 -3
- package/src/editor/ui/TextStylesSection.svelte +173 -0
- package/src/editor/ui/UILineHeightSelector.svelte +6 -5
- package/src/editor/ui/UIPaletteSelector.svelte +6 -1
- package/src/editor/ui/UITextTransformSelector.svelte +106 -0
- package/src/editor/ui/VariablesTab.svelte +23 -10
- package/src/editor/ui/colors/ColorReadouts.svelte +123 -0
- package/src/editor/ui/colors/ColorStory.svelte +375 -0
- package/src/editor/ui/colors/ColorWheel.svelte +740 -0
- package/src/editor/ui/colors/ColorsTab.svelte +508 -0
- package/src/editor/ui/colors/HarmonyAxesList.svelte +320 -0
- package/src/editor/ui/colors/LightnessBar.svelte +261 -0
- package/src/editor/ui/colors/PaletteStepStrip.svelte +142 -0
- package/src/editor/ui/colors/colorWheelMath.ts +9 -0
- package/src/editor/ui/colors/harmony-icons/analogous.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/complementary.svg +10 -0
- package/src/editor/ui/colors/harmony-icons/compound.svg +15 -0
- package/src/editor/ui/colors/harmony-icons/custom.svg +10 -0
- package/src/editor/ui/colors/harmony-icons/monochromatic.svg +11 -0
- package/src/editor/ui/colors/harmony-icons/shades.svg +9 -0
- package/src/editor/ui/colors/harmony-icons/split-complementary.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/square.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/tetradic.svg +15 -0
- package/src/editor/ui/colors/harmony-icons/triadic.svg +13 -0
- package/src/editor/ui/colors/harmonyModeIcons.ts +23 -0
- package/src/editor/ui/colors/paletteBaseColor.ts +214 -0
- package/src/editor/ui/index.ts +0 -1
- package/src/editor/ui/palette/OverridesPanel.svelte +6 -3
- package/src/editor/ui/palette/PaletteBase.svelte +16 -8
- package/src/editor/ui/palette/dockMagnify.ts +22 -0
- package/src/editor/ui/palette/paletteEditorState.ts +5 -4
- package/src/editor/ui/palette/paletteMath.ts +93 -181
- package/src/editor/ui/sections/textStyles.ts +82 -0
- package/src/editor/ui/sections/tokenScales.ts +3 -3
- package/src/live-tokens/data/themes/default.json +3 -3
- package/src/live-tokens/data/tokens.generated.css +96 -95
- package/src/system/components/Badge.svelte +11 -11
- package/src/system/components/Button.svelte +7 -7
- package/src/system/components/Callout.svelte +8 -8
- package/src/system/components/Card.svelte +2 -2
- package/src/system/components/CodeSnippet.svelte +1 -1
- package/src/system/components/CollapsibleSection.svelte +7 -7
- package/src/system/components/CornerBadge.svelte +1 -1
- package/src/system/components/Dialog.svelte +2 -2
- package/src/system/components/FloatingTokenTags.css +5 -4
- package/src/system/components/Input.svelte +6 -6
- package/src/system/components/MenuSelect.svelte +4 -4
- package/src/system/components/Notification.svelte +8 -8
- package/src/system/components/ProgressBar.svelte +2 -2
- package/src/system/components/RadioButton.svelte +4 -4
- package/src/system/components/SectionDivider.svelte +6 -6
- package/src/system/components/SegmentedControl.svelte +5 -5
- package/src/system/components/SideNavigation.svelte +12 -12
- package/src/system/components/TabBar.svelte +4 -4
- package/src/system/components/Table.svelte +2 -2
- package/src/system/components/Tooltip.svelte +1 -1
- package/src/system/styles/tokens.css +59 -6
- package/src/editor/ui/TextTab.svelte +0 -203
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const bubble = createBubbler();
|
|
5
|
-
import { onMount, onDestroy, tick, untrack } from 'svelte';
|
|
6
|
-
import { hexToOklch, oklchToHex, gamutClamp } from '../core/palettes/oklch';
|
|
2
|
+
import { onMount, onDestroy, tick } from 'svelte';
|
|
3
|
+
import { oklchToHexClamped, type Oklch } from '../core/palettes/oklch';
|
|
7
4
|
import { type CurveAnchor, lightnessCurveConfig, saturationCurveConfig } from './curveEngine';
|
|
8
5
|
import ColorEditPanel from './ColorEditPanel.svelte';
|
|
9
6
|
import OverridesPanel from './palette/OverridesPanel.svelte';
|
|
@@ -12,6 +9,7 @@
|
|
|
12
9
|
import ScaleCurveEditor from './palette/ScaleCurveEditor.svelte';
|
|
13
10
|
import PaletteBase from './palette/PaletteBase.svelte';
|
|
14
11
|
import { type EditingState, idleState, BASE_KEY, isEditingBase as isBaseEdit } from './palette/paletteEditorState';
|
|
12
|
+
import { dockTrackTemplate } from './palette/dockMagnify';
|
|
15
13
|
import {
|
|
16
14
|
type Step, type Scale,
|
|
17
15
|
GRAY_FALLBACK,
|
|
@@ -20,19 +18,22 @@
|
|
|
20
18
|
paletteStepLightness, scales,
|
|
21
19
|
paletteStepKey, stepKey, scaleCurveKey as getScaleCurveKey,
|
|
22
20
|
stepIndexToX,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
syncBaseAnchor, clearBaseAnchor,
|
|
22
|
+
computePaletteOklch as computePaletteOklchPure,
|
|
23
|
+
computeDerivedOklch as computeDerivedOklchPure,
|
|
26
24
|
snapScaleToPalette as snapScaleToPalettePure,
|
|
27
25
|
} from './palette/paletteMath';
|
|
28
26
|
import type { PaletteConfig, GradientStop } from '../core/themes/themeTypes';
|
|
29
27
|
import { editorState, mutate, setPaletteConfig, beginSliderGesture, beginScope, commitScope, cancelScope, type Scope } from '../core/store/editorStore';
|
|
28
|
+
// Base-color edits route through the shared setter so a bound harmony axis
|
|
29
|
+
// follows the hue (invariant 1); the local `edit` would silently detach it.
|
|
30
|
+
import { setBaseColor } from './colors/paletteBaseColor';
|
|
30
31
|
import { showCopyPopover } from './copyPopover';
|
|
31
32
|
|
|
32
33
|
interface Props {
|
|
33
34
|
label: string;
|
|
34
35
|
displayLabel?: string | null;
|
|
35
|
-
initialColor?:
|
|
36
|
+
initialColor?: Oklch;
|
|
36
37
|
neutral?: boolean;
|
|
37
38
|
cssNamespace?: string | null;
|
|
38
39
|
emptySelector?: boolean;
|
|
@@ -47,6 +48,8 @@
|
|
|
47
48
|
emptySelector = false
|
|
48
49
|
}: Props = $props();
|
|
49
50
|
|
|
51
|
+
const toHex = (o: Oklch): string => oklchToHexClamped(o.l, o.c, o.h);
|
|
52
|
+
|
|
50
53
|
const seedConfig = () => defaultPaletteConfig({ baseColor: initialColor, neutral });
|
|
51
54
|
|
|
52
55
|
function edit<K extends keyof PaletteConfig>(field: K, value: PaletteConfig[K]): void {
|
|
@@ -64,15 +67,15 @@
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
let lockedLightnessIdx: number | null = $derived.by(() => {
|
|
67
|
-
if (!anchorToBase) return null;
|
|
68
|
-
const
|
|
69
|
-
const idx = lightnessCurve.findIndex(a => Math.abs(a.x -
|
|
70
|
+
if (!anchorToBase || anchorPlacement === undefined) return null;
|
|
71
|
+
const x = stepIndexToX(anchorPlacement.step);
|
|
72
|
+
const idx = lightnessCurve.findIndex(a => Math.abs(a.x - x) < 0.5);
|
|
70
73
|
return idx >= 0 ? idx : null;
|
|
71
74
|
});
|
|
72
75
|
let lockedSaturationIdx: number | null = $derived.by(() => {
|
|
73
|
-
if (!anchorToBase) return null;
|
|
74
|
-
const
|
|
75
|
-
const idx = saturationCurve.findIndex(a => Math.abs(a.x -
|
|
76
|
+
if (!anchorToBase || anchorPlacement === undefined) return null;
|
|
77
|
+
const x = stepIndexToX(anchorPlacement.step);
|
|
78
|
+
const idx = saturationCurve.findIndex(a => Math.abs(a.x - x) < 0.5);
|
|
76
79
|
return idx >= 0 ? idx : null;
|
|
77
80
|
});
|
|
78
81
|
|
|
@@ -105,55 +108,32 @@
|
|
|
105
108
|
|
|
106
109
|
let editing: EditingState = $state(idleState);
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
function computePaletteColor(index: number, base: string): string {
|
|
112
|
-
return computePaletteColorPure(index, base, lightnessCurve, saturationCurve, curveOffset);
|
|
111
|
+
function computePaletteOklch(index: number, base: Oklch): Oklch {
|
|
112
|
+
return computePaletteOklchPure(index, base, lightnessCurve, saturationCurve, curveOffset);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function
|
|
116
|
-
return
|
|
115
|
+
function computeDerivedOklch(step: Step, base: Oklch, scaleTitle: string): Oklch {
|
|
116
|
+
return computeDerivedOklchPure(step, base, scaleTitle, scaleCurves, curveOffset);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* thing. Transient `injectedLightness` / `injectedSaturation` remember
|
|
123
|
-
* whether we created the anchor (vs it already existed) so toggle-off
|
|
124
|
-
* doesn't destroy a user-authored anchor. Not persisted — acceptable drift
|
|
125
|
-
* on reload is that a pre-existing anchor would be preserved on toggle-off.
|
|
126
|
-
*/
|
|
119
|
+
// Toggle placement atomically with the flag flip so one undo reverses the
|
|
120
|
+
// whole thing. The anchor's lifecycle (placement, displaced-y restore)
|
|
121
|
+
// lives in syncBaseAnchor/clearBaseAnchor, shared with the store setters.
|
|
127
122
|
function setAnchorToBase(next: boolean) {
|
|
128
123
|
if (next === anchorToBase) return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}, 'anchor on');
|
|
140
|
-
} else {
|
|
141
|
-
const lCurr = lightnessCurve;
|
|
142
|
-
const sCurr = saturationCurve;
|
|
143
|
-
const nextL = injectedLightness ? removeLockedAnchor(lCurr, lockedLightnessIdx) : lCurr;
|
|
144
|
-
const nextS = injectedSaturation ? removeLockedAnchor(sCurr, lockedSaturationIdx) : sCurr;
|
|
145
|
-
injectedLightness = false;
|
|
146
|
-
injectedSaturation = false;
|
|
147
|
-
patchPalette({
|
|
148
|
-
anchorToBase: false,
|
|
149
|
-
lightnessCurve: nextL,
|
|
150
|
-
saturationCurve: nextS,
|
|
151
|
-
}, 'anchor off');
|
|
152
|
-
}
|
|
124
|
+
mutate(`${label}: ${next ? 'anchor on' : 'anchor off'}`, (s) => {
|
|
125
|
+
if (!s.palettes[label]) s.palettes[label] = seedConfig();
|
|
126
|
+
const cfg = s.palettes[label];
|
|
127
|
+
if (next) {
|
|
128
|
+
cfg.anchorToBase = true;
|
|
129
|
+
syncBaseAnchor(cfg);
|
|
130
|
+
} else {
|
|
131
|
+
clearBaseAnchor(cfg);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
153
134
|
}
|
|
154
135
|
|
|
155
136
|
|
|
156
|
-
|
|
157
137
|
function startBaseEdit() {
|
|
158
138
|
if (editing.kind === 'editingBase') { confirmEdit(); return; }
|
|
159
139
|
editing = { kind: 'editingBase' };
|
|
@@ -166,7 +146,7 @@
|
|
|
166
146
|
confirmEdit();
|
|
167
147
|
return;
|
|
168
148
|
}
|
|
169
|
-
const current = (k in overrides) ? overrides[k] :
|
|
149
|
+
const current = (k in overrides) ? overrides[k] : computePaletteOklch(ps.index, baseColor);
|
|
170
150
|
editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
|
|
171
151
|
openSession();
|
|
172
152
|
}
|
|
@@ -183,23 +163,18 @@
|
|
|
183
163
|
edit('scaleCurves', { ...scaleCurves, [title]: { ...cur, [channel]: a } });
|
|
184
164
|
}
|
|
185
165
|
|
|
186
|
-
function handleColorChange(
|
|
166
|
+
function handleColorChange(color: Oklch) {
|
|
187
167
|
if (editing.kind === 'editingStep') {
|
|
188
|
-
editing = { ...editing, draft:
|
|
168
|
+
editing = { ...editing, draft: color };
|
|
189
169
|
// Write every drag tick to the store (not just pre-existing overrides) so
|
|
190
170
|
// the live page and the derived-scale preview swatches update in realtime.
|
|
191
171
|
// The open session collapses these to one undo entry; cancel restores the
|
|
192
172
|
// snapshot, and confirmEdit drops the override if the draft equals the
|
|
193
173
|
// computed value, so a no-op edit leaves nothing behind.
|
|
194
|
-
edit('overrides', { ...overrides, [editing.stepKey]:
|
|
174
|
+
edit('overrides', { ...overrides, [editing.stepKey]: color });
|
|
195
175
|
}
|
|
196
176
|
}
|
|
197
177
|
|
|
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
|
-
|
|
203
178
|
function resetOverride(k: string) {
|
|
204
179
|
if (!(k in overrides)) return;
|
|
205
180
|
const { [k]: _, ...rest } = overrides;
|
|
@@ -211,7 +186,7 @@
|
|
|
211
186
|
confirmEdit();
|
|
212
187
|
return;
|
|
213
188
|
}
|
|
214
|
-
const current = (k in overrides) ? overrides[k] :
|
|
189
|
+
const current = (k in overrides) ? overrides[k] : computeDerivedOklch(step, baseColor, scaleTitle);
|
|
215
190
|
editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
|
|
216
191
|
openSession();
|
|
217
192
|
}
|
|
@@ -223,9 +198,13 @@
|
|
|
223
198
|
let nextOverrides = { ...overrides };
|
|
224
199
|
if (editingDraft !== null) {
|
|
225
200
|
const computed = computedValueForKey(editingKey);
|
|
226
|
-
|
|
201
|
+
// Compare via the sRGB projection: an override that renders identically
|
|
202
|
+
// to the computed default is dropped (same rule as the pre-Wave-2 hex
|
|
203
|
+
// compare); the stored value stays the lossless numeric draft.
|
|
204
|
+
const draftHex = toHex(editingDraft);
|
|
205
|
+
if (computed !== null && draftHex !== toHex(computed)) {
|
|
227
206
|
nextOverrides[editingKey] = editingDraft;
|
|
228
|
-
} else if (computed !== null && editingKey in nextOverrides &&
|
|
207
|
+
} else if (computed !== null && editingKey in nextOverrides && draftHex === toHex(computed)) {
|
|
229
208
|
delete nextOverrides[editingKey];
|
|
230
209
|
}
|
|
231
210
|
}
|
|
@@ -258,16 +237,16 @@
|
|
|
258
237
|
if (editingKey === k) { editing = idleState; }
|
|
259
238
|
}
|
|
260
239
|
|
|
261
|
-
function computedValueForKey(key: string):
|
|
240
|
+
function computedValueForKey(key: string): Oklch | null {
|
|
262
241
|
const ps = paletteStepLightness.find(p => paletteStepKey(p.label) === key);
|
|
263
242
|
if (ps) {
|
|
264
243
|
const idx = paletteStepLightness.indexOf(ps);
|
|
265
|
-
return
|
|
244
|
+
return computePaletteOklch(idx, baseColor);
|
|
266
245
|
}
|
|
267
246
|
for (const scale of scales) {
|
|
268
247
|
for (const step of scale.steps) {
|
|
269
248
|
if (stepKey(scale.title, step.name) === key) {
|
|
270
|
-
return
|
|
249
|
+
return computeDerivedOklch(step, baseColor, scale.title);
|
|
271
250
|
}
|
|
272
251
|
}
|
|
273
252
|
}
|
|
@@ -275,8 +254,8 @@
|
|
|
275
254
|
}
|
|
276
255
|
|
|
277
256
|
function effectiveColor(k: string, step: Step, scaleTitle: string, _version?: string): string {
|
|
278
|
-
if (editingKey === k && editingDraft !== null) return editingDraft;
|
|
279
|
-
return (k in overrides) ? overrides[k] :
|
|
257
|
+
if (editingKey === k && editingDraft !== null) return toHex(editingDraft);
|
|
258
|
+
return (k in overrides) ? toHex(overrides[k]) : toHex(computeDerivedOklch(step, baseColor, scaleTitle));
|
|
280
259
|
}
|
|
281
260
|
|
|
282
261
|
let copiedKey: string | null = $state(null);
|
|
@@ -295,7 +274,7 @@
|
|
|
295
274
|
setTimeout(() => { copiedLabelKey = null; }, 1500);
|
|
296
275
|
}
|
|
297
276
|
|
|
298
|
-
function snapScaleToPalette(scale: Scale): Record<string,
|
|
277
|
+
function snapScaleToPalette(scale: Scale): Record<string, Oklch> {
|
|
299
278
|
return snapScaleToPalettePure(scale, baseColor, scaleCurves, curveOffset, paletteComputed);
|
|
300
279
|
}
|
|
301
280
|
|
|
@@ -348,8 +327,8 @@
|
|
|
348
327
|
onMount(() => document.addEventListener('click', handleDocClick, true));
|
|
349
328
|
onDestroy(() => document.removeEventListener('click', handleDocClick, true));
|
|
350
329
|
|
|
351
|
-
function selectSnapValue(k: string,
|
|
352
|
-
edit('overrides', { ...overrides, [k]:
|
|
330
|
+
function selectSnapValue(k: string, value: Oklch, _scaleTitle: string) {
|
|
331
|
+
edit('overrides', { ...overrides, [k]: value });
|
|
353
332
|
snapPickerKey = null;
|
|
354
333
|
}
|
|
355
334
|
|
|
@@ -368,9 +347,12 @@
|
|
|
368
347
|
for (const scale of scales.filter(s => !s.isText)) {
|
|
369
348
|
if (!snappedScales.has(scale.title)) continue;
|
|
370
349
|
const assigned = snapScaleToPalette(scale);
|
|
371
|
-
for (const [k,
|
|
372
|
-
|
|
373
|
-
|
|
350
|
+
for (const [k, color] of Object.entries(assigned)) {
|
|
351
|
+
// Value compare, not reference: the recompute yields fresh objects, so a
|
|
352
|
+
// reference test would always fire and loop the resnap effect.
|
|
353
|
+
const prev = next[k];
|
|
354
|
+
if (!prev || prev.l !== color.l || prev.c !== color.c || prev.h !== color.h) {
|
|
355
|
+
next[k] = color;
|
|
374
356
|
changed = true;
|
|
375
357
|
}
|
|
376
358
|
}
|
|
@@ -394,8 +376,18 @@
|
|
|
394
376
|
let overrides = $derived($editorState.palettes[label]?.overrides ?? {});
|
|
395
377
|
let snappedScales = $derived(new Set($editorState.palettes[label]?.snappedScales ?? []));
|
|
396
378
|
let anchorToBase = $derived($editorState.palettes[label]?.anchorToBase ?? true);
|
|
379
|
+
let anchorPlacement = $derived($editorState.palettes[label]?.anchorPlacement);
|
|
380
|
+
let anchorStepLabel = $derived(anchorToBase && anchorPlacement !== undefined
|
|
381
|
+
? paletteStepLightness[anchorPlacement.step]?.label ?? null
|
|
382
|
+
: null);
|
|
383
|
+
// Dock magnification: the grid transitions between track lists, so dialing
|
|
384
|
+
// lightness reads as swatches opening/closing while the anchor walks the
|
|
385
|
+
// ramp. Column 0 is the white bookend, so step i is column i+1.
|
|
386
|
+
let swatchTemplate = $derived(dockTrackTemplate(
|
|
387
|
+
paletteStepLightness.length + 2,
|
|
388
|
+
anchorToBase && anchorPlacement !== undefined ? anchorPlacement.step + 1 : null,
|
|
389
|
+
));
|
|
397
390
|
let emptyMode = $derived($editorState.palettes[label]?.emptyMode ?? 'solid');
|
|
398
|
-
let emptyStep = $derived($editorState.palettes[label]?.emptyStep ?? '850');
|
|
399
391
|
let gradientStyle = $derived($editorState.palettes[label]?.gradientStyle ?? 'linear');
|
|
400
392
|
let gradientAngle = $derived($editorState.palettes[label]?.gradientAngle ?? 180);
|
|
401
393
|
let gradientReverse = $derived($editorState.palettes[label]?.gradientReverse ?? false);
|
|
@@ -406,35 +398,23 @@
|
|
|
406
398
|
let gradientSize = $derived($editorState.palettes[label]?.gradientSize ?? 'page');
|
|
407
399
|
let editingKey = $derived(editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? BASE_KEY : editing.stepKey);
|
|
408
400
|
let editingDraft = $derived(editing.kind === 'editingStep' ? editing.draft : null);
|
|
409
|
-
// Keep the locked lightness anchor y in sync with baseColor. Idempotent.
|
|
410
|
-
// `lightnessCurve` is read via `untrack` so writing it back via `edit` does
|
|
411
|
-
// not retrigger this effect (Svelte 5 flags the read+write pattern as
|
|
412
|
-
// recursive).
|
|
413
|
-
$effect(() => {
|
|
414
|
-
if (!anchorToBase || lockedLightnessIdx === null || !baseColor) return;
|
|
415
|
-
const targetY = hexToOklch(baseColor).l * 100;
|
|
416
|
-
untrack(() => {
|
|
417
|
-
const idx = lockedLightnessIdx;
|
|
418
|
-
if (idx === null) return;
|
|
419
|
-
const curve = lightnessCurve;
|
|
420
|
-
if (curve[idx] && Math.abs(curve[idx].y - targetY) > 0.01) {
|
|
421
|
-
edit('lightnessCurve', curve.map((a, i) => i === idx ? { ...a, y: targetY } : a));
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
401
|
let paletteComputed = $derived((() => {
|
|
426
|
-
const _bc = baseColor, _lc = lightnessCurve, _sc = saturationCurve, _co = curveOffset, _ed = editingDraft, _ek = editingKey, _ov = overrides, _ab = anchorToBase;
|
|
402
|
+
const _bc = baseColor, _lc = lightnessCurve, _sc = saturationCurve, _co = curveOffset, _ed = editingDraft, _ek = editingKey, _ov = overrides, _ab = anchorToBase, _ap = anchorPlacement;
|
|
427
403
|
return paletteStepLightness.map((ps, index) => {
|
|
428
404
|
const k = paletteStepKey(ps.label);
|
|
429
|
-
const
|
|
430
|
-
const
|
|
405
|
+
const oklch = computePaletteOklch(index, baseColor);
|
|
406
|
+
const effOklch = (_ek === k && _ed !== null) ? _ed : (k in _ov) ? _ov[k] : oklch;
|
|
431
407
|
return {
|
|
432
408
|
label: ps.label,
|
|
433
409
|
lightness: ps.lightness,
|
|
434
410
|
index,
|
|
435
411
|
key: k,
|
|
436
|
-
|
|
437
|
-
effective
|
|
412
|
+
anchored: _ab && _ap?.step === index,
|
|
413
|
+
// `oklch` is the numeric step (feeds snap); `hex`/`effective` are the
|
|
414
|
+
// sRGB projections that paint swatches and copy to the clipboard.
|
|
415
|
+
oklch,
|
|
416
|
+
hex: toHex(oklch),
|
|
417
|
+
effective: toHex(effOklch),
|
|
438
418
|
};
|
|
439
419
|
});
|
|
440
420
|
})());
|
|
@@ -447,9 +427,11 @@
|
|
|
447
427
|
// `editingDraft` is folded in so the effective-hex closure passed to
|
|
448
428
|
// OverridesPanel re-derives on every drag tick (the per-step hex text would
|
|
449
429
|
// otherwise lag the live swatch during an override edit).
|
|
450
|
-
let curveVersion = $derived(JSON.stringify(scaleCurves) + JSON.stringify(curveOffset) + baseColor + (editingDraft
|
|
451
|
-
let derivedHexForBase = $derived((step: Step, scaleTitle: string) =>
|
|
430
|
+
let curveVersion = $derived(JSON.stringify(scaleCurves) + JSON.stringify(curveOffset) + JSON.stringify(baseColor) + JSON.stringify(editingDraft));
|
|
431
|
+
let derivedHexForBase = $derived((step: Step, scaleTitle: string) => toHex(computeDerivedOklch(step, baseColor, scaleTitle)));
|
|
452
432
|
let effectiveHexAny = $derived((k: string, step: Step, scaleTitle: string) => effectiveColor(k, step, scaleTitle, curveVersion));
|
|
433
|
+
// Projected hex view of the numeric overrides for the display-only panels.
|
|
434
|
+
let overridesHex = $derived(Object.fromEntries(Object.entries(overrides).map(([k, v]) => [k, toHex(v)])));
|
|
453
435
|
|
|
454
436
|
let isEditingBase = $derived(isBaseEdit(editing));
|
|
455
437
|
let editingColor = $derived(isEditingBase ? baseColor : editingDraft);
|
|
@@ -484,13 +466,14 @@
|
|
|
484
466
|
});
|
|
485
467
|
</script>
|
|
486
468
|
|
|
487
|
-
<div class="palette-editor" style="--editor-base: {baseColor}">
|
|
469
|
+
<div class="palette-editor" style="--editor-base: {toHex(baseColor)}">
|
|
488
470
|
<PaletteBase
|
|
489
471
|
{label}
|
|
490
472
|
{displayLabel}
|
|
491
473
|
{neutral}
|
|
492
474
|
{baseColor}
|
|
493
475
|
{anchorToBase}
|
|
476
|
+
{anchorStepLabel}
|
|
494
477
|
{isEditingBase}
|
|
495
478
|
{panelOpen}
|
|
496
479
|
{editingColor}
|
|
@@ -499,7 +482,7 @@
|
|
|
499
482
|
onStartEdit={startBaseEdit}
|
|
500
483
|
onConfirm={confirmEdit}
|
|
501
484
|
onCancel={cancelEdit}
|
|
502
|
-
onBaseChange={(h, c, l) =>
|
|
485
|
+
onBaseChange={(h, c, l) => setBaseColor(label, { l: l / 100, c, h })}
|
|
503
486
|
onAnchorToBaseChange={setAnchorToBase}
|
|
504
487
|
onCopyBaseHex={copyHex}
|
|
505
488
|
/>
|
|
@@ -524,7 +507,7 @@
|
|
|
524
507
|
{paletteEditorOpen ? 'Close' : 'Edit'}
|
|
525
508
|
</UIPillButton>
|
|
526
509
|
</div>
|
|
527
|
-
<div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}">
|
|
510
|
+
<div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}; grid-template-columns: {swatchTemplate}">
|
|
528
511
|
<div class="step-column">
|
|
529
512
|
<button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-white'} type="button" onclick={(e) => copyVarName('palette-white', `--color-${cssNamespace}-white`, e)}>
|
|
530
513
|
{copiedLabelKey === 'palette-white' ? 'copied!' : 'white'}
|
|
@@ -541,6 +524,8 @@
|
|
|
541
524
|
class="swatch gray-swatch"
|
|
542
525
|
class:active={editingKey === ps.key}
|
|
543
526
|
class:overridden={ps.key in overrides}
|
|
527
|
+
class:anchored={ps.anchored}
|
|
528
|
+
title={ps.anchored ? 'Base color' : undefined}
|
|
544
529
|
style="background: {ps.effective}"
|
|
545
530
|
onclick={() => handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
|
|
546
531
|
role="button"
|
|
@@ -550,16 +535,6 @@
|
|
|
550
535
|
{#if ps.key in overrides}
|
|
551
536
|
<span class="override-dot" title="Palette override"></span>
|
|
552
537
|
{/if}
|
|
553
|
-
{#if emptySelector && emptyMode === 'solid'}
|
|
554
|
-
<input
|
|
555
|
-
type="checkbox"
|
|
556
|
-
class="empty-check"
|
|
557
|
-
checked={emptyStep === ps.label}
|
|
558
|
-
onclick={stopPropagation(() => edit('emptyStep', ps.label))}
|
|
559
|
-
onkeydown={stopPropagation(bubble('keydown'))}
|
|
560
|
-
title="Page background"
|
|
561
|
-
/>
|
|
562
|
-
{/if}
|
|
563
538
|
</div>
|
|
564
539
|
<button
|
|
565
540
|
class="step-hex"
|
|
@@ -633,7 +608,7 @@
|
|
|
633
608
|
{scaleCurves}
|
|
634
609
|
{curveOffset}
|
|
635
610
|
{defaultScaleCurves}
|
|
636
|
-
{
|
|
611
|
+
overrides={overridesHex}
|
|
637
612
|
{editingKey}
|
|
638
613
|
{snapPickerKey}
|
|
639
614
|
{copiedKey}
|
|
@@ -677,14 +652,13 @@
|
|
|
677
652
|
|
|
678
653
|
<!-- Color Edit Panel (non-base edits) -->
|
|
679
654
|
{#if !isEditingBase && panelOpen && editingColor}
|
|
680
|
-
{@const oc = hexToOklch(editingColor)}
|
|
681
655
|
<ColorEditPanel
|
|
682
656
|
title={editPanelTitle}
|
|
683
657
|
showRemoveOverride={!!editingKey}
|
|
684
|
-
hue={
|
|
685
|
-
chroma={
|
|
686
|
-
lightness={
|
|
687
|
-
onHueChromaChange={(h, c, l) => handleColorChange(
|
|
658
|
+
hue={editingColor.h}
|
|
659
|
+
chroma={editingColor.c}
|
|
660
|
+
lightness={editingColor.l * 100}
|
|
661
|
+
onHueChromaChange={(h, c, l) => handleColorChange({ l: l / 100, c, h })}
|
|
688
662
|
onConfirm={confirmEdit}
|
|
689
663
|
onCancel={cancelEdit}
|
|
690
664
|
onRemoveOverride={() => editingKey && removeOverride(editingKey)}
|
|
@@ -855,6 +829,7 @@
|
|
|
855
829
|
.swatch-grid {
|
|
856
830
|
display: grid;
|
|
857
831
|
grid-template-columns: repeat(var(--swatch-cols), minmax(0, 1fr));
|
|
832
|
+
transition: grid-template-columns var(--ui-transition-fast);
|
|
858
833
|
gap: var(--ui-space-4) var(--swatch-gap, var(--ui-space-4));
|
|
859
834
|
align-items: start;
|
|
860
835
|
justify-content: start;
|
|
@@ -871,11 +846,22 @@
|
|
|
871
846
|
.swatch.gray-swatch {
|
|
872
847
|
width: 100%;
|
|
873
848
|
height: calc(4rem + var(--ui-space-2));
|
|
849
|
+
/* Compensating margin: height + margin-bottom is constant every frame of
|
|
850
|
+
the magnification, so the grid's row height never dips mid-transition
|
|
851
|
+
(the dip bounced everything below the palette). Also keeps the hex row
|
|
852
|
+
aligned across anchored and plain columns. */
|
|
853
|
+
margin-bottom: 1rem;
|
|
854
|
+
transition: height var(--ui-transition-fast), margin-bottom var(--ui-transition-fast);
|
|
874
855
|
cursor: pointer;
|
|
875
856
|
position: relative;
|
|
876
857
|
}
|
|
877
858
|
|
|
878
|
-
|
|
859
|
+
/* Bookends (white/black endpoints) are display-only, not clickable. */
|
|
860
|
+
.swatch.gray-swatch.bookend {
|
|
861
|
+
cursor: default;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.swatch.gray-swatch:not(.bookend):hover {
|
|
879
865
|
border-color: var(--ui-border-high);
|
|
880
866
|
}
|
|
881
867
|
|
|
@@ -885,6 +871,15 @@
|
|
|
885
871
|
outline-offset: 1px;
|
|
886
872
|
}
|
|
887
873
|
|
|
874
|
+
/* The step the base color is placed at: dock-magnified (its grid column is
|
|
875
|
+
also widened via the track template) with an emphasized border. No fill
|
|
876
|
+
change — the swatch itself already IS the picked color. */
|
|
877
|
+
.swatch.gray-swatch.anchored {
|
|
878
|
+
border-color: var(--ui-border-higher);
|
|
879
|
+
height: calc(5rem + var(--ui-space-2));
|
|
880
|
+
margin-bottom: 0;
|
|
881
|
+
}
|
|
882
|
+
|
|
888
883
|
.override-dot {
|
|
889
884
|
position: absolute;
|
|
890
885
|
top: 3px;
|
|
@@ -911,39 +906,6 @@
|
|
|
911
906
|
cursor: pointer;
|
|
912
907
|
}
|
|
913
908
|
|
|
914
|
-
.empty-check {
|
|
915
|
-
-webkit-appearance: none;
|
|
916
|
-
appearance: none;
|
|
917
|
-
position: absolute;
|
|
918
|
-
bottom: 3px;
|
|
919
|
-
right: 3px;
|
|
920
|
-
margin: 0;
|
|
921
|
-
cursor: pointer;
|
|
922
|
-
width: 14px;
|
|
923
|
-
height: 14px;
|
|
924
|
-
background: white;
|
|
925
|
-
border: 1px solid rgba(0, 0, 0, 0.3);
|
|
926
|
-
border-radius: 2px;
|
|
927
|
-
opacity: 0.5;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
.empty-check:checked {
|
|
931
|
-
opacity: 1;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
.empty-check:checked::after {
|
|
935
|
-
content: '\2713';
|
|
936
|
-
display: flex;
|
|
937
|
-
align-items: center;
|
|
938
|
-
justify-content: center;
|
|
939
|
-
width: 100%;
|
|
940
|
-
height: 100%;
|
|
941
|
-
font-size: var(--ui-font-size-md);
|
|
942
|
-
font-weight: bold;
|
|
943
|
-
color: black;
|
|
944
|
-
line-height: 1;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
909
|
/* Narrow desktop: tighten palette editor spacing */
|
|
948
910
|
@media (max-width: 1280px) {
|
|
949
911
|
.palette-editor {
|