@motion-proto/live-tokens 0.37.0 → 0.39.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 +66 -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/SectionDividerEditor.svelte +1 -1
- package/src/editor/core/palettes/paletteDerivation.ts +28 -83
- package/src/editor/core/routing/router.ts +12 -1
- 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/index.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.39.0 — One unified palette model (no gray "mode")
|
|
4
|
+
|
|
5
|
+
### Changed (breaking)
|
|
6
|
+
|
|
7
|
+
- **The chromatic/gray palette split collapses into a single OKLCH (Lightness /
|
|
8
|
+
Chroma / Hue) model.** A neutral is no longer a special "mode"; it is an
|
|
9
|
+
ordinary low-chroma palette with calm defaults (a low but non-zero base chroma,
|
|
10
|
+
a wider neutral lightness ramp). One derivation path (`computePaletteColor`)
|
|
11
|
+
now serves every palette, and the Neutral / Alternate editors show the same
|
|
12
|
+
full L/C/H picker, lock-to-500 toggle, and derived-scale snapping as accents.
|
|
13
|
+
`PaletteConfig` drops its gray vocabulary (`tintHue`, `tintChroma`,
|
|
14
|
+
`grayLightnessCurve`, `graySaturationCurve`); `baseColor`, `lightnessCurve`,
|
|
15
|
+
and `saturationCurve` are now universal, and the internal `mode` prop is
|
|
16
|
+
removed from the palette editors.
|
|
17
|
+
- **Token names are unchanged** — this is a theme-config schema change, not a
|
|
18
|
+
`tokens.css` migration, so no consumer CSS or token references are affected.
|
|
19
|
+
The shipped `default.json` was regenerated; the neutral shift is sub-1-LSB per
|
|
20
|
+
channel and every derived `--surface-*` / `--text-*` / `--color-*` token is
|
|
21
|
+
byte-identical.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A component's surface control no longer vanishes when its fill is a flat
|
|
26
|
+
colour.** `componentGradientSource` returned `undefined` for any non-gradient
|
|
27
|
+
alias and `GradientEditor` renders only `{#if gradient}`, so the surface editor
|
|
28
|
+
disappeared whenever a fill was a plain colour (e.g. SectionDivider's default
|
|
29
|
+
transparent). The editor now synthesizes a none/solid single-stop snapshot from
|
|
30
|
+
a flat (or absent) alias so the type picker always renders, and promotes the
|
|
31
|
+
flat alias to a real gradient on first edit. SectionDivider's section heading is
|
|
32
|
+
renamed "Background" → "Surface" to match Panel and disambiguate it from the
|
|
33
|
+
preview backdrop control.
|
|
34
|
+
- **Palette colour overrides now apply live while you drag.** A new Text /
|
|
35
|
+
Surfaces / Borders override previously reached neither the live page nor the
|
|
36
|
+
preview swatch until commit — `handleColorChange` only wrote to the store when
|
|
37
|
+
the key was already an override. Every drag tick now writes (the open session
|
|
38
|
+
collapses to one undo entry; cancel/confirm clean up no-ops), and the per-step
|
|
39
|
+
hex text tracks the live colour too.
|
|
40
|
+
|
|
41
|
+
### Migration
|
|
42
|
+
|
|
43
|
+
- **Consumer themes migrate automatically on load.** `unifyGrayPalettes` (run in
|
|
44
|
+
`loadFromFile` after `renamePrimaryPaletteKey`) close-maps existing neutrals to
|
|
45
|
+
the unified form: `baseColor` snaps to the effective step-500 colour (preserving
|
|
46
|
+
the subtle tint and the neutral lightness ramp), the saturation curve becomes
|
|
47
|
+
flat-100, and the palette is locked to base. Every palette drops the four
|
|
48
|
+
vestigial gray fields, and the `gray-lightness` / `gray-saturation` curve-offset
|
|
49
|
+
keys fold into `lightness` / `saturation`. Default and flat-saturation neutrals
|
|
50
|
+
are visually identical; only a hand-shaped gray *saturation* curve migrates
|
|
51
|
+
approximately and may want a quick manual retune.
|
|
52
|
+
|
|
53
|
+
## 0.38.0 — Overridable scroll reset for smooth-scroll hosts
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **`setScrollReset(fn)` lets a host route navigation's scroll reset through its
|
|
58
|
+
own scroll system.** On a non-hash `navigate()` the router resets the viewport
|
|
59
|
+
to the top. That default calls `window.scrollTo(0, 0)`, which is invisible to
|
|
60
|
+
consumers driving scroll with a smooth-scroll library (Lenis, Locomotive):
|
|
61
|
+
their scroll position is decoupled from the window, so the rendered page stays
|
|
62
|
+
put — most visibly when `LiveTokensRouter` intercepts an in-page link (e.g. a
|
|
63
|
+
card) and the new page opens mid-scroll instead of at the top. Register a reset
|
|
64
|
+
that drives your provider (`setScrollReset(() => lenis.scrollTo(0, { immediate: true }))`)
|
|
65
|
+
and both the overlay's nav and intercepted links reset correctly. Hash targets
|
|
66
|
+
still skip the reset so in-page anchors are unaffected. Backward compatible —
|
|
67
|
+
unset, the native `window.scrollTo` behavior is unchanged.
|
|
68
|
+
|
|
3
69
|
## 0.37.0 — ImageLightbox `capNatural` accepts a multiple
|
|
4
70
|
|
|
5
71
|
### Added
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -192,16 +192,16 @@ function sampleCurve(anchors, xPos) {
|
|
|
192
192
|
|
|
193
193
|
// src/editor/core/palettes/paletteDerivation.ts
|
|
194
194
|
var PALETTE_SPECS = [
|
|
195
|
-
{ label: "Neutral", cssNamespace: "neutral",
|
|
196
|
-
{ label: "Alternate", cssNamespace: "alternate",
|
|
197
|
-
{ label: "Background", cssNamespace: "canvas",
|
|
198
|
-
{ label: "Brand", cssNamespace: "brand",
|
|
199
|
-
{ label: "Accent", cssNamespace: "accent",
|
|
200
|
-
{ label: "Special", cssNamespace: "special",
|
|
201
|
-
{ label: "Info", cssNamespace: "info",
|
|
202
|
-
{ label: "Success", cssNamespace: "success",
|
|
203
|
-
{ label: "Warning", cssNamespace: "warning",
|
|
204
|
-
{ label: "Danger", cssNamespace: "danger",
|
|
195
|
+
{ label: "Neutral", cssNamespace: "neutral", neutral: true, initialColor: "#70787e" },
|
|
196
|
+
{ label: "Alternate", cssNamespace: "alternate", neutral: true, initialColor: "#817b78" },
|
|
197
|
+
{ label: "Background", cssNamespace: "canvas", emptySelector: true, initialColor: "#1a1a2e" },
|
|
198
|
+
{ label: "Brand", cssNamespace: "brand", initialColor: "#c93636" },
|
|
199
|
+
{ label: "Accent", cssNamespace: "accent", initialColor: "#f49e0b" },
|
|
200
|
+
{ label: "Special", cssNamespace: "special", initialColor: "#8b5cf6" },
|
|
201
|
+
{ label: "Info", cssNamespace: "info", initialColor: "#3077e8" },
|
|
202
|
+
{ label: "Success", cssNamespace: "success", initialColor: "#21c45d" },
|
|
203
|
+
{ label: "Warning", cssNamespace: "warning", initialColor: "#e66e1a" },
|
|
204
|
+
{ label: "Danger", cssNamespace: "danger", initialColor: "#e8304f" }
|
|
205
205
|
];
|
|
206
206
|
var PALETTE_STEPS = [
|
|
207
207
|
{ label: "100" },
|
|
@@ -216,19 +216,6 @@ var PALETTE_STEPS = [
|
|
|
216
216
|
{ label: "900" },
|
|
217
217
|
{ label: "950" }
|
|
218
218
|
];
|
|
219
|
-
var GRAY_STEPS = [
|
|
220
|
-
{ label: "100" },
|
|
221
|
-
{ label: "200" },
|
|
222
|
-
{ label: "300" },
|
|
223
|
-
{ label: "400" },
|
|
224
|
-
{ label: "500" },
|
|
225
|
-
{ label: "600" },
|
|
226
|
-
{ label: "700" },
|
|
227
|
-
{ label: "800" },
|
|
228
|
-
{ label: "850" },
|
|
229
|
-
{ label: "900" },
|
|
230
|
-
{ label: "950" }
|
|
231
|
-
];
|
|
232
219
|
var SCALES = [
|
|
233
220
|
{
|
|
234
221
|
title: "Surfaces",
|
|
@@ -268,9 +255,6 @@ var SCALES = [
|
|
|
268
255
|
];
|
|
269
256
|
var DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
|
|
270
257
|
var DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
|
|
271
|
-
var DEFAULT_GRAY_LIGHTNESS = () => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
|
|
272
|
-
var DEFAULT_GRAY_SATURATION = () => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
|
|
273
|
-
var DEFAULT_TINT_CHROMA = 0.04;
|
|
274
258
|
var defaultScaleCurves = {
|
|
275
259
|
Surfaces: {
|
|
276
260
|
lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
|
|
@@ -288,9 +272,6 @@ var defaultScaleCurves = {
|
|
|
288
272
|
function paletteStepKey(label) {
|
|
289
273
|
return `Palette-${label}`;
|
|
290
274
|
}
|
|
291
|
-
function grayStepKey(label) {
|
|
292
|
-
return `gray-${label}`;
|
|
293
|
-
}
|
|
294
275
|
function stepKey(scaleTitle, stepName) {
|
|
295
276
|
return `${scaleTitle}-${stepName}`;
|
|
296
277
|
}
|
|
@@ -306,16 +287,6 @@ function computePaletteColor(index, base, lightnessCurve, saturationCurve, curve
|
|
|
306
287
|
const clamped = gamutClamp(targetL, targetC, h);
|
|
307
288
|
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
308
289
|
}
|
|
309
|
-
function computeGrayColor(index, hue, chroma, grayLightnessCurve, graySaturationCurve, curveOffset) {
|
|
310
|
-
const xPos = stepIndexToX(index, GRAY_STEPS.length);
|
|
311
|
-
const lOff = curveOffset["gray-lightness"] ?? 0;
|
|
312
|
-
const sOff = curveOffset["gray-saturation"] ?? 0;
|
|
313
|
-
const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
|
|
314
|
-
const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
|
|
315
|
-
const targetC = chroma * satMul;
|
|
316
|
-
const clamped = gamutClamp(targetL, targetC, hue);
|
|
317
|
-
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
318
|
-
}
|
|
319
290
|
function computeDerivedColor(step, base, scaleTitle, scaleCurves, curveOffset) {
|
|
320
291
|
const scale = SCALES.find((s) => s.title === scaleTitle);
|
|
321
292
|
const idx = scale.steps.indexOf(step);
|
|
@@ -361,36 +332,18 @@ function derivePaletteVars(spec, config) {
|
|
|
361
332
|
const overrides = config.overrides ?? {};
|
|
362
333
|
const curveOffset = config.curveOffset ?? {};
|
|
363
334
|
const scaleCurves = config.scaleCurves ?? {};
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const k = grayStepKey(step.label);
|
|
373
|
-
const hex = computeGrayColor(index, tintHue, tintChroma, grayLightnessCurve, graySaturationCurve, curveOffset);
|
|
374
|
-
const effective = k in overrides ? overrides[k] : hex;
|
|
375
|
-
out[`--color-${spec.cssNamespace}-${step.label}`] = effective;
|
|
376
|
-
if (step.label === "500") gray500 = hex;
|
|
377
|
-
});
|
|
378
|
-
baseForScales = gray500;
|
|
379
|
-
} else {
|
|
380
|
-
const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
|
|
381
|
-
const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
|
|
382
|
-
PALETTE_STEPS.forEach((ps, index) => {
|
|
383
|
-
const k = paletteStepKey(ps.label);
|
|
384
|
-
const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
|
|
385
|
-
const effective = k in overrides ? overrides[k] : hex;
|
|
386
|
-
out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
|
|
387
|
-
});
|
|
388
|
-
baseForScales = baseColor;
|
|
389
|
-
}
|
|
335
|
+
const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
|
|
336
|
+
const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
|
|
337
|
+
PALETTE_STEPS.forEach((ps, index) => {
|
|
338
|
+
const k = paletteStepKey(ps.label);
|
|
339
|
+
const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
|
|
340
|
+
const effective = k in overrides ? overrides[k] : hex;
|
|
341
|
+
out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
|
|
342
|
+
});
|
|
390
343
|
for (const scale of SCALES) {
|
|
391
344
|
for (const step of scale.steps) {
|
|
392
345
|
const k = stepKey(scale.title, step.name);
|
|
393
|
-
const hex = k in overrides ? overrides[k] : computeDerivedColor(step,
|
|
346
|
+
const hex = k in overrides ? overrides[k] : computeDerivedColor(step, baseColor, scale.title, scaleCurves, curveOffset);
|
|
394
347
|
const varName = scaleToCssVar(scale.title, step.name, spec.cssNamespace);
|
|
395
348
|
if (varName) out[varName] = hex;
|
|
396
349
|
}
|
|
@@ -451,12 +404,7 @@ function reconcilePalettesFromCssVars(palettes, cssVars) {
|
|
|
451
404
|
const anchorHex = cssVars[`--color-${spec.cssNamespace}-500`];
|
|
452
405
|
if (anchorHex && HEX_RE.test(anchorHex.trim())) {
|
|
453
406
|
const hex = anchorHex.trim();
|
|
454
|
-
|
|
455
|
-
const { c, h } = hexToOklch(hex);
|
|
456
|
-
next[spec.label] = { ...current, tintHue: h, tintChroma: c, _imported: false };
|
|
457
|
-
} else {
|
|
458
|
-
next[spec.label] = { ...current, baseColor: hex, _imported: false };
|
|
459
|
-
}
|
|
407
|
+
next[spec.label] = { ...current, baseColor: hex, _imported: false };
|
|
460
408
|
snapped.add(spec.label);
|
|
461
409
|
} else {
|
|
462
410
|
next[spec.label] = { ...current, _imported: false };
|
package/dist-plugin/index.js
CHANGED
|
@@ -151,16 +151,16 @@ function sampleCurve(anchors, xPos) {
|
|
|
151
151
|
|
|
152
152
|
// src/editor/core/palettes/paletteDerivation.ts
|
|
153
153
|
var PALETTE_SPECS = [
|
|
154
|
-
{ label: "Neutral", cssNamespace: "neutral",
|
|
155
|
-
{ label: "Alternate", cssNamespace: "alternate",
|
|
156
|
-
{ label: "Background", cssNamespace: "canvas",
|
|
157
|
-
{ label: "Brand", cssNamespace: "brand",
|
|
158
|
-
{ label: "Accent", cssNamespace: "accent",
|
|
159
|
-
{ label: "Special", cssNamespace: "special",
|
|
160
|
-
{ label: "Info", cssNamespace: "info",
|
|
161
|
-
{ label: "Success", cssNamespace: "success",
|
|
162
|
-
{ label: "Warning", cssNamespace: "warning",
|
|
163
|
-
{ label: "Danger", cssNamespace: "danger",
|
|
154
|
+
{ label: "Neutral", cssNamespace: "neutral", neutral: true, initialColor: "#70787e" },
|
|
155
|
+
{ label: "Alternate", cssNamespace: "alternate", neutral: true, initialColor: "#817b78" },
|
|
156
|
+
{ label: "Background", cssNamespace: "canvas", emptySelector: true, initialColor: "#1a1a2e" },
|
|
157
|
+
{ label: "Brand", cssNamespace: "brand", initialColor: "#c93636" },
|
|
158
|
+
{ label: "Accent", cssNamespace: "accent", initialColor: "#f49e0b" },
|
|
159
|
+
{ label: "Special", cssNamespace: "special", initialColor: "#8b5cf6" },
|
|
160
|
+
{ label: "Info", cssNamespace: "info", initialColor: "#3077e8" },
|
|
161
|
+
{ label: "Success", cssNamespace: "success", initialColor: "#21c45d" },
|
|
162
|
+
{ label: "Warning", cssNamespace: "warning", initialColor: "#e66e1a" },
|
|
163
|
+
{ label: "Danger", cssNamespace: "danger", initialColor: "#e8304f" }
|
|
164
164
|
];
|
|
165
165
|
var PALETTE_STEPS = [
|
|
166
166
|
{ label: "100" },
|
|
@@ -175,19 +175,6 @@ var PALETTE_STEPS = [
|
|
|
175
175
|
{ label: "900" },
|
|
176
176
|
{ label: "950" }
|
|
177
177
|
];
|
|
178
|
-
var GRAY_STEPS = [
|
|
179
|
-
{ label: "100" },
|
|
180
|
-
{ label: "200" },
|
|
181
|
-
{ label: "300" },
|
|
182
|
-
{ label: "400" },
|
|
183
|
-
{ label: "500" },
|
|
184
|
-
{ label: "600" },
|
|
185
|
-
{ label: "700" },
|
|
186
|
-
{ label: "800" },
|
|
187
|
-
{ label: "850" },
|
|
188
|
-
{ label: "900" },
|
|
189
|
-
{ label: "950" }
|
|
190
|
-
];
|
|
191
178
|
var SCALES = [
|
|
192
179
|
{
|
|
193
180
|
title: "Surfaces",
|
|
@@ -227,9 +214,6 @@ var SCALES = [
|
|
|
227
214
|
];
|
|
228
215
|
var DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
|
|
229
216
|
var DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
|
|
230
|
-
var DEFAULT_GRAY_LIGHTNESS = () => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
|
|
231
|
-
var DEFAULT_GRAY_SATURATION = () => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
|
|
232
|
-
var DEFAULT_TINT_CHROMA = 0.04;
|
|
233
217
|
var defaultScaleCurves = {
|
|
234
218
|
Surfaces: {
|
|
235
219
|
lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
|
|
@@ -247,9 +231,6 @@ var defaultScaleCurves = {
|
|
|
247
231
|
function paletteStepKey(label) {
|
|
248
232
|
return `Palette-${label}`;
|
|
249
233
|
}
|
|
250
|
-
function grayStepKey(label) {
|
|
251
|
-
return `gray-${label}`;
|
|
252
|
-
}
|
|
253
234
|
function stepKey(scaleTitle, stepName) {
|
|
254
235
|
return `${scaleTitle}-${stepName}`;
|
|
255
236
|
}
|
|
@@ -265,16 +246,6 @@ function computePaletteColor(index, base, lightnessCurve, saturationCurve, curve
|
|
|
265
246
|
const clamped = gamutClamp(targetL, targetC, h);
|
|
266
247
|
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
267
248
|
}
|
|
268
|
-
function computeGrayColor(index, hue, chroma, grayLightnessCurve, graySaturationCurve, curveOffset) {
|
|
269
|
-
const xPos = stepIndexToX(index, GRAY_STEPS.length);
|
|
270
|
-
const lOff = curveOffset["gray-lightness"] ?? 0;
|
|
271
|
-
const sOff = curveOffset["gray-saturation"] ?? 0;
|
|
272
|
-
const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
|
|
273
|
-
const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
|
|
274
|
-
const targetC = chroma * satMul;
|
|
275
|
-
const clamped = gamutClamp(targetL, targetC, hue);
|
|
276
|
-
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
277
|
-
}
|
|
278
249
|
function computeDerivedColor(step, base, scaleTitle, scaleCurves, curveOffset) {
|
|
279
250
|
const scale = SCALES.find((s) => s.title === scaleTitle);
|
|
280
251
|
const idx = scale.steps.indexOf(step);
|
|
@@ -320,36 +291,18 @@ function derivePaletteVars(spec, config) {
|
|
|
320
291
|
const overrides = config.overrides ?? {};
|
|
321
292
|
const curveOffset = config.curveOffset ?? {};
|
|
322
293
|
const scaleCurves = config.scaleCurves ?? {};
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const k = grayStepKey(step.label);
|
|
332
|
-
const hex = computeGrayColor(index, tintHue, tintChroma, grayLightnessCurve, graySaturationCurve, curveOffset);
|
|
333
|
-
const effective = k in overrides ? overrides[k] : hex;
|
|
334
|
-
out[`--color-${spec.cssNamespace}-${step.label}`] = effective;
|
|
335
|
-
if (step.label === "500") gray500 = hex;
|
|
336
|
-
});
|
|
337
|
-
baseForScales = gray500;
|
|
338
|
-
} else {
|
|
339
|
-
const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
|
|
340
|
-
const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
|
|
341
|
-
PALETTE_STEPS.forEach((ps, index) => {
|
|
342
|
-
const k = paletteStepKey(ps.label);
|
|
343
|
-
const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
|
|
344
|
-
const effective = k in overrides ? overrides[k] : hex;
|
|
345
|
-
out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
|
|
346
|
-
});
|
|
347
|
-
baseForScales = baseColor;
|
|
348
|
-
}
|
|
294
|
+
const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
|
|
295
|
+
const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
|
|
296
|
+
PALETTE_STEPS.forEach((ps, index) => {
|
|
297
|
+
const k = paletteStepKey(ps.label);
|
|
298
|
+
const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
|
|
299
|
+
const effective = k in overrides ? overrides[k] : hex;
|
|
300
|
+
out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
|
|
301
|
+
});
|
|
349
302
|
for (const scale of SCALES) {
|
|
350
303
|
for (const step of scale.steps) {
|
|
351
304
|
const k = stepKey(scale.title, step.name);
|
|
352
|
-
const hex = k in overrides ? overrides[k] : computeDerivedColor(step,
|
|
305
|
+
const hex = k in overrides ? overrides[k] : computeDerivedColor(step, baseColor, scale.title, scaleCurves, curveOffset);
|
|
353
306
|
const varName = scaleToCssVar(scale.title, step.name, spec.cssNamespace);
|
|
354
307
|
if (varName) out[varName] = hex;
|
|
355
308
|
}
|
|
@@ -410,12 +363,7 @@ function reconcilePalettesFromCssVars(palettes, cssVars) {
|
|
|
410
363
|
const anchorHex = cssVars[`--color-${spec.cssNamespace}-500`];
|
|
411
364
|
if (anchorHex && HEX_RE.test(anchorHex.trim())) {
|
|
412
365
|
const hex = anchorHex.trim();
|
|
413
|
-
|
|
414
|
-
const { c, h } = hexToOklch(hex);
|
|
415
|
-
next[spec.label] = { ...current, tintHue: h, tintChroma: c, _imported: false };
|
|
416
|
-
} else {
|
|
417
|
-
next[spec.label] = { ...current, baseColor: hex, _imported: false };
|
|
418
|
-
}
|
|
366
|
+
next[spec.label] = { ...current, baseColor: hex, _imported: false };
|
|
419
367
|
snapped.add(spec.label);
|
|
420
368
|
} else {
|
|
421
369
|
next[spec.label] = { ...current, _imported: false };
|
package/package.json
CHANGED
|
@@ -428,7 +428,7 @@
|
|
|
428
428
|
{#snippet compositeControls(_stateName)}
|
|
429
429
|
<div class="gradient-bg-section">
|
|
430
430
|
<GradientEditor
|
|
431
|
-
sectionLabel="
|
|
431
|
+
sectionLabel="Surface"
|
|
432
432
|
source={gradientSources[v.key]}
|
|
433
433
|
stopIdPrefix={`sectiondivider-${v.key}`}
|
|
434
434
|
familyFilter={getColorFamily(v.key)}
|
|
@@ -17,14 +17,14 @@ import { hexToOklch, oklchToHex, gamutClamp } from './oklch';
|
|
|
17
17
|
import { type CurveAnchor, sampleCurve, makeAnchor } from '../../ui/curveEngine';
|
|
18
18
|
import type { PaletteConfig } from '../themes/themeTypes';
|
|
19
19
|
|
|
20
|
-
export type PaletteMode = 'chromatic' | 'gray';
|
|
21
|
-
|
|
22
20
|
export interface PaletteSpec {
|
|
23
21
|
label: string;
|
|
24
22
|
cssNamespace: string;
|
|
25
|
-
mode: PaletteMode;
|
|
26
23
|
emptySelector?: boolean;
|
|
27
24
|
initialColor: string;
|
|
25
|
+
/** Seed-default role only: neutrals start with a calm low-chroma base and
|
|
26
|
+
* the neutral lightness ramp. The derivation path is identical for all. */
|
|
27
|
+
neutral?: boolean;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -33,16 +33,16 @@ export interface PaletteSpec {
|
|
|
33
33
|
* here lets the store seed boot-time vars without depending on the UI tree.
|
|
34
34
|
*/
|
|
35
35
|
export const PALETTE_SPECS: readonly PaletteSpec[] = [
|
|
36
|
-
{ label: 'Neutral', cssNamespace: 'neutral',
|
|
37
|
-
{ label: 'Alternate', cssNamespace: 'alternate',
|
|
38
|
-
{ label: 'Background', cssNamespace: 'canvas',
|
|
39
|
-
{ label: 'Brand', cssNamespace: 'brand',
|
|
40
|
-
{ label: 'Accent', cssNamespace: 'accent',
|
|
41
|
-
{ label: 'Special', cssNamespace: 'special',
|
|
42
|
-
{ label: 'Info', cssNamespace: 'info',
|
|
43
|
-
{ label: 'Success', cssNamespace: 'success',
|
|
44
|
-
{ label: 'Warning', cssNamespace: 'warning',
|
|
45
|
-
{ label: 'Danger', cssNamespace: 'danger',
|
|
36
|
+
{ label: 'Neutral', cssNamespace: 'neutral', neutral: true, initialColor: '#70787e' },
|
|
37
|
+
{ label: 'Alternate', cssNamespace: 'alternate', neutral: true, initialColor: '#817b78' },
|
|
38
|
+
{ label: 'Background', cssNamespace: 'canvas', emptySelector: true, initialColor: '#1a1a2e' },
|
|
39
|
+
{ label: 'Brand', cssNamespace: 'brand', initialColor: '#c93636' },
|
|
40
|
+
{ label: 'Accent', cssNamespace: 'accent', initialColor: '#f49e0b' },
|
|
41
|
+
{ label: 'Special', cssNamespace: 'special', initialColor: '#8b5cf6' },
|
|
42
|
+
{ label: 'Info', cssNamespace: 'info', initialColor: '#3077e8' },
|
|
43
|
+
{ label: 'Success', cssNamespace: 'success', initialColor: '#21c45d' },
|
|
44
|
+
{ label: 'Warning', cssNamespace: 'warning', initialColor: '#e66e1a' },
|
|
45
|
+
{ label: 'Danger', cssNamespace: 'danger', initialColor: '#e8304f' },
|
|
46
46
|
] as const;
|
|
47
47
|
|
|
48
48
|
const PALETTE_STEPS = [
|
|
@@ -51,12 +51,6 @@ const PALETTE_STEPS = [
|
|
|
51
51
|
{ label: '850' }, { label: '900' }, { label: '950' },
|
|
52
52
|
];
|
|
53
53
|
|
|
54
|
-
const GRAY_STEPS = [
|
|
55
|
-
{ label: '100' }, { label: '200' }, { label: '300' }, { label: '400' },
|
|
56
|
-
{ label: '500' }, { label: '600' }, { label: '700' }, { label: '800' },
|
|
57
|
-
{ label: '850' }, { label: '900' }, { label: '950' },
|
|
58
|
-
];
|
|
59
|
-
|
|
60
54
|
interface ScaleStep { name: string; position: number; }
|
|
61
55
|
interface Scale { title: string; isText: boolean; steps: ScaleStep[]; }
|
|
62
56
|
|
|
@@ -97,9 +91,6 @@ const SCALES: readonly Scale[] = [
|
|
|
97
91
|
|
|
98
92
|
export const DEFAULT_PALETTE_LIGHTNESS = (): CurveAnchor[] => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
|
|
99
93
|
export const DEFAULT_PALETTE_SATURATION = (): CurveAnchor[] => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
|
|
100
|
-
export const DEFAULT_GRAY_LIGHTNESS = (): CurveAnchor[] => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
|
|
101
|
-
export const DEFAULT_GRAY_SATURATION = (): CurveAnchor[] => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
|
|
102
|
-
export const DEFAULT_TINT_CHROMA = 0.04;
|
|
103
94
|
|
|
104
95
|
export const defaultScaleCurves = {
|
|
105
96
|
Surfaces: {
|
|
@@ -117,7 +108,6 @@ export const defaultScaleCurves = {
|
|
|
117
108
|
} as const;
|
|
118
109
|
|
|
119
110
|
function paletteStepKey(label: string): string { return `Palette-${label}`; }
|
|
120
|
-
function grayStepKey(label: string): string { return `gray-${label}`; }
|
|
121
111
|
function stepKey(scaleTitle: string, stepName: string): string { return `${scaleTitle}-${stepName}`; }
|
|
122
112
|
|
|
123
113
|
function stepIndexToX(index: number, total: number): number {
|
|
@@ -140,24 +130,6 @@ function computePaletteColor(
|
|
|
140
130
|
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
141
131
|
}
|
|
142
132
|
|
|
143
|
-
function computeGrayColor(
|
|
144
|
-
index: number,
|
|
145
|
-
hue: number,
|
|
146
|
-
chroma: number,
|
|
147
|
-
grayLightnessCurve: CurveAnchor[],
|
|
148
|
-
graySaturationCurve: CurveAnchor[],
|
|
149
|
-
curveOffset: Record<string, number>,
|
|
150
|
-
): string {
|
|
151
|
-
const xPos = stepIndexToX(index, GRAY_STEPS.length);
|
|
152
|
-
const lOff = curveOffset['gray-lightness'] ?? 0;
|
|
153
|
-
const sOff = curveOffset['gray-saturation'] ?? 0;
|
|
154
|
-
const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
|
|
155
|
-
const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
|
|
156
|
-
const targetC = chroma * satMul;
|
|
157
|
-
const clamped = gamutClamp(targetL, targetC, hue);
|
|
158
|
-
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
133
|
function computeDerivedColor(
|
|
162
134
|
step: ScaleStep,
|
|
163
135
|
base: string,
|
|
@@ -212,43 +184,22 @@ export function derivePaletteVars(spec: PaletteSpec, config: PaletteConfig | und
|
|
|
212
184
|
const overrides = config.overrides ?? {};
|
|
213
185
|
const curveOffset = config.curveOffset ?? {};
|
|
214
186
|
const scaleCurves = config.scaleCurves ?? {};
|
|
187
|
+
const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
|
|
188
|
+
const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
|
|
215
189
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const tintChroma = config.tintChroma ?? DEFAULT_TINT_CHROMA;
|
|
223
|
-
|
|
224
|
-
let gray500 = '#808080';
|
|
225
|
-
GRAY_STEPS.forEach((step, index) => {
|
|
226
|
-
const k = grayStepKey(step.label);
|
|
227
|
-
const hex = computeGrayColor(index, tintHue, tintChroma, grayLightnessCurve, graySaturationCurve, curveOffset);
|
|
228
|
-
const effective = (k in overrides) ? overrides[k] : hex;
|
|
229
|
-
out[`--color-${spec.cssNamespace}-${step.label}`] = effective;
|
|
230
|
-
if (step.label === '500') gray500 = hex;
|
|
231
|
-
});
|
|
232
|
-
baseForScales = gray500;
|
|
233
|
-
} else {
|
|
234
|
-
const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
|
|
235
|
-
const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
|
|
236
|
-
|
|
237
|
-
PALETTE_STEPS.forEach((ps, index) => {
|
|
238
|
-
const k = paletteStepKey(ps.label);
|
|
239
|
-
const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
|
|
240
|
-
const effective = (k in overrides) ? overrides[k] : hex;
|
|
241
|
-
out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
|
|
242
|
-
});
|
|
243
|
-
baseForScales = baseColor;
|
|
244
|
-
}
|
|
190
|
+
PALETTE_STEPS.forEach((ps, index) => {
|
|
191
|
+
const k = paletteStepKey(ps.label);
|
|
192
|
+
const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
|
|
193
|
+
const effective = (k in overrides) ? overrides[k] : hex;
|
|
194
|
+
out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
|
|
195
|
+
});
|
|
245
196
|
|
|
246
197
|
for (const scale of SCALES) {
|
|
247
198
|
for (const step of scale.steps) {
|
|
248
199
|
const k = stepKey(scale.title, step.name);
|
|
249
200
|
const hex = (k in overrides)
|
|
250
201
|
? overrides[k]
|
|
251
|
-
: computeDerivedColor(step,
|
|
202
|
+
: computeDerivedColor(step, baseColor, scale.title, scaleCurves, curveOffset);
|
|
252
203
|
const varName = scaleToCssVar(scale.title, step.name, spec.cssNamespace);
|
|
253
204
|
if (varName) out[varName] = hex;
|
|
254
205
|
}
|
|
@@ -307,12 +258,11 @@ const HEX_RE = /^#[0-9a-f]{6}$/i;
|
|
|
307
258
|
*
|
|
308
259
|
* - **Snap** (gated by `_imported`): for any palette whose `_imported` flag
|
|
309
260
|
* is true, the imported `--color-{ns}-500` value is treated as the
|
|
310
|
-
* authoritative anchor
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
* `themes/default.json`'s accent from teal to olive on first read.
|
|
261
|
+
* authoritative anchor and `baseColor` is snapped to it. The flag is then
|
|
262
|
+
* cleared. Editor-authored palettes (no `_imported`) are left untouched —
|
|
263
|
+
* see `temp/manifest-robustness-plan.md` §9 for why "snap on any
|
|
264
|
+
* divergence" was wrong: it would have flipped `themes/default.json`'s
|
|
265
|
+
* accent from teal to olive on first read.
|
|
316
266
|
*
|
|
317
267
|
* - **Consume** (always): every variable the palette's derivation produces
|
|
318
268
|
* is reported in `consumed` so the caller can strip it from
|
|
@@ -345,12 +295,7 @@ export function reconcilePalettesFromCssVars(
|
|
|
345
295
|
const anchorHex = cssVars[`--color-${spec.cssNamespace}-500`];
|
|
346
296
|
if (anchorHex && HEX_RE.test(anchorHex.trim())) {
|
|
347
297
|
const hex = anchorHex.trim();
|
|
348
|
-
|
|
349
|
-
const { c, h } = hexToOklch(hex);
|
|
350
|
-
next[spec.label] = { ...current, tintHue: h, tintChroma: c, _imported: false };
|
|
351
|
-
} else {
|
|
352
|
-
next[spec.label] = { ...current, baseColor: hex, _imported: false };
|
|
353
|
-
}
|
|
298
|
+
next[spec.label] = { ...current, baseColor: hex, _imported: false };
|
|
354
299
|
snapped.add(spec.label);
|
|
355
300
|
} else {
|
|
356
301
|
// No anchor in cssVariables to snap to — flag has nothing to do; clear
|
|
@@ -13,6 +13,17 @@ function rememberPrev(current: string) {
|
|
|
13
13
|
|
|
14
14
|
export const route = writable<string>('/');
|
|
15
15
|
|
|
16
|
+
// Scroll reset on navigation. The default jumps the native viewport to the top,
|
|
17
|
+
// which is invisible to consumers that drive scrolling with a smooth-scroll
|
|
18
|
+
// library (Lenis, Locomotive): their internal scroll position is decoupled from
|
|
19
|
+
// the window, so `window.scrollTo` leaves the rendered page where it was.
|
|
20
|
+
// `setScrollReset` lets such hosts route the reset through their own provider.
|
|
21
|
+
let scrollReset = () => window.scrollTo(0, 0);
|
|
22
|
+
|
|
23
|
+
export function setScrollReset(fn: () => void) {
|
|
24
|
+
scrollReset = fn;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
let initialised = false;
|
|
17
28
|
|
|
18
29
|
/**
|
|
@@ -43,7 +54,7 @@ export function navigate(path: string) {
|
|
|
43
54
|
rememberPrev(window.location.pathname || '/');
|
|
44
55
|
history.pushState(null, '', path);
|
|
45
56
|
if (!path.includes('#')) {
|
|
46
|
-
|
|
57
|
+
scrollReset();
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
route.set(pathname);
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
runMigrations,
|
|
29
29
|
} from '../themes/migrations';
|
|
30
30
|
import { renamePrimaryPaletteKey } from '../themes/migrations/2026-05-13-primary-to-brand';
|
|
31
|
+
import { unifyGrayPalettes } from '../themes/migrations/2026-06-05-palette-unification';
|
|
31
32
|
import { __resetRendererCacheForTests, installRenderer } from './editorRenderer';
|
|
32
33
|
import {
|
|
33
34
|
store,
|
|
@@ -511,7 +512,7 @@ const domainLoaders: Record<string, DomainLoader> = {
|
|
|
511
512
|
*/
|
|
512
513
|
export function loadFromFile(theme: Theme): void {
|
|
513
514
|
const next = emptyState();
|
|
514
|
-
next.palettes = renamePrimaryPaletteKey(structuredClone(theme.editorConfigs ?? {}));
|
|
515
|
+
next.palettes = unifyGrayPalettes(renamePrimaryPaletteKey(structuredClone(theme.editorConfigs ?? {})));
|
|
515
516
|
next.fonts.sources = structuredClone(theme.fontSources ?? []);
|
|
516
517
|
next.fonts.stacks = structuredClone(theme.fontStacks ?? []);
|
|
517
518
|
const rawVars = runMigrations(
|