@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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-DBN0RPuT.cjs';
|
|
2
|
+
|
|
3
|
+
interface LegacyRename {
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface MigrateDataOptions {
|
|
9
|
+
/** The directory the three below sit under. Locates the pre-0.48
|
|
10
|
+
* `manifests/` directory, which no config key names any more. */
|
|
11
|
+
dataDir: string;
|
|
12
|
+
colorsAndTypeDir: string;
|
|
13
|
+
componentConfigsDir: string;
|
|
14
|
+
themesDir: string;
|
|
15
|
+
/** The retired `manifestsDir` config key, when the consumer set it. */
|
|
16
|
+
legacyManifestsDir?: string;
|
|
17
|
+
/** The installed package's `src/live-tokens/data`. Its files are read-only:
|
|
18
|
+
* they resolve names the consumer has no local copy of, and are never
|
|
19
|
+
* deleted even when their content matches a theme. */
|
|
20
|
+
packageDataDir?: string;
|
|
21
|
+
/** Plan only, write nothing. */
|
|
22
|
+
check?: boolean;
|
|
23
|
+
}
|
|
24
|
+
interface MigrateDataResult {
|
|
25
|
+
status: 'clean' | 'planned' | 'healed';
|
|
26
|
+
/** Directories moved to the 0.48 layout, old → new, in the order they ran. */
|
|
27
|
+
renames: LegacyRename[];
|
|
28
|
+
/** The theme production resolves to after the heal, and how it was decided. */
|
|
29
|
+
production: {
|
|
30
|
+
slug: string;
|
|
31
|
+
how: 'all-default' | 'matched' | 'recovered';
|
|
32
|
+
} | null;
|
|
33
|
+
/** Path of the theme written to record an unmatched production state. */
|
|
34
|
+
recoveredThemePath: string | null;
|
|
35
|
+
/** Buffers filled because the live state differed from the open theme. */
|
|
36
|
+
workingWritten: string[];
|
|
37
|
+
/** Buffers left as they were: the new server already owns them. */
|
|
38
|
+
workingKept: string[];
|
|
39
|
+
/** Materialised copies removed. */
|
|
40
|
+
deletedFiles: string[];
|
|
41
|
+
/** Retired per-layer pointer files removed. */
|
|
42
|
+
deletedPointers: string[];
|
|
43
|
+
/** Named files matching no theme. Yours, so they stay. */
|
|
44
|
+
keptUserFiles: string[];
|
|
45
|
+
/** Files in the themes directory that are not themes. Read past, never
|
|
46
|
+
* rewritten. */
|
|
47
|
+
notThemes: string[];
|
|
48
|
+
/** One sentence per reference a theme named that resolved to nothing. The
|
|
49
|
+
* theme carries the shipped default there now. */
|
|
50
|
+
droppedRefs: string[];
|
|
51
|
+
/** A local `colors-and-type/default.json` whose content differs from the copy
|
|
52
|
+
* the package ships. It shadows the shipped default. Reported only. */
|
|
53
|
+
shadowedDefaults: string[];
|
|
54
|
+
/** Themes rewritten from the pre-v3 by-reference form, so step 3 cannot
|
|
55
|
+
* delete a file they still name. */
|
|
56
|
+
upgradedThemes: string[];
|
|
57
|
+
}
|
|
58
|
+
declare function migrateData(opts: MigrateDataOptions): MigrateDataResult;
|
|
59
|
+
|
|
60
|
+
export { type MigrateDataOptions, type MigrateDataResult, migrateData };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-DBN0RPuT.js';
|
|
2
|
+
|
|
3
|
+
interface LegacyRename {
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface MigrateDataOptions {
|
|
9
|
+
/** The directory the three below sit under. Locates the pre-0.48
|
|
10
|
+
* `manifests/` directory, which no config key names any more. */
|
|
11
|
+
dataDir: string;
|
|
12
|
+
colorsAndTypeDir: string;
|
|
13
|
+
componentConfigsDir: string;
|
|
14
|
+
themesDir: string;
|
|
15
|
+
/** The retired `manifestsDir` config key, when the consumer set it. */
|
|
16
|
+
legacyManifestsDir?: string;
|
|
17
|
+
/** The installed package's `src/live-tokens/data`. Its files are read-only:
|
|
18
|
+
* they resolve names the consumer has no local copy of, and are never
|
|
19
|
+
* deleted even when their content matches a theme. */
|
|
20
|
+
packageDataDir?: string;
|
|
21
|
+
/** Plan only, write nothing. */
|
|
22
|
+
check?: boolean;
|
|
23
|
+
}
|
|
24
|
+
interface MigrateDataResult {
|
|
25
|
+
status: 'clean' | 'planned' | 'healed';
|
|
26
|
+
/** Directories moved to the 0.48 layout, old → new, in the order they ran. */
|
|
27
|
+
renames: LegacyRename[];
|
|
28
|
+
/** The theme production resolves to after the heal, and how it was decided. */
|
|
29
|
+
production: {
|
|
30
|
+
slug: string;
|
|
31
|
+
how: 'all-default' | 'matched' | 'recovered';
|
|
32
|
+
} | null;
|
|
33
|
+
/** Path of the theme written to record an unmatched production state. */
|
|
34
|
+
recoveredThemePath: string | null;
|
|
35
|
+
/** Buffers filled because the live state differed from the open theme. */
|
|
36
|
+
workingWritten: string[];
|
|
37
|
+
/** Buffers left as they were: the new server already owns them. */
|
|
38
|
+
workingKept: string[];
|
|
39
|
+
/** Materialised copies removed. */
|
|
40
|
+
deletedFiles: string[];
|
|
41
|
+
/** Retired per-layer pointer files removed. */
|
|
42
|
+
deletedPointers: string[];
|
|
43
|
+
/** Named files matching no theme. Yours, so they stay. */
|
|
44
|
+
keptUserFiles: string[];
|
|
45
|
+
/** Files in the themes directory that are not themes. Read past, never
|
|
46
|
+
* rewritten. */
|
|
47
|
+
notThemes: string[];
|
|
48
|
+
/** One sentence per reference a theme named that resolved to nothing. The
|
|
49
|
+
* theme carries the shipped default there now. */
|
|
50
|
+
droppedRefs: string[];
|
|
51
|
+
/** A local `colors-and-type/default.json` whose content differs from the copy
|
|
52
|
+
* the package ships. It shadows the shipped default. Reported only. */
|
|
53
|
+
shadowedDefaults: string[];
|
|
54
|
+
/** Themes rewritten from the pre-v3 by-reference form, so step 3 cannot
|
|
55
|
+
* delete a file they still name. */
|
|
56
|
+
upgradedThemes: string[];
|
|
57
|
+
}
|
|
58
|
+
declare function migrateData(opts: MigrateDataOptions): MigrateDataResult;
|
|
59
|
+
|
|
60
|
+
export { type MigrateDataOptions, type MigrateDataResult, migrateData };
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import {
|
|
2
|
+
THEME_SCHEMA_VERSION,
|
|
3
|
+
applyLegacyRenames,
|
|
4
|
+
detectLegacyLayout,
|
|
5
|
+
isColorsAndTypeShaped,
|
|
6
|
+
nextAvailableName,
|
|
7
|
+
normalizeTheme,
|
|
8
|
+
planLegacyRenames,
|
|
9
|
+
versionedFileResourceServer
|
|
10
|
+
} from "../chunk-6OZFXIQI.js";
|
|
11
|
+
import {
|
|
12
|
+
readLiveTokensConfig,
|
|
13
|
+
resolveDataDirs
|
|
14
|
+
} from "../chunk-D3ZVKOR4.js";
|
|
15
|
+
|
|
16
|
+
// vite-plugin/migrateData/migrateData.ts
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
var RECOVERED_SLUG = "recovered-production";
|
|
20
|
+
var RECOVERED_NAME = "Recovered production";
|
|
21
|
+
var MARKER_KEYS = ["_fileName", "_source"];
|
|
22
|
+
var VOLATILE_KEYS = [...MARKER_KEYS, "updatedAt"];
|
|
23
|
+
function isObject(value) {
|
|
24
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function sortDeep(value) {
|
|
27
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
28
|
+
if (!isObject(value)) return value;
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const key of Object.keys(value).sort()) out[key] = sortDeep(value[key]);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function comparable(value) {
|
|
34
|
+
if (!isObject(value)) return JSON.stringify(sortDeep(value));
|
|
35
|
+
const stable = {};
|
|
36
|
+
for (const key of Object.keys(value).sort()) {
|
|
37
|
+
if (VOLATILE_KEYS.includes(key)) continue;
|
|
38
|
+
stable[key] = sortDeep(value[key]);
|
|
39
|
+
}
|
|
40
|
+
return JSON.stringify(stable);
|
|
41
|
+
}
|
|
42
|
+
function sameContent(a, b) {
|
|
43
|
+
return comparable(a) === comparable(b);
|
|
44
|
+
}
|
|
45
|
+
function stripMarkers(value) {
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const [key, v] of Object.entries(value)) {
|
|
48
|
+
if (!MARKER_KEYS.includes(key)) out[key] = v;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function readPointerName(file, key) {
|
|
53
|
+
if (!fs.existsSync(file)) return null;
|
|
54
|
+
try {
|
|
55
|
+
const name = JSON.parse(fs.readFileSync(file, "utf-8"))[key];
|
|
56
|
+
return typeof name === "string" && name ? name : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function localNamedFiles(dir) {
|
|
62
|
+
if (!fs.existsSync(dir)) return [];
|
|
63
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.startsWith("_")).map((f) => f.slice(0, -".json".length)).filter((name) => name !== "default");
|
|
64
|
+
}
|
|
65
|
+
function componentNames(componentConfigsDir) {
|
|
66
|
+
if (!fs.existsSync(componentConfigsDir)) return [];
|
|
67
|
+
return fs.readdirSync(componentConfigsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
68
|
+
}
|
|
69
|
+
function shippedNames(packageDataDir, subdir) {
|
|
70
|
+
try {
|
|
71
|
+
const pkgRoot = path.resolve(packageDataDir, "..", "..", "..");
|
|
72
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf-8"));
|
|
73
|
+
const files = Array.isArray(pkg.files) ? pkg.files : [];
|
|
74
|
+
const prefix = `src/live-tokens/data/${subdir}/`;
|
|
75
|
+
return files.filter((f) => f.startsWith(prefix) && /^[^/]+\.json$/.test(f.slice(prefix.length))).map((f) => path.basename(f, ".json"));
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function migrateData(opts) {
|
|
81
|
+
const { componentConfigsDir, check = false } = opts;
|
|
82
|
+
const packageDataDir = opts.packageDataDir;
|
|
83
|
+
const layoutInput = {
|
|
84
|
+
dataDir: opts.dataDir,
|
|
85
|
+
colorsAndTypeDir: opts.colorsAndTypeDir,
|
|
86
|
+
themesDir: opts.themesDir,
|
|
87
|
+
configuredManifestsDir: opts.legacyManifestsDir
|
|
88
|
+
};
|
|
89
|
+
const legacyLayout = detectLegacyLayout(layoutInput);
|
|
90
|
+
const renames = legacyLayout ? planLegacyRenames(legacyLayout, layoutInput) : [];
|
|
91
|
+
if (legacyLayout && !check) applyLegacyRenames(renames);
|
|
92
|
+
const planningLegacy = legacyLayout !== null && check;
|
|
93
|
+
const colorsAndTypeDir = planningLegacy ? opts.themesDir : opts.colorsAndTypeDir;
|
|
94
|
+
const themesDir = planningLegacy ? legacyLayout.manifestsDir : opts.themesDir;
|
|
95
|
+
const reported = (p) => {
|
|
96
|
+
if (!planningLegacy) return p;
|
|
97
|
+
for (const { from, to } of renames) {
|
|
98
|
+
if (p.startsWith(from + path.sep)) return path.join(to, p.slice(from.length + 1));
|
|
99
|
+
}
|
|
100
|
+
return p;
|
|
101
|
+
};
|
|
102
|
+
const colorsRes = versionedFileResourceServer({
|
|
103
|
+
dir: colorsAndTypeDir,
|
|
104
|
+
packageDir: packageDataDir ? path.join(packageDataDir, "colors-and-type") : void 0,
|
|
105
|
+
packageOwnedNames: packageDataDir ? shippedNames(packageDataDir, "colors-and-type") : void 0
|
|
106
|
+
});
|
|
107
|
+
const themesRes = versionedFileResourceServer({
|
|
108
|
+
dir: themesDir,
|
|
109
|
+
packageDir: packageDataDir ? path.join(packageDataDir, "themes") : void 0
|
|
110
|
+
});
|
|
111
|
+
const componentResources = /* @__PURE__ */ new Map();
|
|
112
|
+
const compRes = (comp) => {
|
|
113
|
+
let r = componentResources.get(comp);
|
|
114
|
+
if (!r) {
|
|
115
|
+
r = versionedFileResourceServer({
|
|
116
|
+
dir: path.join(componentConfigsDir, comp),
|
|
117
|
+
packageDir: packageDataDir ? path.join(packageDataDir, "component-configs", comp) : void 0,
|
|
118
|
+
packageOwnedNames: packageDataDir ? shippedNames(packageDataDir, `component-configs/${comp}`) : void 0
|
|
119
|
+
});
|
|
120
|
+
componentResources.set(comp, r);
|
|
121
|
+
}
|
|
122
|
+
return r;
|
|
123
|
+
};
|
|
124
|
+
const comps = componentNames(componentConfigsDir);
|
|
125
|
+
const legacyPointers = [];
|
|
126
|
+
for (const dir of [colorsAndTypeDir, ...comps.map((c) => path.join(componentConfigsDir, c))]) {
|
|
127
|
+
for (const name of ["_active.json", "_production.json"]) {
|
|
128
|
+
const p = path.join(dir, name);
|
|
129
|
+
if (fs.existsSync(p)) legacyPointers.push(p);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const empty = {
|
|
133
|
+
status: "clean",
|
|
134
|
+
renames,
|
|
135
|
+
production: null,
|
|
136
|
+
recoveredThemePath: null,
|
|
137
|
+
workingWritten: [],
|
|
138
|
+
workingKept: [],
|
|
139
|
+
deletedFiles: [],
|
|
140
|
+
deletedPointers: [],
|
|
141
|
+
keptUserFiles: [],
|
|
142
|
+
notThemes: [],
|
|
143
|
+
droppedRefs: [],
|
|
144
|
+
shadowedDefaults: [],
|
|
145
|
+
upgradedThemes: []
|
|
146
|
+
};
|
|
147
|
+
if (legacyPointers.length === 0) {
|
|
148
|
+
if (!legacyLayout) return empty;
|
|
149
|
+
return { ...empty, status: check ? "planned" : "healed" };
|
|
150
|
+
}
|
|
151
|
+
const resolvers = {
|
|
152
|
+
readColorsAndType: (name) => colorsRes.readJson(name),
|
|
153
|
+
readComponentConfig: (comp, name) => compRes(comp).readJson(name),
|
|
154
|
+
normalizeColorsAndType: (colorsAndType) => colorsAndType
|
|
155
|
+
};
|
|
156
|
+
const upgradedThemes = [];
|
|
157
|
+
const notThemes = [];
|
|
158
|
+
const droppedRefs = [];
|
|
159
|
+
const themes = [];
|
|
160
|
+
for (const slug of themesRes.listNames()) {
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = themesRes.readJson(slug);
|
|
164
|
+
} catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!raw) continue;
|
|
168
|
+
if (isColorsAndTypeShaped(raw)) {
|
|
169
|
+
notThemes.push(themesRes.filePath(slug));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const { theme, migrated, dropped } = normalizeTheme(raw, resolvers);
|
|
173
|
+
themes.push({ slug, theme });
|
|
174
|
+
for (const ref of dropped) {
|
|
175
|
+
droppedRefs.push(
|
|
176
|
+
`theme "${slug}": the reference ${ref} resolved to nothing, so it carries the shipped default instead`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (migrated && fs.existsSync(themesRes.filePath(slug))) {
|
|
180
|
+
upgradedThemes.push(themesRes.filePath(slug));
|
|
181
|
+
if (!check) fs.writeFileSync(themesRes.filePath(slug), JSON.stringify(theme, null, 2));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const themeBySlug = new Map(themes.map((t) => [t.slug, t.theme]));
|
|
185
|
+
const resolveName = (res, dir, file, key) => {
|
|
186
|
+
const name = readPointerName(path.join(dir, file), key);
|
|
187
|
+
if (!name || res.existingPath(name) === null) return "default";
|
|
188
|
+
return name;
|
|
189
|
+
};
|
|
190
|
+
const readPointed = (res, name) => {
|
|
191
|
+
try {
|
|
192
|
+
return res.readJson(name);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
const file = res.existingPath(name) ?? res.filePath(name);
|
|
195
|
+
throw new Error(
|
|
196
|
+
`${file} will not parse (${err instanceof Error ? err.message : String(err)}), and a retired pointer names it. Fix or delete the file, then run the migration again.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const defaultConfig = (comp) => compRes(comp).readJson("default");
|
|
201
|
+
const themeConfigFor = (theme, comp) => {
|
|
202
|
+
const embedded = theme.componentConfigs[comp];
|
|
203
|
+
return embedded ? { ...embedded, component: comp } : defaultConfig(comp);
|
|
204
|
+
};
|
|
205
|
+
const prodColorsName = resolveName(colorsRes, colorsAndTypeDir, "_production.json", "productionFile");
|
|
206
|
+
const prodConfigNames = new Map(
|
|
207
|
+
comps.map((comp) => [
|
|
208
|
+
comp,
|
|
209
|
+
resolveName(compRes(comp), path.join(componentConfigsDir, comp), "_production.json", "productionFile")
|
|
210
|
+
])
|
|
211
|
+
);
|
|
212
|
+
const prodColors = readPointed(colorsRes, prodColorsName);
|
|
213
|
+
const prodConfigs = new Map(
|
|
214
|
+
comps.map((comp) => [comp, readPointed(compRes(comp), prodConfigNames.get(comp))])
|
|
215
|
+
);
|
|
216
|
+
const allDefault = prodColorsName === "default" && [...prodConfigNames.values()].every((n) => n === "default");
|
|
217
|
+
const matchesPointedProduction = (theme) => sameContent(theme.colorsAndType, prodColors) && comps.every((comp) => sameContent(themeConfigFor(theme, comp), prodConfigs.get(comp)));
|
|
218
|
+
let production;
|
|
219
|
+
let recoveredTheme = null;
|
|
220
|
+
let recoveredThemePath = null;
|
|
221
|
+
if (allDefault) {
|
|
222
|
+
production = { slug: "default", how: "all-default" };
|
|
223
|
+
} else {
|
|
224
|
+
const match = themes.find((t) => matchesPointedProduction(t.theme));
|
|
225
|
+
if (match) {
|
|
226
|
+
production = { slug: match.slug, how: "matched" };
|
|
227
|
+
} else {
|
|
228
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
229
|
+
const componentConfigs = {};
|
|
230
|
+
for (const comp of comps) {
|
|
231
|
+
const pointed = prodConfigs.get(comp);
|
|
232
|
+
if (!isObject(pointed)) continue;
|
|
233
|
+
if (sameContent(pointed, defaultConfig(comp))) continue;
|
|
234
|
+
componentConfigs[comp] = stripMarkers(pointed);
|
|
235
|
+
}
|
|
236
|
+
recoveredTheme = {
|
|
237
|
+
name: RECOVERED_NAME,
|
|
238
|
+
createdAt: now,
|
|
239
|
+
updatedAt: now,
|
|
240
|
+
schemaVersion: THEME_SCHEMA_VERSION,
|
|
241
|
+
colorsAndType: isObject(prodColors) ? stripMarkers(prodColors) : null,
|
|
242
|
+
componentConfigs
|
|
243
|
+
};
|
|
244
|
+
const slug = nextAvailableName((n) => themesRes.existingPath(n) !== null, RECOVERED_SLUG);
|
|
245
|
+
recoveredThemePath = themesRes.filePath(slug);
|
|
246
|
+
production = { slug, how: "recovered" };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const activeThemeSlug = themesRes.getActiveName();
|
|
250
|
+
const activeTheme = themeBySlug.get(activeThemeSlug) ?? null;
|
|
251
|
+
const workingWritten = [];
|
|
252
|
+
const workingKept = [];
|
|
253
|
+
const planWorking = (res, live, saved) => {
|
|
254
|
+
if (!isObject(live) || sameContent(live, saved)) return;
|
|
255
|
+
if (res.hasWorking()) {
|
|
256
|
+
workingKept.push(res.workingPath);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
workingWritten.push(res.workingPath);
|
|
260
|
+
if (!check) res.writeWorking(stripMarkers(live));
|
|
261
|
+
};
|
|
262
|
+
const activeColorsName = resolveName(colorsRes, colorsAndTypeDir, "_active.json", "activeFile");
|
|
263
|
+
planWorking(
|
|
264
|
+
colorsRes,
|
|
265
|
+
readPointed(colorsRes, activeColorsName),
|
|
266
|
+
activeTheme?.colorsAndType ?? colorsRes.readJson("default")
|
|
267
|
+
);
|
|
268
|
+
for (const comp of comps) {
|
|
269
|
+
const activeName = resolveName(
|
|
270
|
+
compRes(comp),
|
|
271
|
+
path.join(componentConfigsDir, comp),
|
|
272
|
+
"_active.json",
|
|
273
|
+
"activeFile"
|
|
274
|
+
);
|
|
275
|
+
planWorking(
|
|
276
|
+
compRes(comp),
|
|
277
|
+
readPointed(compRes(comp), activeName),
|
|
278
|
+
activeTheme ? themeConfigFor(activeTheme, comp) : defaultConfig(comp)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const deletedFiles = [];
|
|
282
|
+
const keptUserFiles = [];
|
|
283
|
+
const sweep = (res, dir, savedCopies) => {
|
|
284
|
+
for (const name of localNamedFiles(dir)) {
|
|
285
|
+
if (res.isPackageFile(name)) continue;
|
|
286
|
+
let content;
|
|
287
|
+
try {
|
|
288
|
+
content = res.readJson(name);
|
|
289
|
+
} catch {
|
|
290
|
+
keptUserFiles.push(res.filePath(name));
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const matched = themes.some((t) => sameContent(savedCopies(t.theme), content));
|
|
294
|
+
if (!matched) {
|
|
295
|
+
keptUserFiles.push(res.filePath(name));
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
deletedFiles.push(res.filePath(name));
|
|
299
|
+
if (!check) fs.unlinkSync(res.filePath(name));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
sweep(colorsRes, colorsAndTypeDir, (theme) => theme.colorsAndType);
|
|
303
|
+
for (const comp of comps) {
|
|
304
|
+
sweep(compRes(comp), path.join(componentConfigsDir, comp), (theme) => themeConfigFor(theme, comp));
|
|
305
|
+
}
|
|
306
|
+
const shadowedDefaults = [];
|
|
307
|
+
const localDefault = path.join(colorsAndTypeDir, "default.json");
|
|
308
|
+
const packageDefault = packageDataDir ? path.join(packageDataDir, "colors-and-type", "default.json") : null;
|
|
309
|
+
if (packageDefault && fs.existsSync(localDefault) && fs.existsSync(packageDefault) && path.resolve(localDefault) !== path.resolve(packageDefault)) {
|
|
310
|
+
try {
|
|
311
|
+
const local = JSON.parse(fs.readFileSync(localDefault, "utf-8"));
|
|
312
|
+
const shipped = JSON.parse(fs.readFileSync(packageDefault, "utf-8"));
|
|
313
|
+
if (!sameContent(local, shipped)) shadowedDefaults.push(localDefault);
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!check) {
|
|
318
|
+
for (const p of legacyPointers) fs.unlinkSync(p);
|
|
319
|
+
}
|
|
320
|
+
if (!check) {
|
|
321
|
+
if (recoveredTheme && recoveredThemePath) {
|
|
322
|
+
themesRes.ensureDir();
|
|
323
|
+
fs.writeFileSync(recoveredThemePath, JSON.stringify(recoveredTheme, null, 2));
|
|
324
|
+
}
|
|
325
|
+
themesRes.ensureDir();
|
|
326
|
+
themesRes.setProductionName(production.slug);
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
status: check ? "planned" : "healed",
|
|
330
|
+
renames,
|
|
331
|
+
production,
|
|
332
|
+
recoveredThemePath: recoveredThemePath && reported(recoveredThemePath),
|
|
333
|
+
workingWritten: workingWritten.map(reported),
|
|
334
|
+
workingKept: workingKept.map(reported),
|
|
335
|
+
deletedFiles: deletedFiles.map(reported),
|
|
336
|
+
deletedPointers: legacyPointers.map(reported),
|
|
337
|
+
keptUserFiles: keptUserFiles.map(reported),
|
|
338
|
+
notThemes: notThemes.map(reported),
|
|
339
|
+
droppedRefs,
|
|
340
|
+
shadowedDefaults: shadowedDefaults.map(reported),
|
|
341
|
+
upgradedThemes: upgradedThemes.map(reported)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export {
|
|
345
|
+
migrateData,
|
|
346
|
+
readLiveTokensConfig,
|
|
347
|
+
resolveDataDirs
|
|
348
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
interface CurveAnchor {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
inDx: number;
|
|
5
|
+
inDy: number;
|
|
6
|
+
outDx: number;
|
|
7
|
+
outDy: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Oklch {
|
|
11
|
+
l: number;
|
|
12
|
+
c: number;
|
|
13
|
+
h: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type HarmonyMode = 'complementary' | 'split-complementary' | 'triadic' | 'tetradic' | 'compound' | 'square' | 'analogous' | 'monochromatic' | 'custom';
|
|
17
|
+
interface HarmonyAxis {
|
|
18
|
+
/** Hue in [0, 360). Always present, bound or not. */
|
|
19
|
+
hue: number;
|
|
20
|
+
/** Bound family label (member of HARMONY_ELIGIBLE), or null when the axis is empty. */
|
|
21
|
+
family: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type GradientStyle = 'linear' | 'radial' | 'conic';
|
|
25
|
+
interface GradientStop {
|
|
26
|
+
position: number;
|
|
27
|
+
paletteLabel: string;
|
|
28
|
+
}
|
|
29
|
+
interface PaletteConfig {
|
|
30
|
+
/** Numeric OKLCH intent — the single stored basis. Unclamped: the store holds
|
|
31
|
+
* what the user meant; gamut clamping is projection-only (derivation output,
|
|
32
|
+
* swatch/canvas painting, hex/CSS serialization). */
|
|
33
|
+
baseColor: Oklch;
|
|
34
|
+
lightnessCurve: CurveAnchor[];
|
|
35
|
+
saturationCurve: CurveAnchor[];
|
|
36
|
+
scaleCurves: Record<string, {
|
|
37
|
+
lightness: CurveAnchor[];
|
|
38
|
+
saturation: CurveAnchor[];
|
|
39
|
+
}>;
|
|
40
|
+
curveOffset: Record<string, number>;
|
|
41
|
+
overrides: Record<string, Oklch>;
|
|
42
|
+
snappedScales: string[];
|
|
43
|
+
emptyMode?: 'solid' | 'gradient';
|
|
44
|
+
gradientStyle?: GradientStyle;
|
|
45
|
+
gradientAngle?: number;
|
|
46
|
+
gradientReverse?: boolean;
|
|
47
|
+
gradientStops?: GradientStop[];
|
|
48
|
+
gradientSize?: 'page' | 'window';
|
|
49
|
+
anchorToBase?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Where the base color sits in the ramp while `anchorToBase` is on: the
|
|
52
|
+
* PALETTE_STEPS index whose curve anchors are pinned to the base color
|
|
53
|
+
* (lightness = base L, saturation = multiplier 100). `displacedL` /
|
|
54
|
+
* `displacedS` remember the y of a pre-existing anchor (endpoint or
|
|
55
|
+
* user-authored) the placement overwrote at that x, so moving the
|
|
56
|
+
* placement or toggling off restores it instead of deleting it.
|
|
57
|
+
* `priorLightnessEndpoints` / `priorSaturationEndpoints` remember the two
|
|
58
|
+
* endpoints exactly as they stood before the curve's first-ever placement
|
|
59
|
+
* (a smoothed reshape, not algebraically invertible the way a later
|
|
60
|
+
* scaled-handle re-placement is), so a clear can still restore the true
|
|
61
|
+
* original regardless of how many times the anchor has since moved.
|
|
62
|
+
*/
|
|
63
|
+
anchorPlacement?: {
|
|
64
|
+
step: number;
|
|
65
|
+
displacedL?: number;
|
|
66
|
+
displacedS?: number;
|
|
67
|
+
priorLightnessEndpoints?: [CurveAnchor, CurveAnchor];
|
|
68
|
+
priorSaturationEndpoints?: [CurveAnchor, CurveAnchor];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Set to true by importers when they overlay `cssVariables[--color-{ns}-*]`
|
|
72
|
+
* without owning the typed-state curves. The storage-layer reconciler uses
|
|
73
|
+
* it as an opt-in switch: snap `baseColor` to the imported
|
|
74
|
+
* `--color-{ns}-500` anchor and clear the flag. Editor-authored themes
|
|
75
|
+
* never set this, so the reconciler is a strict no-op for them.
|
|
76
|
+
*
|
|
77
|
+
* Persists on disk for first-load reconciliation. After reconcile strips
|
|
78
|
+
* the palette-derived keys from `cssVariables`, subsequent reconciles find
|
|
79
|
+
* no anchor and become idempotent no-ops regardless of the flag's value.
|
|
80
|
+
*/
|
|
81
|
+
_imported?: boolean;
|
|
82
|
+
}
|
|
83
|
+
type FontSourceKind = 'google' | 'typekit' | 'css-url' | 'font-face';
|
|
84
|
+
interface FontFamily {
|
|
85
|
+
id: string;
|
|
86
|
+
name: string;
|
|
87
|
+
cssName: string;
|
|
88
|
+
weights?: number[];
|
|
89
|
+
italics?: boolean;
|
|
90
|
+
}
|
|
91
|
+
interface FontSource {
|
|
92
|
+
id: string;
|
|
93
|
+
kind: FontSourceKind;
|
|
94
|
+
url?: string;
|
|
95
|
+
cssText?: string;
|
|
96
|
+
families: FontFamily[];
|
|
97
|
+
label?: string;
|
|
98
|
+
}
|
|
99
|
+
type SystemCascadePreset = 'system-ui-sans' | 'system-ui-serif' | 'system-ui-mono';
|
|
100
|
+
type GenericFamily = 'sans-serif' | 'serif' | 'monospace' | 'cursive' | 'fantasy';
|
|
101
|
+
type FontStackVariable = '--font-display' | '--font-sans' | '--font-serif' | '--font-mono';
|
|
102
|
+
type FontStackSlot = {
|
|
103
|
+
kind: 'project';
|
|
104
|
+
familyId: string;
|
|
105
|
+
} | {
|
|
106
|
+
kind: 'system';
|
|
107
|
+
preset: SystemCascadePreset;
|
|
108
|
+
} | {
|
|
109
|
+
kind: 'generic';
|
|
110
|
+
value: GenericFamily;
|
|
111
|
+
};
|
|
112
|
+
interface FontStack {
|
|
113
|
+
variable: FontStackVariable;
|
|
114
|
+
slots: FontStackSlot[];
|
|
115
|
+
}
|
|
116
|
+
/** On-disk shape of one `--gradient-N` library token. Structural mirror of the
|
|
117
|
+
* store's `GradientToken` (like `AliasDiskValue`'s inline gradient shape) so
|
|
118
|
+
* the file schema stays independent of store types. The rendered strings in
|
|
119
|
+
* `cssVariables` are a projection of these for production CSS; this field is
|
|
120
|
+
* the editable basis. */
|
|
121
|
+
interface GradientDiskToken {
|
|
122
|
+
variable: string;
|
|
123
|
+
type: 'linear' | 'radial' | 'solid' | 'none';
|
|
124
|
+
angle: number;
|
|
125
|
+
radius?: number;
|
|
126
|
+
centerX?: number;
|
|
127
|
+
aspectX?: number;
|
|
128
|
+
aspectY?: number;
|
|
129
|
+
stops: {
|
|
130
|
+
position: number;
|
|
131
|
+
color: string;
|
|
132
|
+
opacity?: number;
|
|
133
|
+
}[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Where a live read resolved from: the unsaved `_working` buffer, the open
|
|
137
|
+
* theme's embedded copy, or the shipped default. It says which layer answered,
|
|
138
|
+
* never whether the content was edited — applying a theme fills the buffer for
|
|
139
|
+
* every layer it carries.
|
|
140
|
+
*/
|
|
141
|
+
type LiveSource = 'working' | 'theme' | 'default';
|
|
142
|
+
interface ColorsAndType {
|
|
143
|
+
name: string;
|
|
144
|
+
createdAt: string;
|
|
145
|
+
updatedAt: string;
|
|
146
|
+
editorConfigs: Record<string, PaletteConfig>;
|
|
147
|
+
cssVariables: Record<string, string>;
|
|
148
|
+
fontSources?: FontSource[];
|
|
149
|
+
fontStacks?: FontStack[];
|
|
150
|
+
/** Absent on files saved before gradients round-tripped; the loader keeps
|
|
151
|
+
* the stock defaults then, which match what those files rendered. */
|
|
152
|
+
gradients?: GradientDiskToken[];
|
|
153
|
+
/** Four fixed harmony axes, each owning a hue with an optional bound family. */
|
|
154
|
+
harmonyAxes?: HarmonyAxis[];
|
|
155
|
+
/**
|
|
156
|
+
* Server-attached marker naming the open theme — the document this content
|
|
157
|
+
* belongs to, not a colors-and-type file. Set by `themeFileApi`'s GET
|
|
158
|
+
* handlers; read by `themeInit` to seed `openThemeSlug`. Not persisted.
|
|
159
|
+
*/
|
|
160
|
+
_fileName?: string;
|
|
161
|
+
/** Server-attached marker: which of the three live layers answered. */
|
|
162
|
+
_source?: LiveSource;
|
|
163
|
+
/**
|
|
164
|
+
* Migration stamp. Absent on legacy files, treated as 0; the loader runs
|
|
165
|
+
* any registered theme migrations whose `fromVersion >= file.schemaVersion`.
|
|
166
|
+
* Save paths stamp the current value so resaved files skip past
|
|
167
|
+
* migrations.
|
|
168
|
+
*/
|
|
169
|
+
schemaVersion?: number;
|
|
170
|
+
}
|
|
171
|
+
/** On-disk shape of a single alias entry. Plain strings carry the bulk of
|
|
172
|
+
* aliases (token refs like `--surface-canvas-low` or literal CSS like `4px`);
|
|
173
|
+
* the gradient object shape is the structured payload for component-owned
|
|
174
|
+
* gradients that can't compress to a single string. */
|
|
175
|
+
type AliasDiskValue = string | {
|
|
176
|
+
kind: 'gradient';
|
|
177
|
+
value: {
|
|
178
|
+
type: 'linear' | 'radial' | 'solid' | 'none';
|
|
179
|
+
angle: number;
|
|
180
|
+
radius?: number;
|
|
181
|
+
centerX?: number;
|
|
182
|
+
aspectX?: number;
|
|
183
|
+
aspectY?: number;
|
|
184
|
+
stops: {
|
|
185
|
+
position: number;
|
|
186
|
+
color: string;
|
|
187
|
+
opacity?: number;
|
|
188
|
+
}[];
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
interface ComponentConfig {
|
|
192
|
+
name: string;
|
|
193
|
+
component: string;
|
|
194
|
+
createdAt: string;
|
|
195
|
+
updatedAt: string;
|
|
196
|
+
aliases: Record<string, AliasDiskValue>;
|
|
197
|
+
config?: Record<string, unknown>;
|
|
198
|
+
/**
|
|
199
|
+
* Server-attached marker. Same role as `ColorsAndType._fileName`: the open
|
|
200
|
+
* theme, not a config file. Set by the component-configs GET handlers.
|
|
201
|
+
*/
|
|
202
|
+
_fileName?: string;
|
|
203
|
+
/** Server-attached marker: which of the three live layers answered. */
|
|
204
|
+
_source?: LiveSource;
|
|
205
|
+
/**
|
|
206
|
+
* Migration stamp. Absent on legacy files, treated as 0. See `ColorsAndType.schemaVersion`.
|
|
207
|
+
*/
|
|
208
|
+
schemaVersion?: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type { AliasDiskValue as A, ColorsAndType as C, FontSource as F, GradientDiskToken as G, HarmonyMode as H, Oklch as O, ComponentConfig as a, FontStack as b };
|