@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
|
@@ -13,17 +13,18 @@
|
|
|
13
13
|
* - `editorRenderer` (DOM subscriber)
|
|
14
14
|
* - `editorPersistence` (debounced localStorage)
|
|
15
15
|
*
|
|
16
|
-
* Component-config +
|
|
17
|
-
* `loadComponentActive` / `seedComponentsFromApi` / `loadFromFile` / `
|
|
16
|
+
* Component-config + colors-and-type migrations live in `./migrations` (Wave 4).
|
|
17
|
+
* `loadComponentActive` / `seedComponentsFromApi` / `loadFromFile` / `toColorsAndType`
|
|
18
18
|
* orchestrate across slices and stay here.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { derived, writable, type Readable } from 'svelte/store';
|
|
21
22
|
import type { CssVarRef, EditorState, GradientAliasValue } from './editorTypes';
|
|
22
|
-
import type { AliasDiskValue,
|
|
23
|
+
import type { AliasDiskValue, ColorsAndType } from '../themes/themeTypes';
|
|
23
24
|
import { KNOWN_COMPONENT_CONFIG_KEYS } from '../components/componentConfigKeys';
|
|
24
25
|
import { cssStringToRef } from './cssVarRef';
|
|
25
26
|
import {
|
|
26
|
-
|
|
27
|
+
CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
|
|
27
28
|
CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
28
29
|
runMigrations,
|
|
29
30
|
} from '../themes/migrations';
|
|
@@ -68,7 +69,11 @@ import {
|
|
|
68
69
|
loadShadowsFromVars,
|
|
69
70
|
shadowsToVars,
|
|
70
71
|
} from '../themes/slices/shadows';
|
|
71
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
gradientsToVars,
|
|
74
|
+
loadGradientsFromFile,
|
|
75
|
+
makeDefaultGradients,
|
|
76
|
+
} from '../themes/slices/gradients';
|
|
72
77
|
import {
|
|
73
78
|
componentBaseline,
|
|
74
79
|
loadComponentsFromVars,
|
|
@@ -199,12 +204,12 @@ export {
|
|
|
199
204
|
export {
|
|
200
205
|
setFontSources,
|
|
201
206
|
setFontStacks,
|
|
202
|
-
|
|
207
|
+
seedFontsFromColorsAndType,
|
|
203
208
|
} from '../themes/slices/fonts';
|
|
204
209
|
|
|
205
210
|
export {
|
|
206
211
|
setPaletteConfig,
|
|
207
|
-
|
|
212
|
+
seedPalettesFromColorsAndType,
|
|
208
213
|
} from '../themes/slices/palettes';
|
|
209
214
|
|
|
210
215
|
// ── Component-config load orchestration ───────────────────────────────────
|
|
@@ -439,33 +444,44 @@ function splitAliasesAndConfig(
|
|
|
439
444
|
}
|
|
440
445
|
|
|
441
446
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
447
|
+
* Disk-shape config → in-memory slice, migrations included. `schemaVersion` is
|
|
448
|
+
* the stamp on the loaded file (0 for legacy files with no stamp); the runner
|
|
449
|
+
* applies any migrations between that and `CURRENT_COMPONENT_SCHEMA_VERSION`,
|
|
450
|
+
* so in-memory state is always at the current version.
|
|
445
451
|
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
|
|
449
|
-
|
|
452
|
+
* Pure: the store paths below commit the result, the theme preview renders
|
|
453
|
+
* one without committing.
|
|
454
|
+
*/
|
|
455
|
+
export function toComponentSlice(
|
|
456
|
+
component: string,
|
|
457
|
+
aliases: Record<string, AliasDiskValue>,
|
|
458
|
+
config?: Record<string, unknown>,
|
|
459
|
+
schemaVersion: number = 0,
|
|
460
|
+
): { aliases: Record<string, CssVarRef>; config: Record<string, unknown> } {
|
|
461
|
+
const migrated = migrateComponentAliases(component, aliases, config, schemaVersion);
|
|
462
|
+
return splitAliasesAndConfig(migrated.aliases, migrated.config);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Replace a component's slice with a loaded config's contents. Uses `mutate()`
|
|
467
|
+
* so the load is one undoable entry; updates the dirty baseline so the
|
|
468
|
+
* post-load state reads clean for this component.
|
|
450
469
|
*/
|
|
451
470
|
export function loadComponentActive(
|
|
452
471
|
component: string,
|
|
453
|
-
activeFile: string,
|
|
454
472
|
aliases: Record<string, AliasDiskValue>,
|
|
455
473
|
config?: Record<string, unknown>,
|
|
456
474
|
schemaVersion: number = 0,
|
|
457
475
|
): void {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
s.components[component] = { activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
|
|
476
|
+
const split = toComponentSlice(component, aliases, config, schemaVersion);
|
|
477
|
+
mutate(`load ${component}`, (s) => {
|
|
478
|
+
s.components[component] = { aliases: { ...split.aliases }, config: { ...split.config } };
|
|
462
479
|
});
|
|
463
480
|
setSavedComponentBaseline(component, componentBaseline(split));
|
|
464
481
|
notifyComponentSavedChanged();
|
|
465
482
|
}
|
|
466
483
|
|
|
467
484
|
export interface ComponentSeed {
|
|
468
|
-
activeFile: string;
|
|
469
485
|
aliases: Record<string, AliasDiskValue>;
|
|
470
486
|
config?: Record<string, unknown>;
|
|
471
487
|
schemaVersion?: number;
|
|
@@ -484,9 +500,8 @@ export function seedComponentsFromApi(
|
|
|
484
500
|
store.update((s) => {
|
|
485
501
|
s.components = {};
|
|
486
502
|
for (const [comp, cfg] of Object.entries(configs)) {
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
s.components[comp] = { activeFile: cfg.activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
|
|
503
|
+
const split = toComponentSlice(comp, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
|
|
504
|
+
s.components[comp] = { aliases: { ...split.aliases }, config: { ...split.config } };
|
|
490
505
|
setSavedComponentBaseline(comp, componentBaseline(split));
|
|
491
506
|
}
|
|
492
507
|
return s;
|
|
@@ -495,10 +510,10 @@ export function seedComponentsFromApi(
|
|
|
495
510
|
schedulePersist();
|
|
496
511
|
}
|
|
497
512
|
|
|
498
|
-
// ──
|
|
513
|
+
// ── Colors and type load / save ────────────────────────────────────────────
|
|
499
514
|
|
|
500
515
|
/**
|
|
501
|
-
* Per-domain loader: routes a freshly-loaded
|
|
516
|
+
* Per-domain loader: routes a freshly-loaded file's `cssVariables` bag
|
|
502
517
|
* into typed state on `next`, then removes the routed entries so the
|
|
503
518
|
* remainder lands as the catch-all `cssVars` bag. Each loader owns its
|
|
504
519
|
* domain's parser + name list; this table is the only place editorStore
|
|
@@ -514,13 +529,14 @@ const domainLoaders: Record<string, DomainLoader> = {
|
|
|
514
529
|
};
|
|
515
530
|
|
|
516
531
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
532
|
+
* Project a colors-and-type file onto a fresh `EditorState` without committing
|
|
533
|
+
* it. `loadFromFile` commits the projection; the theme preview renders one
|
|
534
|
+
* without touching the store, so both paths derive vars from the same shape.
|
|
519
535
|
*
|
|
520
|
-
* Reads `
|
|
521
|
-
* `
|
|
536
|
+
* Reads `schemaVersion` (absent = 0) and runs colors-and-type migrations up to
|
|
537
|
+
* `CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION` before splitting the bag into domains.
|
|
522
538
|
*/
|
|
523
|
-
export function
|
|
539
|
+
export function colorsAndTypeToState(colorsAndType: ColorsAndType): EditorState {
|
|
524
540
|
const next = emptyState();
|
|
525
541
|
// Structural palette migrations, innermost → outermost: rename Primary→Brand
|
|
526
542
|
// and Background→Canvas (both key renames, so they precede anything that looks
|
|
@@ -529,38 +545,44 @@ export function loadFromFile(theme: Theme): void {
|
|
|
529
545
|
// placement, fold a legacy background spot into the base color, then place any
|
|
530
546
|
// still-unplaced anchor by base luminance. The clone is still pre-basis (hex
|
|
531
547
|
// or already-numeric) on disk.
|
|
532
|
-
const raw = structuredClone(
|
|
548
|
+
const raw = structuredClone(colorsAndType.editorConfigs ?? {}) as Record<string, PreOklchPaletteConfig>;
|
|
533
549
|
next.palettes = placeUnplacedBaseAnchors(adoptBackgroundSpotAsBase(adoptLegacyBaseAnchor(migratePaletteColorsToOklch(unifyGrayPalettes(renameBackgroundPaletteKey(renamePrimaryPaletteKey(raw)))))));
|
|
534
|
-
next.fonts.sources = structuredClone(
|
|
535
|
-
next.fonts.stacks = structuredClone(
|
|
550
|
+
next.fonts.sources = structuredClone(colorsAndType.fontSources ?? []);
|
|
551
|
+
next.fonts.stacks = structuredClone(colorsAndType.fontStacks ?? []);
|
|
536
552
|
const rawVars = runMigrations(
|
|
537
|
-
'
|
|
538
|
-
|
|
539
|
-
|
|
553
|
+
'colors-and-type',
|
|
554
|
+
colorsAndType.schemaVersion ?? 0,
|
|
555
|
+
colorsAndType.cssVariables ?? {},
|
|
540
556
|
);
|
|
541
557
|
// Route domain-owned entries out of the catch-all bag into typed state.
|
|
542
558
|
// Order doesn't matter — each loader claims a disjoint set of var names
|
|
543
|
-
// (or in components' case, vars that wouldn't have been in the
|
|
559
|
+
// (or in components' case, vars that wouldn't have been in the file to
|
|
544
560
|
// begin with).
|
|
545
561
|
for (const load of Object.values(domainLoaders)) load(next, rawVars);
|
|
546
|
-
next
|
|
562
|
+
loadGradientsFromFile(next, colorsAndType.gradients, rawVars);
|
|
563
|
+
next.harmonyAxes = sanitizeHarmonyAxes(renameBackgroundHarmonyFamily(colorsAndType.harmonyAxes), next.palettes);
|
|
547
564
|
next.cssVars = rawVars;
|
|
565
|
+
return next;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Replace state with a loaded colors-and-type file. Clears history and marks
|
|
570
|
+
* saved — "open a different document" semantics. Undo cannot cross a load.
|
|
571
|
+
*/
|
|
572
|
+
export function loadFromFile(colorsAndType: ColorsAndType): void {
|
|
573
|
+
const next = colorsAndTypeToState(colorsAndType);
|
|
548
574
|
resetHistoryForLoad();
|
|
549
575
|
store.set(next);
|
|
576
|
+
markColorsAndTypeSaved(next);
|
|
550
577
|
schedulePersist();
|
|
551
578
|
}
|
|
552
579
|
|
|
553
580
|
/**
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
* everything not yet migrated to a typed domain.
|
|
558
|
-
*
|
|
559
|
-
* Stamps the file with `CURRENT_THEME_SCHEMA_VERSION` so future loads can
|
|
560
|
-
* skip migrations the file is already past.
|
|
581
|
+
* Everything a colors-and-type file carries except its identity and timestamps.
|
|
582
|
+
* Split out of `toColorsAndType` so the dirty baseline below compares exactly
|
|
583
|
+
* what a save would write, with nothing that changes on its own between two saves.
|
|
561
584
|
*/
|
|
562
|
-
|
|
563
|
-
const now = new Date().toISOString();
|
|
585
|
+
function colorsAndTypeContent(state: EditorState): Omit<ColorsAndType, 'name' | 'createdAt' | 'updatedAt'> {
|
|
564
586
|
const cssVariables: Record<string, string> = { ...state.cssVars };
|
|
565
587
|
if (!columnsEqualsDefault(state.columns)) {
|
|
566
588
|
Object.assign(cssVariables, columnsToVars(state.columns));
|
|
@@ -569,19 +591,59 @@ export function toTheme(state: EditorState, meta: { name: string }): Theme {
|
|
|
569
591
|
if (state.shadows.tokens.length > 0) {
|
|
570
592
|
Object.assign(cssVariables, shadowsToVars(state.shadows));
|
|
571
593
|
}
|
|
594
|
+
// Rendered gradient strings ride along in cssVariables for the production
|
|
595
|
+
// CSS pipeline; the structured `gradients` field is the editable basis.
|
|
596
|
+
if (state.gradients.tokens.length > 0) {
|
|
597
|
+
Object.assign(cssVariables, gradientsToVars(state.gradients));
|
|
598
|
+
}
|
|
572
599
|
return {
|
|
573
|
-
name: meta.name,
|
|
574
|
-
createdAt: now,
|
|
575
|
-
updatedAt: now,
|
|
576
600
|
editorConfigs: state.palettes,
|
|
577
601
|
cssVariables,
|
|
578
602
|
fontSources: state.fonts.sources,
|
|
579
603
|
fontStacks: state.fonts.stacks,
|
|
604
|
+
gradients: state.gradients.tokens,
|
|
580
605
|
harmonyAxes: state.harmonyAxes,
|
|
581
|
-
schemaVersion:
|
|
606
|
+
schemaVersion: CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
|
|
582
607
|
};
|
|
583
608
|
}
|
|
584
609
|
|
|
610
|
+
/**
|
|
611
|
+
* Serialize current state for saving. Domains with their own typed state
|
|
612
|
+
* (columns, overlays, shadows) fold derived vars into `cssVariables` only
|
|
613
|
+
* when they diverge from defaults; the catch-all `cssVars` bag carries
|
|
614
|
+
* everything not yet migrated to a typed domain.
|
|
615
|
+
*
|
|
616
|
+
* Stamps the file with `CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION` so future loads
|
|
617
|
+
* can skip migrations the file is already past.
|
|
618
|
+
*/
|
|
619
|
+
export function toColorsAndType(state: EditorState, meta: { name: string }): ColorsAndType {
|
|
620
|
+
const now = new Date().toISOString();
|
|
621
|
+
return { name: meta.name, createdAt: now, updatedAt: now, ...colorsAndTypeContent(state) };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── Colors-and-type dirty tracking ─────────────────────────────────────────
|
|
625
|
+
//
|
|
626
|
+
// `dirty` counts history entries, and component edits push history too, so it
|
|
627
|
+
// cannot answer whether the colors and type differ from their file. This is
|
|
628
|
+
// the components slice's baseline mechanism at colors-and-type scope: the
|
|
629
|
+
// serialized content of the last file read or written, compared against live
|
|
630
|
+
// state.
|
|
631
|
+
|
|
632
|
+
let savedColorsAndTypeContent = JSON.stringify(colorsAndTypeContent(emptyState()));
|
|
633
|
+
const colorsAndTypeSavedTick = writable(0);
|
|
634
|
+
|
|
635
|
+
export const colorsAndTypeDirty: Readable<boolean> = derived(
|
|
636
|
+
[store, colorsAndTypeSavedTick],
|
|
637
|
+
([$state]) => JSON.stringify(colorsAndTypeContent($state)) !== savedColorsAndTypeContent,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
/** Take `state` as what the colors-and-type file now holds. `loadFromFile`
|
|
641
|
+
* passes what it loaded, `persistColorsAndType` what it wrote. */
|
|
642
|
+
export function markColorsAndTypeSaved(state: EditorState): void {
|
|
643
|
+
savedColorsAndTypeContent = JSON.stringify(colorsAndTypeContent(state));
|
|
644
|
+
colorsAndTypeSavedTick.update((n) => n + 1);
|
|
645
|
+
}
|
|
646
|
+
|
|
585
647
|
// ── Persistence ────────────────────────────────────────────────────────────
|
|
586
648
|
//
|
|
587
649
|
// `schedulePersist` + `hydrate` + the eager-hydrate gate live in
|
|
@@ -604,6 +666,7 @@ export function __resetForTests(): void {
|
|
|
604
666
|
__resetCoreForTests();
|
|
605
667
|
__resetRendererCacheForTests();
|
|
606
668
|
__resetComponentsForTests();
|
|
669
|
+
markColorsAndTypeSaved(emptyState());
|
|
607
670
|
}
|
|
608
671
|
|
|
609
672
|
// ── Wiring ─────────────────────────────────────────────────────────────────
|
|
@@ -53,7 +53,7 @@ export interface GradientSource {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Adapter for `--gradient-N` library tokens in `state.gradients.tokens`. */
|
|
56
|
-
export function
|
|
56
|
+
export function colorsAndTypeGradientSource(variable: string): GradientSource {
|
|
57
57
|
const current = derived(editorState, ($s) => {
|
|
58
58
|
const t = $s.gradients.tokens.find((g) => g.variable === variable);
|
|
59
59
|
if (!t) return undefined;
|
|
@@ -137,7 +137,7 @@ export function componentGradientSource(component: string, varName: string): Gra
|
|
|
137
137
|
* theme-gradient path. */
|
|
138
138
|
const update = (label: string, mutator: (g: GradientAliasValue) => void): void => {
|
|
139
139
|
mutate(label, (s) => {
|
|
140
|
-
const slice = s.components[component] ?? (s.components[component] = {
|
|
140
|
+
const slice = s.components[component] ?? (s.components[component] = { aliases: {}, config: {} });
|
|
141
141
|
const ref = slice.aliases[varName];
|
|
142
142
|
// Seed the editable base from the existing gradient, or lift the current
|
|
143
143
|
// flat-colour alias into one so the first edit promotes (rather than
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { tick } from 'svelte';
|
|
2
|
+
import type { ColorsAndType, ColorsAndTypeMeta } from './themeTypes';
|
|
3
|
+
import type { EditorState } from '../store/editorTypes';
|
|
4
|
+
import {
|
|
5
|
+
versionedFileResource,
|
|
6
|
+
sanitizeFileName as sanitizeFileNameImpl,
|
|
7
|
+
} from '../storage/files/versionedFileResourceClient';
|
|
8
|
+
import { API_BASE } from '../storage/apiBase';
|
|
9
|
+
import { liveMovedSinceBake } from '../productionPulse';
|
|
10
|
+
import { loadFromFile as loadEditorState, toColorsAndType, markSaved, markColorsAndTypeSaved } from '../store/editorStore';
|
|
11
|
+
import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
|
|
12
|
+
import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
|
|
13
|
+
|
|
14
|
+
// ── API helpers ──────────────────────────────────────────────
|
|
15
|
+
//
|
|
16
|
+
// Named colors-and-type files are presets: the user saves, loads and deletes
|
|
17
|
+
// them, and nothing machine-written lands among them. The live colors and type
|
|
18
|
+
// are the `_working` buffer, read back through `/colors-and-type/active`.
|
|
19
|
+
|
|
20
|
+
const colorsAndTypeResource = versionedFileResource<ColorsAndType, ColorsAndTypeMeta>({
|
|
21
|
+
baseUrl: `${API_BASE}/colors-and-type`,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export async function listColorsAndType(): Promise<ColorsAndTypeMeta[]> {
|
|
25
|
+
const data = await colorsAndTypeResource.list();
|
|
26
|
+
return data.files;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const loadColorsAndType = (fileName: string): Promise<ColorsAndType> => colorsAndTypeResource.load(fileName);
|
|
30
|
+
export const saveColorsAndType = (fileName: string, data: ColorsAndType): Promise<void> =>
|
|
31
|
+
colorsAndTypeResource.save(fileName, data);
|
|
32
|
+
export const deleteColorsAndType = (fileName: string): Promise<void> => colorsAndTypeResource.remove(fileName);
|
|
33
|
+
|
|
34
|
+
/** The colors and type the page is running: the buffer, else the open theme's
|
|
35
|
+
* copy, else the shipped default. `_source` says which. */
|
|
36
|
+
export async function getActiveColorsAndType(): Promise<ColorsAndType | null> {
|
|
37
|
+
return colorsAndTypeResource.getActive();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Write the unsaved buffer. Absence of the buffer means "the open theme's
|
|
41
|
+
* saved colors and type", so this is what puts the screen ahead of them. */
|
|
42
|
+
export async function writeWorkingColorsAndType(data: ColorsAndType): Promise<void> {
|
|
43
|
+
const res = await fetch(`${API_BASE}/colors-and-type/working`, {
|
|
44
|
+
method: 'PUT',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify(data),
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const err = await res.json().catch(() => ({ error: 'Write failed' }));
|
|
50
|
+
throw new Error(err.error || 'Write failed');
|
|
51
|
+
}
|
|
52
|
+
liveMovedSinceBake.set(true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Sanitize a display name to a safe file name. Re-exported from the shared
|
|
56
|
+
* `files/versionedFileResource` so the dev-server plugin can import the
|
|
57
|
+
* canonical pure helper without depending on this module's CSS imports. */
|
|
58
|
+
export const sanitizeFileName = sanitizeFileNameImpl;
|
|
59
|
+
|
|
60
|
+
// ── Colors and type flush / load orchestration ─────────────
|
|
61
|
+
//
|
|
62
|
+
// `persistColorsAndType` and `hydrateColorsAndType` are the canonical entry
|
|
63
|
+
// points for round-tripping editor state through the server. The caller —
|
|
64
|
+
// `ThemePanel`, which flushes the colors and type on screen before it captures
|
|
65
|
+
// or ships — handles only UI-level concerns (status flashing, error chrome).
|
|
66
|
+
|
|
67
|
+
/** Flush the editor state to the buffer under `displayName`, the name of the
|
|
68
|
+
* theme the buffer belongs to, and clear the dirty flag. A capture reads the
|
|
69
|
+
* buffer back, so this is what makes Save and Adopt mean the look on screen. */
|
|
70
|
+
export async function persistColorsAndType(
|
|
71
|
+
state: EditorState,
|
|
72
|
+
displayName: string,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
await tick();
|
|
75
|
+
const colorsAndType = toColorsAndType(state, { name: displayName });
|
|
76
|
+
await writeWorkingColorsAndType(colorsAndType);
|
|
77
|
+
markSaved();
|
|
78
|
+
markColorsAndTypeSaved(state);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Load colors and type into the editor state and re-apply font side-effects
|
|
82
|
+
* (@font-face rules + `--font-*` CSS vars on :root). */
|
|
83
|
+
export function hydrateColorsAndType(colorsAndType: ColorsAndType): void {
|
|
84
|
+
migrateColorsAndTypeFonts(colorsAndType);
|
|
85
|
+
loadEditorState(colorsAndType);
|
|
86
|
+
// Font data is in state.fonts via loadEditorState; the DOM-side-effect
|
|
87
|
+
// helpers still need to run so @font-face rules and --font-* CSS vars
|
|
88
|
+
// land on :root.
|
|
89
|
+
if (colorsAndType.fontSources && colorsAndType.fontSources.length > 0) {
|
|
90
|
+
applyFontSources(colorsAndType.fontSources);
|
|
91
|
+
}
|
|
92
|
+
if (colorsAndType.fontStacks && colorsAndType.fontStacks.length > 0) {
|
|
93
|
+
applyFontStacks(colorsAndType.fontStacks, colorsAndType.fontSources ?? []);
|
|
94
|
+
}
|
|
95
|
+
}
|