@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
|
@@ -16,34 +16,13 @@
|
|
|
16
16
|
setComponentAliasLinked,
|
|
17
17
|
clearComponentAliasLinked,
|
|
18
18
|
} from '../../core/store/editorStore';
|
|
19
|
+
import { rawKind, type TokenKind } from '../../core/components/aliasKinds';
|
|
19
20
|
import { getEditorContext } from './editorContext';
|
|
20
21
|
import type { Token } from './types';
|
|
21
22
|
|
|
22
23
|
/** Selector kind. `padding-split` is `padding` whose per-side variables exist;
|
|
23
24
|
it renders the four-sided field group instead of the single-value row. */
|
|
24
|
-
type Kind =
|
|
25
|
-
| 'surface'
|
|
26
|
-
| 'border'
|
|
27
|
-
| 'border-width'
|
|
28
|
-
| 'radius'
|
|
29
|
-
| 'divider-width'
|
|
30
|
-
| 'divider-height'
|
|
31
|
-
| 'divider-inset'
|
|
32
|
-
| 'dot-size'
|
|
33
|
-
| 'blur'
|
|
34
|
-
| 'scale'
|
|
35
|
-
| 'shadow'
|
|
36
|
-
| 'font-family'
|
|
37
|
-
| 'font-weight'
|
|
38
|
-
| 'font-size'
|
|
39
|
-
| 'line-height'
|
|
40
|
-
| 'letter-spacing'
|
|
41
|
-
| 'padding'
|
|
42
|
-
| 'padding-split'
|
|
43
|
-
| 'gap'
|
|
44
|
-
| 'duration'
|
|
45
|
-
| 'easing'
|
|
46
|
-
| 'text-color';
|
|
25
|
+
type Kind = TokenKind | 'padding-split';
|
|
47
26
|
|
|
48
27
|
type Entry = { kind: Kind; token: Token };
|
|
49
28
|
|
|
@@ -82,42 +61,6 @@
|
|
|
82
61
|
onchange,
|
|
83
62
|
}: Props = $props();
|
|
84
63
|
|
|
85
|
-
/** Suffix/prefix patterns mapped to kinds — single source of truth used by `rawKind`.
|
|
86
|
-
Order matters: `-text` must run before `-border`/`-surface` because `--text-*`
|
|
87
|
-
would otherwise match `surface`/`border` if any pattern overlapped. Variables
|
|
88
|
-
that don't match any pattern fall through to `text-color` (renders as a palette
|
|
89
|
-
picker). Tokens with unconventional suffixes should be renamed. */
|
|
90
|
-
const KIND_PATTERNS: Array<{ kind: Kind; matches: (v: string) => boolean }> = [
|
|
91
|
-
{ kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
|
|
92
|
-
{ kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
|
|
93
|
-
{ kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') || v.endsWith('-thumb-size') },
|
|
94
|
-
{ kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
|
|
95
|
-
{ kind: 'letter-spacing', matches: (v) => v.endsWith('-letter-spacing') },
|
|
96
|
-
{ kind: 'text-color', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
|
|
97
|
-
{ kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
|
|
98
|
-
{ kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
|
|
99
|
-
{ kind: 'divider-height', matches: (v) => v.endsWith('-divider-height') || v.endsWith('-track-height') },
|
|
100
|
-
{ kind: 'divider-inset', matches: (v) => v.endsWith('-divider-inset') },
|
|
101
|
-
{ kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
|
|
102
|
-
{ kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
|
|
103
|
-
{ kind: 'scale', matches: (v) => v.endsWith('-scale') || v.startsWith('--scale-') },
|
|
104
|
-
{ kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
|
|
105
|
-
{ kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
|
|
106
|
-
{ kind: 'gap', matches: (v) => v.endsWith('-gap') },
|
|
107
|
-
{ kind: 'duration', matches: (v) => v.endsWith('-duration') || v.startsWith('--duration-') },
|
|
108
|
-
{ kind: 'easing', matches: (v) => v.endsWith('-easing') || v.startsWith('--ease-') },
|
|
109
|
-
{ kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
|
|
110
|
-
{ kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
|
|
111
|
-
{ kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
function rawKind(variable: string): Kind {
|
|
115
|
-
for (const { kind, matches } of KIND_PATTERNS) {
|
|
116
|
-
if (matches(variable)) return kind;
|
|
117
|
-
}
|
|
118
|
-
return 'text-color';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
64
|
/** Fixed internal order for tokens within a layout. `padding-split` co-orders with `padding`. */
|
|
122
65
|
const baseKindOrder: Kind[] = [
|
|
123
66
|
'font-family',
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
const dstTypeGroups = typeGroups[toState] ?? [];
|
|
170
170
|
|
|
171
171
|
mutate(`copy ${fromVariant}/${fromState} → ${name}/${toState}`, (s) => {
|
|
172
|
-
const slice = s.components[component!] ?? (s.components[component!] = {
|
|
172
|
+
const slice = s.components[component!] ?? (s.components[component!] = { aliases: {}, config: {} });
|
|
173
173
|
const dstVarsTouched: string[] = [];
|
|
174
174
|
/** Resolve a variable's effective value as a CSS string: the override if
|
|
175
175
|
set, otherwise its declared default. Returns null if neither exists.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { matchesKind } from './aliasKinds';
|
|
2
|
+
import type { AliasDiskValue, ComponentConfig } from '../themes/themeTypes';
|
|
3
|
+
|
|
4
|
+
export type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
|
|
5
|
+
|
|
6
|
+
export interface AdjustOp {
|
|
7
|
+
/** Component id; omitted applies the op to every config. */
|
|
8
|
+
target?: string;
|
|
9
|
+
kind: AdjustKind;
|
|
10
|
+
set?: string;
|
|
11
|
+
shift?: number;
|
|
12
|
+
/** Radius shifts only: admits `--radius-full` as the ladder's top rung. */
|
|
13
|
+
full?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type SkipReason = 'raw-value' | 'off-ladder' | 'clamped' | 'pill-preserved';
|
|
17
|
+
|
|
18
|
+
export interface AliasChange {
|
|
19
|
+
variable: string;
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AliasSkip {
|
|
25
|
+
variable: string;
|
|
26
|
+
value: AliasDiskValue;
|
|
27
|
+
reason: SkipReason;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ComponentReport {
|
|
31
|
+
component: string;
|
|
32
|
+
changes: AliasChange[];
|
|
33
|
+
skips: AliasSkip[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AdjustReport {
|
|
37
|
+
components: ComponentReport[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AdjustResult {
|
|
41
|
+
configs: Record<string, ComponentConfig>;
|
|
42
|
+
report: AdjustReport;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const RADIUS_FULL = '--radius-full';
|
|
46
|
+
|
|
47
|
+
const RADIUS_RUNGS = [
|
|
48
|
+
'--radius-none', '--radius-sm', '--radius-md', '--radius-lg', '--radius-xl',
|
|
49
|
+
'--radius-2xl', '--radius-3xl', '--radius-4xl',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** The editor picker's 12-step subset (`UIPaddingSelector`), not the full
|
|
53
|
+
`--space-*` scale: every value this engine writes must stay re-editable
|
|
54
|
+
by hand in the picker. */
|
|
55
|
+
const SPACE_RUNGS = [
|
|
56
|
+
'--space-0', '--space-2', '--space-4', '--space-6', '--space-8', '--space-10',
|
|
57
|
+
'--space-12', '--space-16', '--space-20', '--space-24', '--space-32', '--space-48',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const SPACE_FAMILY = [...SPACE_RUNGS, '--space-40', '--space-64', '--space-96', '--space-128'];
|
|
61
|
+
|
|
62
|
+
const BORDER_WIDTH_RUNGS = [
|
|
63
|
+
'--border-width-0', '--border-width-1', '--border-width-2', '--border-width-3',
|
|
64
|
+
'--border-width-4', '--border-width-5', '--border-width-6', '--border-width-8',
|
|
65
|
+
'--border-width-10', '--border-width-12', '--border-width-16', '--border-width-20',
|
|
66
|
+
'--border-width-24',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const LADDERS: Record<AdjustKind, { rungs: string[]; family: string[] }> = {
|
|
70
|
+
radius: { rungs: RADIUS_RUNGS, family: [...RADIUS_RUNGS, RADIUS_FULL] },
|
|
71
|
+
padding: { rungs: SPACE_RUNGS, family: SPACE_FAMILY },
|
|
72
|
+
gap: { rungs: SPACE_RUNGS, family: SPACE_FAMILY },
|
|
73
|
+
'border-width': { rungs: BORDER_WIDTH_RUNGS, family: BORDER_WIDTH_RUNGS },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const TOKEN_NAME = /^--[a-z0-9-]+$/;
|
|
77
|
+
|
|
78
|
+
type Outcome = { from: string; to: string } | { skip: SkipReason };
|
|
79
|
+
|
|
80
|
+
function rungsFor(op: AdjustOp): string[] {
|
|
81
|
+
return op.kind === 'radius' && op.full ? [...RADIUS_RUNGS, RADIUS_FULL] : LADDERS[op.kind].rungs;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function spaceStep(token: string): number {
|
|
85
|
+
return Number(token.slice('--space-'.length));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `<=` sends an exact tie (`--space-40`, midway between 32 and 48) upward. */
|
|
89
|
+
function nearestSpaceRung(token: string, rungs: string[]): number {
|
|
90
|
+
const step = spaceStep(token);
|
|
91
|
+
let best = 0;
|
|
92
|
+
for (let i = 1; i < rungs.length; i++) {
|
|
93
|
+
if (Math.abs(spaceStep(rungs[i]) - step) <= Math.abs(spaceStep(rungs[best]) - step)) best = i;
|
|
94
|
+
}
|
|
95
|
+
return best;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolve(op: AdjustOp, value: AliasDiskValue): Outcome {
|
|
99
|
+
if (typeof value !== 'string' || !TOKEN_NAME.test(value)) return { skip: 'raw-value' };
|
|
100
|
+
if (!LADDERS[op.kind].family.includes(value)) return { skip: 'off-ladder' };
|
|
101
|
+
if (op.set !== undefined) return { from: value, to: op.set };
|
|
102
|
+
|
|
103
|
+
const rungs = rungsFor(op);
|
|
104
|
+
let index = rungs.indexOf(value);
|
|
105
|
+
if (index < 0) {
|
|
106
|
+
if (value === RADIUS_FULL) return { skip: 'pill-preserved' };
|
|
107
|
+
index = nearestSpaceRung(value, rungs);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const wanted = index + op.shift!;
|
|
111
|
+
const landed = Math.min(Math.max(wanted, 0), rungs.length - 1);
|
|
112
|
+
if (landed !== wanted && rungs[landed] === value) return { skip: 'clamped' };
|
|
113
|
+
return { from: value, to: rungs[landed] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validateOp(op: AdjustOp, configs: Record<string, ComponentConfig>): void {
|
|
117
|
+
const ladder = LADDERS[op.kind];
|
|
118
|
+
if (!ladder) throw new Error(`Unknown kind "${op.kind}" (expected ${Object.keys(LADDERS).join(', ')})`);
|
|
119
|
+
if (op.target !== undefined && !(op.target in configs)) {
|
|
120
|
+
throw new Error(`Unknown target component "${op.target}"`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hasSet = op.set !== undefined;
|
|
124
|
+
const hasShift = op.shift !== undefined;
|
|
125
|
+
if (hasSet === hasShift) throw new Error(`A ${op.kind} op needs exactly one of "set" or "shift"`);
|
|
126
|
+
const settable = op.kind === 'radius' ? ladder.family : ladder.rungs;
|
|
127
|
+
if (hasSet && !settable.includes(op.set!)) {
|
|
128
|
+
throw new Error(`"${op.set}" is not on the ${op.kind} ladder`);
|
|
129
|
+
}
|
|
130
|
+
if (hasShift && !Number.isInteger(op.shift)) {
|
|
131
|
+
throw new Error(`"shift" must be a whole number of steps, got ${op.shift}`);
|
|
132
|
+
}
|
|
133
|
+
if (op.full !== undefined && !(op.kind === 'radius' && hasShift)) {
|
|
134
|
+
throw new Error('"full" applies to radius shifts only');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function adjustAliases(
|
|
139
|
+
configs: Record<string, ComponentConfig>,
|
|
140
|
+
ops: readonly AdjustOp[],
|
|
141
|
+
now: string,
|
|
142
|
+
): AdjustResult {
|
|
143
|
+
for (const op of ops) validateOp(op, configs);
|
|
144
|
+
|
|
145
|
+
const next = { ...configs };
|
|
146
|
+
const reports = new Map<string, ComponentReport>();
|
|
147
|
+
const reportFor = (component: string): ComponentReport => {
|
|
148
|
+
let report = reports.get(component);
|
|
149
|
+
if (!report) {
|
|
150
|
+
report = { component, changes: [], skips: [] };
|
|
151
|
+
reports.set(component, report);
|
|
152
|
+
}
|
|
153
|
+
return report;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
for (const op of ops) {
|
|
157
|
+
for (const component of op.target ? [op.target] : Object.keys(next)) {
|
|
158
|
+
const config = next[component];
|
|
159
|
+
const aliases = { ...config.aliases };
|
|
160
|
+
let changed = false;
|
|
161
|
+
|
|
162
|
+
for (const [variable, value] of Object.entries(config.aliases)) {
|
|
163
|
+
if (!matchesKind(variable, op.kind)) continue;
|
|
164
|
+
const outcome = resolve(op, value);
|
|
165
|
+
if ('skip' in outcome) {
|
|
166
|
+
reportFor(component).skips.push({ variable, value, reason: outcome.skip });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (outcome.to === outcome.from) continue;
|
|
170
|
+
aliases[variable] = outcome.to;
|
|
171
|
+
reportFor(component).changes.push({ variable, from: outcome.from, to: outcome.to });
|
|
172
|
+
changed = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (changed) next[component] = { ...config, aliases, updatedAt: now };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { configs: next, report: { components: [...reports.values()] } };
|
|
180
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type TokenKind =
|
|
2
|
+
| 'surface'
|
|
3
|
+
| 'border'
|
|
4
|
+
| 'border-width'
|
|
5
|
+
| 'radius'
|
|
6
|
+
| 'divider-width'
|
|
7
|
+
| 'divider-height'
|
|
8
|
+
| 'divider-inset'
|
|
9
|
+
| 'dot-size'
|
|
10
|
+
| 'blur'
|
|
11
|
+
| 'scale'
|
|
12
|
+
| 'shadow'
|
|
13
|
+
| 'font-family'
|
|
14
|
+
| 'font-weight'
|
|
15
|
+
| 'font-size'
|
|
16
|
+
| 'line-height'
|
|
17
|
+
| 'letter-spacing'
|
|
18
|
+
| 'padding'
|
|
19
|
+
| 'gap'
|
|
20
|
+
| 'duration'
|
|
21
|
+
| 'easing'
|
|
22
|
+
| 'text-color';
|
|
23
|
+
|
|
24
|
+
/** Suffix/prefix patterns mapped to kinds — the one source of truth for the
|
|
25
|
+
editor's selector layout and the `adjust` CLI, so the two cannot drift.
|
|
26
|
+
Order matters: `-text` must run before `-border`/`-surface` because `--text-*`
|
|
27
|
+
would otherwise match `surface`/`border` if any pattern overlapped. Variables
|
|
28
|
+
that don't match any pattern fall through to `text-color` (renders as a palette
|
|
29
|
+
picker). Tokens with unconventional suffixes should be renamed. */
|
|
30
|
+
export const KIND_PATTERNS: ReadonlyArray<{ kind: TokenKind; matches: (v: string) => boolean }> = [
|
|
31
|
+
{ kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
|
|
32
|
+
{ kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
|
|
33
|
+
{ kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') || v.endsWith('-thumb-size') },
|
|
34
|
+
{ kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
|
|
35
|
+
{ kind: 'letter-spacing', matches: (v) => v.endsWith('-letter-spacing') },
|
|
36
|
+
{ kind: 'text-color', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
|
|
37
|
+
{ kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
|
|
38
|
+
{ kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
|
|
39
|
+
{ kind: 'divider-height', matches: (v) => v.endsWith('-divider-height') || v.endsWith('-track-height') },
|
|
40
|
+
{ kind: 'divider-inset', matches: (v) => v.endsWith('-divider-inset') },
|
|
41
|
+
{ kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
|
|
42
|
+
{ kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
|
|
43
|
+
{ kind: 'scale', matches: (v) => v.endsWith('-scale') || v.startsWith('--scale-') },
|
|
44
|
+
{ kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
|
|
45
|
+
{ kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
|
|
46
|
+
{ kind: 'gap', matches: (v) => v.endsWith('-gap') },
|
|
47
|
+
{ kind: 'duration', matches: (v) => v.endsWith('-duration') || v.startsWith('--duration-') },
|
|
48
|
+
{ kind: 'easing', matches: (v) => v.endsWith('-easing') || v.startsWith('--ease-') },
|
|
49
|
+
{ kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
|
|
50
|
+
{ kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
|
|
51
|
+
{ kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export function rawKind(variable: string): TokenKind {
|
|
55
|
+
for (const { kind, matches } of KIND_PATTERNS) {
|
|
56
|
+
if (matches(variable)) return kind;
|
|
57
|
+
}
|
|
58
|
+
return 'text-color';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const MATCHER_BY_KIND = Object.fromEntries(
|
|
62
|
+
KIND_PATTERNS.map((p) => [p.kind, p.matches]),
|
|
63
|
+
) as Record<TokenKind, (v: string) => boolean>;
|
|
64
|
+
|
|
65
|
+
const SIDE_SUFFIXES = ['-top', '-right', '-bottom', '-left'];
|
|
66
|
+
|
|
67
|
+
/** Side-aware kind test. Per-side paddings (`--card-default-body-padding-top`)
|
|
68
|
+
are written by UIPaddingSelector and never declared as editor tokens, so
|
|
69
|
+
`rawKind` never meets one; the CLI does, and they belong with their parent. */
|
|
70
|
+
export function matchesKind(variable: string, kind: TokenKind): boolean {
|
|
71
|
+
const side = SIDE_SUFFIXES.find((s) => variable.endsWith(s));
|
|
72
|
+
return MATCHER_BY_KIND[kind](side ? variable.slice(0, -side.length) : variable);
|
|
73
|
+
}
|
|
@@ -1,37 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ComponentConfig, ComponentConfigMeta, LiveSource } from '../themes/themeTypes';
|
|
2
2
|
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
3
|
import { API_BASE } from '../storage/apiBase';
|
|
4
|
+
import { liveMovedSinceBake } from '../productionPulse';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* REST client for per-component config files. Parallel to `
|
|
7
|
+
* REST client for per-component config files. Parallel to `colorsAndTypeService.ts`
|
|
7
8
|
* but scoped to `${API_BASE}/component-configs/*`. Each component (button,
|
|
8
|
-
* card, …) has
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Both this and `themeService` consume `versionedFileResource(...)`. Adding a
|
|
13
|
-
* third file-managed resource — per the user's "mirror theme-file lifecycle
|
|
14
|
-
* for new editor artifacts" invariant — is one helper call here, not a third
|
|
15
|
-
* round of copy-paste CRUD.
|
|
9
|
+
* card, …) has a `default.json` generated from its `.svelte` source, a reserved
|
|
10
|
+
* `_working.json` buffer holding whatever the editor is running, and any number
|
|
11
|
+
* of user-saved presets.
|
|
16
12
|
*/
|
|
17
13
|
|
|
18
14
|
export interface ComponentSummary {
|
|
19
15
|
name: string;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ComponentProductionInfo {
|
|
25
|
-
fileName: string;
|
|
26
|
-
name: string;
|
|
27
|
-
aliases: Record<string, AliasDiskValue>;
|
|
16
|
+
/** Where the component's live config resolves from. */
|
|
17
|
+
source: LiveSource;
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
export interface ComponentConfigList {
|
|
31
21
|
component: string;
|
|
32
22
|
files: ComponentConfigMeta[];
|
|
33
|
-
activeFile: string;
|
|
34
|
-
productionFile: string;
|
|
35
23
|
}
|
|
36
24
|
|
|
37
25
|
export async function listComponents(): Promise<ComponentSummary[]> {
|
|
@@ -42,19 +30,14 @@ export async function listComponents(): Promise<ComponentSummary[]> {
|
|
|
42
30
|
}
|
|
43
31
|
|
|
44
32
|
function resourceFor(component: string) {
|
|
45
|
-
return versionedFileResource<ComponentConfig, ComponentConfigMeta
|
|
33
|
+
return versionedFileResource<ComponentConfig, ComponentConfigMeta>({
|
|
46
34
|
baseUrl: `${API_BASE}/component-configs/${encodeURIComponent(component)}`,
|
|
47
35
|
});
|
|
48
36
|
}
|
|
49
37
|
|
|
50
38
|
export async function listComponentConfigs(component: string): Promise<ComponentConfigList> {
|
|
51
39
|
const data = await resourceFor(component).list();
|
|
52
|
-
return {
|
|
53
|
-
component,
|
|
54
|
-
files: data.files,
|
|
55
|
-
activeFile: data.activeFile ?? 'default',
|
|
56
|
-
productionFile: data.productionFile ?? 'default',
|
|
57
|
-
};
|
|
40
|
+
return { component, files: data.files };
|
|
58
41
|
}
|
|
59
42
|
|
|
60
43
|
export const loadComponentConfig = (
|
|
@@ -71,19 +54,24 @@ export const saveComponentConfig = (
|
|
|
71
54
|
export const deleteComponentConfig = (component: string, fileName: string): Promise<void> =>
|
|
72
55
|
resourceFor(component).remove(fileName);
|
|
73
56
|
|
|
57
|
+
/** The config the component is running: the buffer, else the open theme's
|
|
58
|
+
* copy, else the component default. `_source` says which. */
|
|
74
59
|
export const getActiveComponentConfig = (component: string): Promise<ComponentConfig | null> =>
|
|
75
60
|
resourceFor(component).getActive();
|
|
76
61
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
export const getComponentProductionInfo = (component: string): Promise<ComponentProductionInfo> =>
|
|
81
|
-
resourceFor(component).getProductionInfo();
|
|
82
|
-
|
|
83
|
-
export async function setComponentProductionFile(
|
|
62
|
+
/** Write the component's unsaved buffer. */
|
|
63
|
+
export async function writeWorkingComponentConfig(
|
|
84
64
|
component: string,
|
|
85
|
-
|
|
86
|
-
): Promise<
|
|
87
|
-
const
|
|
88
|
-
|
|
65
|
+
data: ComponentConfig,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const res = await fetch(`${API_BASE}/component-configs/${encodeURIComponent(component)}/working`, {
|
|
68
|
+
method: 'PUT',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(data),
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const err = await res.json().catch(() => ({ error: 'Write failed' }));
|
|
74
|
+
throw new Error(err.error || 'Write failed');
|
|
75
|
+
}
|
|
76
|
+
liveMovedSinceBake.set(true);
|
|
89
77
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralised "flash a transient status, then revert to idle" helper for the
|
|
3
|
-
* file managers (
|
|
3
|
+
* file managers (colors and type / component / theme). Each pulse — saved, error,
|
|
4
4
|
* adopted — shares the same timing so the UI stays in sync.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FontFamily, FontSource, FontStack,
|
|
1
|
+
import type { FontFamily, FontSource, FontStack, ColorsAndType } from '../themes/themeTypes';
|
|
2
2
|
|
|
3
3
|
// Google Fonts CDN URLs for the default font families. We used to ship local
|
|
4
4
|
// woff2 files and emit @font-face blocks pointing at them, but Vite's url()
|
|
@@ -30,7 +30,7 @@ function fam(sourceId: string, name: string, cssName?: string, weights?: number[
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Build the default fontSources. Used when a
|
|
33
|
+
* Build the default fontSources. Used when a file has no fontSources yet.
|
|
34
34
|
*/
|
|
35
35
|
export function defaultFontSources(): FontSource[] {
|
|
36
36
|
const typekitId = 'src_typekit_jes8oow';
|
|
@@ -143,29 +143,29 @@ function migrateLegacyLocalFonts(src: FontSource): boolean {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
|
-
* Ensure the loaded
|
|
147
|
-
* only when missing; safe to call on already-migrated
|
|
146
|
+
* Ensure the loaded colors and type have fontSources and fontStacks. Mutates in place
|
|
147
|
+
* only when missing; safe to call on already-migrated files. Also strips any
|
|
148
148
|
* stale --font-* entries from cssVariables since those are now derived, and
|
|
149
149
|
* migrates legacy local-font sources to Google Fonts URL sources.
|
|
150
150
|
*/
|
|
151
|
-
export function
|
|
151
|
+
export function migrateColorsAndTypeFonts(colorsAndType: ColorsAndType): { migrated: boolean } {
|
|
152
152
|
let migrated = false;
|
|
153
|
-
if (!
|
|
154
|
-
|
|
153
|
+
if (!colorsAndType.fontSources || colorsAndType.fontSources.length === 0) {
|
|
154
|
+
colorsAndType.fontSources = defaultFontSources();
|
|
155
155
|
migrated = true;
|
|
156
156
|
} else {
|
|
157
|
-
for (const src of
|
|
157
|
+
for (const src of colorsAndType.fontSources) {
|
|
158
158
|
if (migrateLegacyLocalFonts(src)) migrated = true;
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
if (!
|
|
162
|
-
|
|
161
|
+
if (!colorsAndType.fontStacks || colorsAndType.fontStacks.length === 0) {
|
|
162
|
+
colorsAndType.fontStacks = defaultFontStacks(colorsAndType.fontSources);
|
|
163
163
|
migrated = true;
|
|
164
164
|
}
|
|
165
|
-
if (
|
|
165
|
+
if (colorsAndType.cssVariables) {
|
|
166
166
|
for (const key of ['--font-display', '--font-sans', '--font-serif', '--font-mono']) {
|
|
167
|
-
if (key in
|
|
168
|
-
delete
|
|
167
|
+
if (key in colorsAndType.cssVariables) {
|
|
168
|
+
delete colorsAndType.cssVariables[key];
|
|
169
169
|
migrated = true;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FontFamily, FontSource, FontStack, FontStackVariable } from '../themes/themeTypes';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The two faces a theme is recognised by, for the read-only identity line:
|
|
5
|
+
* headings then body, named as the user named them rather than as a CSS
|
|
6
|
+
* cascade. The first slot that resolves is the face the page actually shows.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const PAIRING: FontStackVariable[] = ['--font-display', '--font-sans'];
|
|
10
|
+
|
|
11
|
+
function slotLabel(
|
|
12
|
+
stack: FontStack | undefined,
|
|
13
|
+
familyById: Map<string, FontFamily>,
|
|
14
|
+
): string | null {
|
|
15
|
+
for (const slot of stack?.slots ?? []) {
|
|
16
|
+
if (slot.kind === 'project') {
|
|
17
|
+
const family = familyById.get(slot.familyId);
|
|
18
|
+
if (family) return family.name;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (slot.kind === 'system') return 'System';
|
|
22
|
+
return slot.value;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function fontPairingLabel(stacks: FontStack[], sources: FontSource[]): string {
|
|
28
|
+
const familyById = new Map<string, FontFamily>();
|
|
29
|
+
for (const source of sources) {
|
|
30
|
+
for (const family of source.families) familyById.set(family.id, family);
|
|
31
|
+
}
|
|
32
|
+
return PAIRING.map((variable) => slotLabel(stacks.find((s) => s.variable === variable), familyById))
|
|
33
|
+
.filter((label): label is string => label !== null)
|
|
34
|
+
.join(' / ');
|
|
35
|
+
}
|