@motion-proto/live-tokens 0.47.0 → 0.48.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-adjust-shape-space/SKILL.md +68 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
- package/.claude/skills/live-tokens-generate-theme/SKILL.md +152 -0
- package/CHANGELOG.md +292 -0
- package/README.md +37 -26
- package/bin/adjust.mjs +254 -0
- package/bin/cli.mjs +94 -7
- package/bin/generate-theme.mjs +251 -0
- package/bin/migrate.mjs +95 -6
- package/dist-plugin/adjust/index.cjs +259 -0
- package/dist-plugin/adjust/index.d.cts +45 -0
- package/dist-plugin/adjust/index.d.ts +45 -0
- package/dist-plugin/adjust/index.js +176 -0
- package/dist-plugin/{chunk-H4TRUINI.js → chunk-44RSTAII.js} +0 -48
- package/dist-plugin/chunk-6OZFXIQI.js +316 -0
- package/dist-plugin/chunk-76TFDTJO.js +556 -0
- package/dist-plugin/chunk-D3ZVKOR4.js +52 -0
- package/dist-plugin/dataPaths-DBN0RPuT.d.cts +54 -0
- package/dist-plugin/dataPaths-DBN0RPuT.d.ts +54 -0
- package/dist-plugin/generateColorsAndType/index.cjs +1781 -0
- package/dist-plugin/generateColorsAndType/index.d.cts +81 -0
- package/dist-plugin/generateColorsAndType/index.d.ts +81 -0
- package/dist-plugin/generateColorsAndType/index.js +1210 -0
- package/dist-plugin/index.cjs +901 -533
- package/dist-plugin/index.d.cts +3 -2
- package/dist-plugin/index.d.ts +3 -2
- package/dist-plugin/index.js +658 -1082
- package/dist-plugin/migrateData/index.cjs +728 -0
- package/dist-plugin/migrateData/index.d.cts +60 -0
- package/dist-plugin/migrateData/index.d.ts +60 -0
- package/dist-plugin/migrateData/index.js +348 -0
- package/dist-plugin/themeTypes-DMHZOnUn.d.cts +211 -0
- package/dist-plugin/themeTypes-DMHZOnUn.d.ts +211 -0
- package/dist-plugin/tokensCssMigrations/index.cjs +2 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +3 -15
- package/dist-plugin/tokensCssMigrations/index.d.ts +3 -15
- package/dist-plugin/tokensCssMigrations/index.js +4 -2
- package/package.json +18 -4
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +183 -158
- package/src/editor/component-editor/scaffolding/ComponentFileMenu.svelte +0 -4
- package/src/editor/component-editor/scaffolding/SaveAsDialog.svelte +4 -4
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +2 -59
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +1 -1
- package/src/editor/core/components/adjustAliases.ts +180 -0
- package/src/editor/core/components/aliasKinds.ts +73 -0
- package/src/editor/core/components/componentConfigService.ts +26 -38
- package/src/editor/core/flashStatus.ts +1 -1
- package/src/editor/core/fonts/fontMigration.ts +13 -13
- package/src/editor/core/fonts/fontPairing.ts +35 -0
- package/src/editor/core/palettes/paletteDerivation.ts +128 -16
- package/src/editor/core/preview/lookPreview.ts +141 -0
- package/src/editor/core/productionPulse.ts +34 -21
- package/src/editor/core/storage/files/versionedFileResourceClient.ts +18 -45
- package/src/editor/core/store/editorConfigStore.ts +2 -2
- package/src/editor/core/store/editorPersistence.ts +0 -1
- package/src/editor/core/store/editorStore.ts +114 -51
- package/src/editor/core/store/editorTypes.ts +0 -1
- package/src/editor/core/store/gradientSource.ts +2 -2
- package/src/editor/core/themes/colorsAndTypeService.ts +95 -0
- package/src/editor/core/themes/generateColorsAndType.ts +433 -0
- package/src/editor/core/themes/loadRows.ts +64 -0
- package/src/editor/core/themes/lookSummary.ts +75 -0
- package/src/editor/core/themes/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +3 -3
- package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -2
- package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +3 -3
- package/src/editor/core/themes/migrations/2026-08-13-drop-legacy-shape-space-keys.ts +38 -0
- package/src/editor/core/themes/migrations/2026-08-15-line-height-scale-rename.ts +57 -0
- package/src/editor/core/themes/migrations/index.ts +19 -11
- package/src/editor/core/themes/slices/components.ts +4 -4
- package/src/editor/core/themes/slices/fonts.ts +1 -1
- package/src/editor/core/themes/slices/gradients.ts +22 -0
- package/src/editor/core/themes/slices/palettes.ts +2 -2
- package/src/editor/core/themes/themeInit.ts +21 -29
- package/src/editor/core/themes/themeService.ts +218 -78
- package/src/editor/core/themes/themeTypes.ts +94 -51
- package/src/editor/docs/Docs.svelte +1 -0
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/01-overview.md +1 -1
- package/src/editor/docs/content/editing-tokens.md +6 -6
- package/src/editor/docs/content/getting-started.md +9 -7
- package/src/editor/docs/content/themes-workflow.md +74 -34
- package/src/editor/docs/content/where-themes-live.md +61 -0
- package/src/editor/docs/content.generated.ts +5 -4
- package/src/editor/index.ts +27 -27
- package/src/editor/pages/ComponentEditorPage.svelte +2 -2
- package/src/editor/pages/EditorShell.svelte +4 -30
- package/src/editor/ui/BezierCurveEditor.svelte +2 -2
- package/src/editor/ui/ColorEditPanel.svelte +1 -1
- package/src/editor/ui/FileLoadList.svelte +67 -7
- package/src/editor/ui/GradientEditor.svelte +3 -3
- package/src/editor/ui/PaletteEditor.svelte +26 -12
- package/src/editor/ui/ThemePanel.svelte +1067 -0
- package/src/editor/ui/UIDialog.svelte +11 -9
- package/src/editor/ui/UIPillButton.svelte +7 -2
- package/src/editor/ui/colors/ColorWheel.svelte +34 -1
- package/src/editor/ui/curveEngine.ts +48 -0
- package/src/editor/ui/index.ts +4 -2
- package/src/editor/ui/palette/PaletteBase.svelte +75 -51
- package/src/editor/ui/palette/PaletteJumpButton.svelte +3 -8
- package/src/live-tokens/data/colors-and-type/autumn.json +2490 -0
- package/src/live-tokens/data/{themes → colors-and-type}/default.json +816 -293
- package/src/live-tokens/data/colors-and-type/halloween.json +2529 -0
- package/src/live-tokens/data/colors-and-type/midnight-study.json +2536 -0
- package/src/live-tokens/data/colors-and-type/ocean.json +2489 -0
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +2520 -0
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +2476 -0
- package/src/live-tokens/data/colors-and-type/sunset.json +2528 -0
- package/src/live-tokens/data/themes/autumn.json +3918 -0
- package/src/live-tokens/data/themes/halloween.json +3992 -0
- package/src/live-tokens/data/themes/midnight-study.json +3932 -0
- package/src/live-tokens/data/themes/ocean.json +3917 -0
- package/src/live-tokens/data/themes/royal-velvet.json +3983 -0
- package/src/live-tokens/data/themes/spring-meadow.json +3904 -0
- package/src/live-tokens/data/themes/sunset.json +3931 -0
- package/src/live-tokens/data/tokens.generated.css +106 -117
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/fonts.css +1 -1
- package/template/README.md +7 -5
- package/template/_gitignore +0 -6
- package/src/editor/core/components/componentPersist.ts +0 -62
- package/src/editor/core/manifests/manifestService.ts +0 -172
- package/src/editor/ui/ManifestFileManager.svelte +0 -446
- package/src/editor/ui/ThemeFileManager.svelte +0 -785
- package/src/live-tokens/data/manifests/default.json +0 -35
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { hexToOklch, oklchToHexClamped, gamutClamp, type Oklch } from './oklch';
|
|
17
|
-
import { type CurveAnchor, sampleCurve, makeAnchor } from '../../ui/curveEngine';
|
|
17
|
+
import { type CurveAnchor, sampleCurve, makeAnchor, tangentAnchor } from '../../ui/curveEngine';
|
|
18
18
|
import type { PaletteConfig } from '../themes/themeTypes';
|
|
19
19
|
|
|
20
20
|
export interface PaletteSpec {
|
|
@@ -225,9 +225,53 @@ export function nearestPaletteStep(lightnessCurve: CurveAnchor[], baseL: number)
|
|
|
225
225
|
return best;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
/**
|
|
229
|
-
*
|
|
230
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Insert an anchor at x, or overwrite one already there (endpoints included).
|
|
230
|
+
* Returns the y it displaced so the caller can restore it later.
|
|
231
|
+
*
|
|
232
|
+
* `smooth` is for a curve's first-ever placement only (nothing has had a
|
|
233
|
+
* chance to edit it yet): every anchor, endpoints included, gets a tangent
|
|
234
|
+
* computed jointly with its real neighbour, so the placement reads as one
|
|
235
|
+
* smooth curve instead of two segments meeting at a corner where the
|
|
236
|
+
* endpoints still aim along their pre-insertion secant. It also returns the
|
|
237
|
+
* untouched original endpoints as `priorEndpoints` — this reshaping isn't
|
|
238
|
+
* algebraically invertible the way the scale-and-preserve path below is, so
|
|
239
|
+
* the caller must hold onto them to restore the curve exactly later. Every
|
|
240
|
+
* other placement (`smooth` false, the default) preserves whatever shape is
|
|
241
|
+
* already there, edited or not.
|
|
242
|
+
*/
|
|
243
|
+
export function setCurveAnchor(
|
|
244
|
+
curve: CurveAnchor[],
|
|
245
|
+
x: number,
|
|
246
|
+
y: number,
|
|
247
|
+
smooth = false,
|
|
248
|
+
): { curve: CurveAnchor[]; displacedY?: number; priorEndpoints?: [CurveAnchor, CurveAnchor] } {
|
|
249
|
+
if (smooth && curve.length === 2) {
|
|
250
|
+
const [p0, p2] = curve;
|
|
251
|
+
const mid: CurveAnchor = { x, y, inDx: 0, inDy: 0, outDx: 0, outDy: 0 };
|
|
252
|
+
if (Math.abs(p0.x - x) < 0.5) {
|
|
253
|
+
return {
|
|
254
|
+
curve: [tangentAnchor(x, y, null, p2), tangentAnchor(p2.x, p2.y, mid, null)],
|
|
255
|
+
displacedY: p0.y,
|
|
256
|
+
priorEndpoints: [p0, p2],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (Math.abs(p2.x - x) < 0.5) {
|
|
260
|
+
return {
|
|
261
|
+
curve: [tangentAnchor(p0.x, p0.y, null, mid), tangentAnchor(x, y, p0, null)],
|
|
262
|
+
displacedY: p2.y,
|
|
263
|
+
priorEndpoints: [p0, p2],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
curve: [
|
|
268
|
+
tangentAnchor(p0.x, p0.y, null, mid),
|
|
269
|
+
tangentAnchor(x, y, p0, p2),
|
|
270
|
+
tangentAnchor(p2.x, p2.y, mid, null),
|
|
271
|
+
],
|
|
272
|
+
priorEndpoints: [p0, p2],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
231
275
|
const existing = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
|
|
232
276
|
if (existing >= 0) {
|
|
233
277
|
const displacedY = curve[existing].y;
|
|
@@ -235,18 +279,72 @@ export function setCurveAnchor(curve: CurveAnchor[], x: number, y: number): { cu
|
|
|
235
279
|
}
|
|
236
280
|
let insertAt = curve.findIndex((a) => a.x > x);
|
|
237
281
|
if (insertAt < 0) insertAt = curve.length;
|
|
238
|
-
|
|
282
|
+
const prev = curve[insertAt - 1] ?? null;
|
|
283
|
+
const next = curve[insertAt] ?? null;
|
|
284
|
+
const out = [...curve];
|
|
285
|
+
// The neighbours aimed their handles across a gap this insert has just split,
|
|
286
|
+
// so a handle that was in bounds can now reach clean past the new anchor and
|
|
287
|
+
// leave the segment non-monotone in x, which is the one thing sampleCurve
|
|
288
|
+
// cannot survive. Scale them by the share of the gap each one keeps: the same
|
|
289
|
+
// rule de Casteljau uses, so the curve holds its shape, and exactly invertible
|
|
290
|
+
// by liftCurveAnchor when the placement moves off again.
|
|
291
|
+
if (prev && next) {
|
|
292
|
+
const span = next.x - prev.x;
|
|
293
|
+
out[insertAt - 1] = scaleOutHandle(prev, (x - prev.x) / span);
|
|
294
|
+
out[insertAt] = scaleInHandle(next, (next.x - x) / span);
|
|
295
|
+
}
|
|
296
|
+
out.splice(insertAt, 0, fitBetween(tangentAnchor(x, y, prev, next), out[insertAt - 1] ?? null, out[insertAt] ?? null));
|
|
297
|
+
return { curve: out };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const scaleOutHandle = (a: CurveAnchor, k: number): CurveAnchor => ({ ...a, outDx: a.outDx * k, outDy: a.outDy * k });
|
|
301
|
+
const scaleInHandle = (a: CurveAnchor, k: number): CurveAnchor => ({ ...a, inDx: a.inDx * k, inDy: a.inDy * k });
|
|
302
|
+
|
|
303
|
+
/** Shorten a freshly derived anchor's handles to whatever room its neighbours
|
|
304
|
+
* leave. Only the new anchor gives ground: its handles carry no intent yet,
|
|
305
|
+
* and a stored handle is someone's edit. */
|
|
306
|
+
function fitBetween(a: CurveAnchor, prev: CurveAnchor | null, next: CurveAnchor | null): CurveAnchor {
|
|
307
|
+
let out = a;
|
|
308
|
+
if (prev) {
|
|
309
|
+
const room = a.x - (prev.x + prev.outDx);
|
|
310
|
+
if (room < -out.inDx) out = scaleInHandle(out, room <= 0 ? 0 : room / -out.inDx);
|
|
311
|
+
}
|
|
312
|
+
if (next) {
|
|
313
|
+
const room = next.x + next.inDx - a.x;
|
|
314
|
+
if (room < out.outDx) out = scaleOutHandle(out, room <= 0 ? 0 : room / out.outDx);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
239
317
|
}
|
|
240
318
|
|
|
241
|
-
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Undo a placement at x: restore the displaced y when one was recorded,
|
|
321
|
+
* remove the anchor when the placement created it. Endpoints are never
|
|
322
|
+
* removed — a curve needs its boundaries.
|
|
323
|
+
*
|
|
324
|
+
* `priorEndpoints`, when given, wins outright: it's the untouched pair from
|
|
325
|
+
* before a `smooth` placement, which the scale/displace math below can't
|
|
326
|
+
* reconstruct on its own.
|
|
327
|
+
*/
|
|
328
|
+
export function liftCurveAnchor(
|
|
329
|
+
curve: CurveAnchor[],
|
|
330
|
+
x: number,
|
|
331
|
+
displacedY?: number,
|
|
332
|
+
priorEndpoints?: [CurveAnchor, CurveAnchor],
|
|
333
|
+
): CurveAnchor[] {
|
|
334
|
+
if (priorEndpoints) return priorEndpoints;
|
|
245
335
|
const idx = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
|
|
246
336
|
if (idx < 0) return curve;
|
|
247
337
|
if (displacedY !== undefined) return curve.map((a, i) => (i === idx ? { ...a, y: displacedY } : a));
|
|
248
338
|
if (idx === 0 || idx === curve.length - 1) return curve;
|
|
249
|
-
|
|
339
|
+
const out = curve.filter((_, i) => i !== idx);
|
|
340
|
+
const prev = curve[idx - 1];
|
|
341
|
+
const next = curve[idx + 1];
|
|
342
|
+
if (prev && next) {
|
|
343
|
+
const span = next.x - prev.x;
|
|
344
|
+
out[idx - 1] = scaleOutHandle(prev, span / (curve[idx].x - prev.x));
|
|
345
|
+
out[idx] = scaleInHandle(next, span / (next.x - curve[idx].x));
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
250
348
|
}
|
|
251
349
|
|
|
252
350
|
/**
|
|
@@ -261,20 +359,34 @@ export function liftCurveAnchor(curve: CurveAnchor[], x: number, displacedY?: nu
|
|
|
261
359
|
export function syncBaseAnchor(cfg: PaletteConfig): void {
|
|
262
360
|
if (cfg.anchorToBase === false) return;
|
|
263
361
|
const prev = cfg.anchorPlacement;
|
|
362
|
+
const fresh = prev === undefined;
|
|
264
363
|
let lCurve = cfg.lightnessCurve;
|
|
265
364
|
let sCurve = cfg.saturationCurve;
|
|
266
365
|
if (prev) {
|
|
366
|
+
// Re-placing an existing anchor (a hue move, typically) must preserve
|
|
367
|
+
// whatever is currently on the curve, edited or not — never restore the
|
|
368
|
+
// pristine `priorEndpoints` here, or every hand-tuned handle would be
|
|
369
|
+
// discarded the next time the base color moves.
|
|
267
370
|
const prevX = stepIndexToX(prev.step);
|
|
268
371
|
lCurve = liftCurveAnchor(lCurve, prevX, prev.displacedL);
|
|
269
372
|
sCurve = liftCurveAnchor(sCurve, prevX, prev.displacedS);
|
|
270
373
|
}
|
|
271
374
|
const step = nearestPaletteStep(lCurve, cfg.baseColor.l);
|
|
272
375
|
const x = stepIndexToX(step);
|
|
273
|
-
const l = setCurveAnchor(lCurve, x, cfg.baseColor.l * 100);
|
|
274
|
-
const s = setCurveAnchor(sCurve, x, 100);
|
|
376
|
+
const l = setCurveAnchor(lCurve, x, cfg.baseColor.l * 100, fresh);
|
|
377
|
+
const s = setCurveAnchor(sCurve, x, 100, fresh);
|
|
275
378
|
cfg.lightnessCurve = l.curve;
|
|
276
379
|
cfg.saturationCurve = s.curve;
|
|
277
|
-
|
|
380
|
+
// priorEndpoints is captured once, on the curve's first-ever placement,
|
|
381
|
+
// and carried forward untouched thereafter so a later clear can still
|
|
382
|
+
// restore the true original regardless of how many times the anchor moved.
|
|
383
|
+
cfg.anchorPlacement = {
|
|
384
|
+
step,
|
|
385
|
+
displacedL: l.displacedY,
|
|
386
|
+
displacedS: s.displacedY,
|
|
387
|
+
priorLightnessEndpoints: prev?.priorLightnessEndpoints ?? l.priorEndpoints,
|
|
388
|
+
priorSaturationEndpoints: prev?.priorSaturationEndpoints ?? s.priorEndpoints,
|
|
389
|
+
};
|
|
278
390
|
}
|
|
279
391
|
|
|
280
392
|
/** Toggle-off: lift the placement out of both curves and drop the flag. */
|
|
@@ -282,8 +394,8 @@ export function clearBaseAnchor(cfg: PaletteConfig): void {
|
|
|
282
394
|
const prev = cfg.anchorPlacement;
|
|
283
395
|
if (prev) {
|
|
284
396
|
const x = stepIndexToX(prev.step);
|
|
285
|
-
cfg.lightnessCurve = liftCurveAnchor(cfg.lightnessCurve, x, prev.displacedL);
|
|
286
|
-
cfg.saturationCurve = liftCurveAnchor(cfg.saturationCurve, x, prev.displacedS);
|
|
397
|
+
cfg.lightnessCurve = liftCurveAnchor(cfg.lightnessCurve, x, prev.displacedL, prev.priorLightnessEndpoints);
|
|
398
|
+
cfg.saturationCurve = liftCurveAnchor(cfg.saturationCurve, x, prev.displacedS, prev.priorSaturationEndpoints);
|
|
287
399
|
cfg.anchorPlacement = undefined;
|
|
288
400
|
}
|
|
289
401
|
cfg.anchorToBase = false;
|
|
@@ -453,7 +565,7 @@ const HEX_RE = /^#[0-9a-f]{6}$/i;
|
|
|
453
565
|
* authoritative anchor and `baseColor` is snapped to it. The flag is then
|
|
454
566
|
* cleared. Editor-authored palettes (no `_imported`) are left untouched —
|
|
455
567
|
* see `temp/manifest-robustness-plan.md` §9 for why "snap on any
|
|
456
|
-
* divergence" was wrong: it would have flipped `
|
|
568
|
+
* divergence" was wrong: it would have flipped `colors-and-type/default.json`'s
|
|
457
569
|
* accent from teal to olive on first read.
|
|
458
570
|
*
|
|
459
571
|
* - **Consume** (always): every variable the palette's derivation produces
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { get } from 'svelte/store';
|
|
2
|
+
import type { FontSource, Theme, ColorsAndType } from '../themes/themeTypes';
|
|
3
|
+
import { editorState, colorsAndTypeToState, toComponentSlice } from '../store/editorStore';
|
|
4
|
+
import { deriveCssVars } from '../store/editorRenderer';
|
|
5
|
+
import { setCssVar, removeCssVar } from '../cssVarSync';
|
|
6
|
+
import { applyFontSources, resolveFontStackValues } from '../fonts/fontLoader';
|
|
7
|
+
import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
|
|
8
|
+
import { loadTheme } from '../themes/themeService';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Client-side rendering of a saved file, for browsing looks without committing
|
|
12
|
+
* one. A theme previews as a whole look; a colors-and-type file previews as
|
|
13
|
+
* colors and type over the components the user has right now. Nothing here writes to the server,
|
|
14
|
+
* mutates the editor store, or marks anything dirty: a preview is paint only.
|
|
15
|
+
* Save-As stays honest because `captureLook` reads the server's active files,
|
|
16
|
+
* which a preview never touches.
|
|
17
|
+
*
|
|
18
|
+
* Both painting and reverting run the same derivation the renderer runs on
|
|
19
|
+
* store state (`deriveCssVars`), so a preview cannot drift from what an Apply
|
|
20
|
+
* of the same theme produces, and a revert re-derives from the live store
|
|
21
|
+
* rather than replaying DOM values scraped before the preview.
|
|
22
|
+
*
|
|
23
|
+
* One preview is live at a time, whichever kind it is: painting reverts first,
|
|
24
|
+
* so every look is a diff against the user's real state.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Everything a look puts on the page: the full var set (colors and type,
|
|
28
|
+
* components and the composed `--font-*` stacks) plus the sources its faces
|
|
29
|
+
* come from. */
|
|
30
|
+
export interface RenderedLook {
|
|
31
|
+
vars: Record<string, string>;
|
|
32
|
+
fontSources: FontSource[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The full var set a theme paints. Components the theme carries no config
|
|
37
|
+
* for render `defaults`' config, matching Apply's complete-look semantics;
|
|
38
|
+
* configs for components this install lacks are skipped, as Apply skips them.
|
|
39
|
+
*/
|
|
40
|
+
export function themeLook(theme: Theme, defaults: Theme): RenderedLook {
|
|
41
|
+
const colorsAndType = structuredClone(theme.colorsAndType);
|
|
42
|
+
migrateColorsAndTypeFonts(colorsAndType);
|
|
43
|
+
const state = colorsAndTypeToState(colorsAndType);
|
|
44
|
+
state.components = {};
|
|
45
|
+
for (const [comp, config] of Object.entries(defaults.componentConfigs)) {
|
|
46
|
+
state.components[comp] = toComponentSlice(comp, config.aliases, config.config, config.schemaVersion);
|
|
47
|
+
}
|
|
48
|
+
for (const [comp, config] of Object.entries(theme.componentConfigs)) {
|
|
49
|
+
if (!(comp in state.components)) continue;
|
|
50
|
+
state.components[comp] = toComponentSlice(comp, config.aliases, config.config, config.schemaVersion);
|
|
51
|
+
}
|
|
52
|
+
const vars = deriveCssVars(state);
|
|
53
|
+
Object.assign(vars, resolveFontStackValues(colorsAndType.fontStacks ?? [], colorsAndType.fontSources ?? []));
|
|
54
|
+
return { vars, fontSources: colorsAndType.fontSources ?? [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The full var set colors and type paint on their own. They are not a whole look,
|
|
59
|
+
* so the components stay as the user has them: `colorsAndTypeToState` carries the live
|
|
60
|
+
* component slice forward (`loadComponentsFromVars`) and strips component-owned
|
|
61
|
+
* vars out of their own bag, which is exactly the composition a preview
|
|
62
|
+
* wants. No defaults fetch, no component reset.
|
|
63
|
+
*/
|
|
64
|
+
export function colorsAndTypeLook(colorsAndType: ColorsAndType): RenderedLook {
|
|
65
|
+
const next = structuredClone(colorsAndType);
|
|
66
|
+
migrateColorsAndTypeFonts(next);
|
|
67
|
+
const vars = deriveCssVars(colorsAndTypeToState(next));
|
|
68
|
+
Object.assign(vars, resolveFontStackValues(next.fontStacks ?? [], next.fontSources ?? []));
|
|
69
|
+
return { vars, fontSources: next.fontSources ?? [] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The look the editor store currently describes — the state a revert returns to. */
|
|
73
|
+
export function liveLook(): RenderedLook {
|
|
74
|
+
const state = get(editorState);
|
|
75
|
+
const vars = deriveCssVars(state);
|
|
76
|
+
Object.assign(vars, resolveFontStackValues(state.fonts.stacks, state.fonts.sources));
|
|
77
|
+
return { vars, fontSources: state.fonts.sources };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let livePreview: RenderedLook | null = null;
|
|
81
|
+
let defaultsPromise: Promise<Theme> | null = null;
|
|
82
|
+
|
|
83
|
+
function loadDefaults(): Promise<Theme> {
|
|
84
|
+
// A rejected promise must not be memoized, or one failed fetch (a dev-server
|
|
85
|
+
// restart mid-dialog) poisons every later preview until a page reload.
|
|
86
|
+
defaultsPromise ??= loadTheme('default').catch((err) => {
|
|
87
|
+
defaultsPromise = null;
|
|
88
|
+
throw err;
|
|
89
|
+
});
|
|
90
|
+
return defaultsPromise;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function paint(next: RenderedLook, from: RenderedLook): void {
|
|
94
|
+
for (const [name, value] of Object.entries(next.vars)) {
|
|
95
|
+
if (from.vars[name] !== value) setCssVar(name, value);
|
|
96
|
+
}
|
|
97
|
+
for (const name of Object.keys(from.vars)) {
|
|
98
|
+
if (!(name in next.vars)) removeCssVar(name);
|
|
99
|
+
}
|
|
100
|
+
applyFontSources(next.fontSources);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Show `look` on the page. Re-entrant: a live preview is reverted first, so
|
|
105
|
+
* every look is painted as a diff against the user's real state and a var one
|
|
106
|
+
* look sets but the next does not returns to its live value instead of vanishing.
|
|
107
|
+
* The two passes run in one task, so the browser never paints the intermediate.
|
|
108
|
+
*/
|
|
109
|
+
function applyPreview(look: RenderedLook): void {
|
|
110
|
+
revertPreview();
|
|
111
|
+
paint(look, liveLook());
|
|
112
|
+
livePreview = look;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Paint a whole look: the theme's colors and type and every component config
|
|
116
|
+
* it carries. */
|
|
117
|
+
export async function previewTheme(theme: Theme): Promise<void> {
|
|
118
|
+
applyPreview(themeLook(theme, await loadDefaults()));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Paint colors and type over the components as they stand. */
|
|
122
|
+
export function previewColorsAndType(colorsAndType: ColorsAndType): void {
|
|
123
|
+
applyPreview(colorsAndTypeLook(colorsAndType));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Restore the live editor state. No-op when no preview is running. */
|
|
127
|
+
export function revertPreview(): void {
|
|
128
|
+
if (!livePreview) return;
|
|
129
|
+
paint(liveLook(), livePreview);
|
|
130
|
+
livePreview = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isPreviewing(): boolean {
|
|
134
|
+
return livePreview !== null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Test-only: drop the live preview and the cached defaults theme. */
|
|
138
|
+
export function __resetPreviewForTests(): void {
|
|
139
|
+
livePreview = null;
|
|
140
|
+
defaultsPromise = null;
|
|
141
|
+
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
|
-
import type {
|
|
3
|
-
import type { ManifestMeta } from './themes/themeTypes';
|
|
2
|
+
import type { Theme } from './themes/themeTypes';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Monotonic counter that ticks every time
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* per-pair wiring.
|
|
5
|
+
* Monotonic counter that ticks every time an Adopt publishes a theme. UI
|
|
6
|
+
* surfaces that need to react to a sibling Adopt subscribe to this so they
|
|
7
|
+
* refresh without per-pair wiring.
|
|
10
8
|
*
|
|
11
|
-
* Bumpers: `
|
|
12
|
-
* `ComponentFileManager.handleUpdateProduction`. Anyone setting
|
|
13
|
-
* `_production.json` should bump.
|
|
9
|
+
* Bumpers: `ThemePanel.runAdopt`, `ComponentFileManager.handleAdopt`.
|
|
14
10
|
*/
|
|
15
11
|
export const productionRevision = writable(0);
|
|
16
12
|
|
|
@@ -19,19 +15,36 @@ export function bumpProductionRevision(): void {
|
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* fresh fetch resolves.
|
|
18
|
+
* Ticks when a component's live config changes on the server — a save, a
|
|
19
|
+
* preset load, a delete. Saving one is visible through `componentDirty`, but
|
|
20
|
+
* loading a preset with no edit in play changes nothing the Theme panel can
|
|
21
|
+
* observe, and its Components count is derived from exactly that. Bumped by
|
|
22
|
+
* `ComponentFileManager`.
|
|
28
23
|
*/
|
|
29
|
-
export const
|
|
24
|
+
export const componentActiveRevision = writable(0);
|
|
25
|
+
|
|
26
|
+
export function bumpComponentActiveRevision(): void {
|
|
27
|
+
componentActiveRevision.update((n) => n + 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The live look has moved past the last bake. Only Adopt bakes the CSS, so a
|
|
32
|
+
* buffer write or a theme save after one leaves production a version behind,
|
|
33
|
+
* and no pointer file records when the bake happened. Every surface reads the
|
|
34
|
+
* one signal: a component save has to reach the theme panel's Adopt, and a
|
|
35
|
+
* component editor's Adopt has to clear it for the panel.
|
|
36
|
+
*
|
|
37
|
+
* Set by the client writes that move the live look (`writeWorkingColorsAndType`,
|
|
38
|
+
* `writeWorkingComponentConfig`, `saveActiveTheme`, `saveAsTheme`); cleared by
|
|
39
|
+
* `adoptLook`. Module-level, so it survives the remounts a view switch causes.
|
|
40
|
+
*/
|
|
41
|
+
export const liveMovedSinceBake = writable(false);
|
|
30
42
|
|
|
31
43
|
/**
|
|
32
|
-
* Last-
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
44
|
+
* Last-read production theme — the document `tokens.generated.css` was baked
|
|
45
|
+
* from. The Theme panel and the component file managers live in surfaces that
|
|
46
|
+
* swap in and out of the DOM, so keeping the last answer in a module-level
|
|
47
|
+
* store means a remount renders the correct Adopt state on the first frame
|
|
48
|
+
* instead of flashing through "not in sync" while a fresh fetch resolves.
|
|
36
49
|
*/
|
|
37
|
-
export const
|
|
50
|
+
export const productionTheme = writable<Theme | null>(null);
|
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side REST helpers for any "versioned file resource" — a class of
|
|
3
3
|
* editable artifact whose lifecycle mirrors the theme files: list / load /
|
|
4
|
-
* save / delete
|
|
4
|
+
* save / delete, plus a live read door and, for themes, the pointer naming the
|
|
5
|
+
* open one.
|
|
5
6
|
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
* with their resource-specific URL.
|
|
7
|
+
* `colorsAndTypeService.ts`, `componentConfigService.ts` and `themeService.ts`
|
|
8
|
+
* all consume `versionedFileResource(...)` with their own URL root.
|
|
9
9
|
*
|
|
10
10
|
* Pure URL construction + fetch — no DOM, no filesystem, safe for any browser
|
|
11
11
|
* or test harness that has a `fetch` global.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
export interface VersionedFileResourceClientOptions {
|
|
15
|
-
/** REST endpoint root, e.g. `${API_BASE}/
|
|
15
|
+
/** REST endpoint root, e.g. `${API_BASE}/colors-and-type` or `${API_BASE}/component-configs/button`. */
|
|
16
16
|
baseUrl: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface VersionedFileResourceClient<TItem, TMeta
|
|
20
|
-
list(): Promise<{ files: TMeta[]
|
|
19
|
+
export interface VersionedFileResourceClient<TItem, TMeta> {
|
|
20
|
+
list(): Promise<{ files: TMeta[] }>;
|
|
21
21
|
load(fileName: string): Promise<TItem>;
|
|
22
22
|
save(fileName: string, data: TItem): Promise<void>;
|
|
23
23
|
remove(fileName: string): Promise<void>;
|
|
24
24
|
getActive(): Promise<TItem | null>;
|
|
25
25
|
setActive(fileName: string): Promise<void>;
|
|
26
|
-
getProductionInfo(): Promise<TProductionInfo>;
|
|
27
|
-
setProduction(fileName: string): Promise<TProductionInfo & { ok: boolean }>;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
/**
|
|
@@ -34,17 +32,15 @@ export interface VersionedFileResourceClient<TItem, TMeta, TProductionInfo> {
|
|
|
34
32
|
* GET {base}/:name → load
|
|
35
33
|
* PUT {base}/:name → save
|
|
36
34
|
* DELETE {base}/:name → delete
|
|
37
|
-
* GET {base}/active →
|
|
38
|
-
* PUT {base}/active {name} →
|
|
39
|
-
* GET {base}/production → production info
|
|
40
|
-
* PUT {base}/production {name} → set production
|
|
35
|
+
* GET {base}/active → the live item
|
|
36
|
+
* PUT {base}/active {name} → open that item (themes only)
|
|
41
37
|
*
|
|
42
|
-
* Generic over the payload types so
|
|
43
|
-
*
|
|
38
|
+
* Generic over the payload types so themes, colors and type and component
|
|
39
|
+
* configs share the plumbing while keeping their own response shapes.
|
|
44
40
|
*/
|
|
45
|
-
export function versionedFileResource<TItem, TMeta
|
|
41
|
+
export function versionedFileResource<TItem, TMeta>(
|
|
46
42
|
opts: VersionedFileResourceClientOptions,
|
|
47
|
-
): VersionedFileResourceClient<TItem, TMeta
|
|
43
|
+
): VersionedFileResourceClient<TItem, TMeta> {
|
|
48
44
|
const { baseUrl } = opts;
|
|
49
45
|
|
|
50
46
|
async function readJsonError(res: Response, fallback: string): Promise<string> {
|
|
@@ -52,7 +48,7 @@ export function versionedFileResource<TItem, TMeta, TProductionInfo>(
|
|
|
52
48
|
return err.error || fallback;
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
async function list(): Promise<{ files: TMeta[]
|
|
51
|
+
async function list(): Promise<{ files: TMeta[] }> {
|
|
56
52
|
const res = await fetch(baseUrl);
|
|
57
53
|
if (!res.ok) throw new Error(`Failed to list ${baseUrl}`);
|
|
58
54
|
return res.json();
|
|
@@ -105,32 +101,7 @@ export function versionedFileResource<TItem, TMeta, TProductionInfo>(
|
|
|
105
101
|
}
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
const res = await fetch(`${baseUrl}/production`);
|
|
110
|
-
if (!res.ok) throw new Error('Failed to get production info');
|
|
111
|
-
return res.json();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function setProduction(fileName: string): Promise<TProductionInfo & { ok: boolean }> {
|
|
115
|
-
const res = await fetch(`${baseUrl}/production`, {
|
|
116
|
-
method: 'PUT',
|
|
117
|
-
headers: { 'Content-Type': 'application/json' },
|
|
118
|
-
body: JSON.stringify({ name: fileName }),
|
|
119
|
-
});
|
|
120
|
-
if (!res.ok) {
|
|
121
|
-
const body = await res.json().catch(() => ({}));
|
|
122
|
-
const err = new Error(body.error || 'Set production failed') as Error & {
|
|
123
|
-
status?: number;
|
|
124
|
-
code?: string;
|
|
125
|
-
};
|
|
126
|
-
err.status = res.status;
|
|
127
|
-
if (body.code) err.code = body.code;
|
|
128
|
-
throw err;
|
|
129
|
-
}
|
|
130
|
-
return res.json();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { list, load, save, remove, getActive, setActive, getProductionInfo, setProduction };
|
|
104
|
+
return { list, load, save, remove, getActive, setActive };
|
|
134
105
|
}
|
|
135
106
|
|
|
136
107
|
/** Sanitize a display name to a safe file name. Pure — no DOM, no fs. */
|
|
@@ -142,6 +113,8 @@ export function sanitizeFileName(name: string): string {
|
|
|
142
113
|
.replace(/\s+/g, '-')
|
|
143
114
|
.replace(/[^a-z0-9\-_]/g, '')
|
|
144
115
|
.replace(/-+/g, '-')
|
|
145
|
-
|
|
116
|
+
// A leading `_` marks the fixed slots (`_working`, the pointer files), so
|
|
117
|
+
// no name a user types or a bundle carries may keep one.
|
|
118
|
+
.replace(/^[-_]+|-+$/g, '') || 'unnamed'
|
|
146
119
|
);
|
|
147
120
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
export const
|
|
3
|
+
/** Slug of the theme the editor has open — the document every edit belongs to. */
|
|
4
|
+
export const openThemeSlug = writable<string>('default');
|
|
@@ -85,7 +85,6 @@ export function normalizeComponents(state: EditorState): EditorState {
|
|
|
85
85
|
if (!slice || typeof slice !== 'object') continue;
|
|
86
86
|
components[name] = {
|
|
87
87
|
...slice,
|
|
88
|
-
activeFile: typeof slice.activeFile === 'string' ? slice.activeFile : 'default',
|
|
89
88
|
aliases: slice.aliases && typeof slice.aliases === 'object' ? slice.aliases : {},
|
|
90
89
|
config: slice.config && typeof slice.config === 'object' ? slice.config : {},
|
|
91
90
|
};
|