@motion-proto/live-tokens 0.52.1 → 0.53.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 +7 -1
- package/CHANGELOG.md +63 -0
- package/bin/adjust.mjs +16 -39
- package/bin/generate-theme.mjs +20 -6
- package/dist-plugin/adjust/index.cjs +781 -21
- package/dist-plugin/adjust/index.d.cts +2 -1
- package/dist-plugin/adjust/index.d.ts +2 -1
- package/dist-plugin/adjust/index.js +43 -21
- package/dist-plugin/{chunk-6OZFXIQI.js → chunk-232GZGQU.js} +207 -12
- package/dist-plugin/chunk-NE6N66EE.js +753 -0
- package/dist-plugin/{chunk-JIGIIE5P.js → chunk-PB2JTK2H.js} +123 -0
- package/dist-plugin/generateColorsAndType/index.cjs +48 -32
- package/dist-plugin/generateColorsAndType/index.d.cts +2 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +2 -1
- package/dist-plugin/generateColorsAndType/index.js +7 -773
- package/dist-plugin/{themeTypes-Dky03HL9.d.cts → index-DpTIRZ2H.d.cts} +49 -21
- package/dist-plugin/{themeTypes-Dky03HL9.d.ts → index-DpTIRZ2H.d.ts} +49 -21
- package/dist-plugin/index.cjs +1159 -85
- package/dist-plugin/index.js +102 -80
- package/dist-plugin/migrateData/index.cjs +956 -14
- package/dist-plugin/migrateData/index.js +10 -3
- package/package.json +6 -4
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +9 -23
- package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +8 -8
- package/src/editor/core/components/adjustAliases.ts +49 -22
- package/src/editor/core/components/aliasKinds.ts +7 -2
- package/src/editor/core/components/componentConfigService.ts +1 -1
- package/src/editor/core/store/editorStore.ts +7 -197
- package/src/editor/core/store/editorTypes.ts +9 -30
- package/src/editor/core/themes/lookSummary.ts +3 -25
- package/src/editor/core/themes/migrateComponentConfig.ts +205 -0
- package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +1 -1
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +1 -1
- package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +1 -1
- package/src/editor/core/themes/parsers/gradient.ts +211 -0
- package/src/editor/core/themes/slices/gradients.ts +5 -58
- package/src/editor/core/themes/themeService.ts +22 -9
- package/src/editor/core/themes/themeTypes.ts +71 -21
- package/src/editor/docs/content/themes-workflow.md +9 -11
- package/src/editor/docs/content/where-themes-live.md +2 -2
- package/src/editor/docs/content.generated.ts +2 -2
- package/src/editor/index.ts +1 -0
- package/src/editor/styles/ui-editor.css +5 -2
- package/src/editor/ui/PaletteEditor.svelte +30 -15
- package/src/editor/ui/ThemePanel.svelte +24 -15
- package/src/editor/ui/Toggle.svelte +22 -22
- package/src/editor/ui/UIPillButton.svelte +1 -1
- package/src/editor/ui/UIReveal.svelte +47 -0
- package/src/editor/ui/UISquareButton.svelte +1 -1
- package/src/editor/ui/colors/ColorsTab.svelte +4 -5
- package/src/editor/ui/palette/OverridesPanel.svelte +4 -3
- package/src/editor/ui/palette/PaletteJumpButton.svelte +3 -3
- package/src/editor/ui/palette/ScaleCurveEditor.svelte +3 -2
- package/src/live-tokens/data/themes/autumn.json +77 -41
- package/src/live-tokens/data/themes/halloween.json +53 -52
- package/src/live-tokens/data/themes/midnight-study.json +136 -68
- package/src/live-tokens/data/themes/ocean.json +77 -41
- package/src/live-tokens/data/themes/royal-velvet.json +43 -42
- package/src/live-tokens/data/themes/spring-meadow.json +86 -52
- package/src/live-tokens/data/themes/sunset.json +101 -40
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
normalizeTheme,
|
|
8
8
|
planLegacyRenames,
|
|
9
9
|
versionedFileResourceServer
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-232GZGQU.js";
|
|
11
|
+
import {
|
|
12
|
+
CURRENT_COMPONENT_SCHEMA_VERSION
|
|
13
|
+
} from "../chunk-NE6N66EE.js";
|
|
11
14
|
import {
|
|
12
15
|
readLiveTokensConfig,
|
|
13
16
|
resolveDataDirs
|
|
@@ -151,6 +154,7 @@ function migrateData(opts) {
|
|
|
151
154
|
const resolvers = {
|
|
152
155
|
readColorsAndType: (name) => colorsRes.readJson(name),
|
|
153
156
|
readComponentConfig: (comp, name) => compRes(comp).readJson(name),
|
|
157
|
+
listComponentNames: () => comps,
|
|
154
158
|
normalizeColorsAndType: (colorsAndType) => colorsAndType
|
|
155
159
|
};
|
|
156
160
|
const upgradedThemes = [];
|
|
@@ -230,7 +234,6 @@ function migrateData(opts) {
|
|
|
230
234
|
for (const comp of comps) {
|
|
231
235
|
const pointed = prodConfigs.get(comp);
|
|
232
236
|
if (!isObject(pointed)) continue;
|
|
233
|
-
if (sameContent(pointed, defaultConfig(comp))) continue;
|
|
234
237
|
componentConfigs[comp] = stripMarkers(pointed);
|
|
235
238
|
}
|
|
236
239
|
recoveredTheme = {
|
|
@@ -239,7 +242,11 @@ function migrateData(opts) {
|
|
|
239
242
|
updatedAt: now,
|
|
240
243
|
schemaVersion: THEME_SCHEMA_VERSION,
|
|
241
244
|
colorsAndType: isObject(prodColors) ? stripMarkers(prodColors) : null,
|
|
242
|
-
componentConfigs
|
|
245
|
+
componentConfigs,
|
|
246
|
+
// The configs above are what boot just wrote (`prodConfigs`, read off
|
|
247
|
+
// the pointed files), at the current component schema — not 0, which
|
|
248
|
+
// would misdescribe them as unmigrated.
|
|
249
|
+
componentSchemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION
|
|
243
250
|
};
|
|
244
251
|
const slug = nextAvailableName((n) => themesRes.existingPath(n) !== null, RECOVERED_SLUG);
|
|
245
252
|
recoveredThemePath = themesRes.filePath(slug);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motion-proto/live-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
|
|
6
6
|
"keywords": [
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"test": "vitest run",
|
|
112
112
|
"test:watch": "vitest",
|
|
113
113
|
"test:e2e": "npm run test:e2e:contract && npm run test:e2e:stateful",
|
|
114
|
-
"test:e2e:contract": "playwright test tests/e2e/component-render-contract.spec.ts tests/e2e/component-alias-contract.spec.ts --workers
|
|
114
|
+
"test:e2e:contract": "playwright test tests/e2e/component-render-contract.spec.ts tests/e2e/component-alias-contract.spec.ts --workers=${PLAYWRIGHT_WORKERS:-4}",
|
|
115
115
|
"test:e2e:stateful": "playwright test tests/e2e/live-editing.spec.ts tests/e2e/theme-workflow.spec.ts --workers=1",
|
|
116
116
|
"test:e2e:components": "playwright test tests/e2e/component-render-contract.spec.ts",
|
|
117
117
|
"test:e2e:ui": "playwright test --ui",
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
"build:lib": "npm run build:plugin",
|
|
121
121
|
"deploy:local": "bash scripts/deploy-local.sh",
|
|
122
122
|
"check:no-style-imports": "node scripts/check-no-style-imports.mjs",
|
|
123
|
+
"check:no-tooling-imports": "node scripts/check-no-tooling-imports.mjs",
|
|
123
124
|
"check:slot-prose": "node scripts/check-slot-prose.mjs",
|
|
124
125
|
"check:overlay-portal": "node scripts/check-overlay-portal.mjs",
|
|
125
126
|
"check:editor-font-isolation": "node scripts/check-editor-font-isolation.mjs",
|
|
@@ -127,13 +128,14 @@
|
|
|
127
128
|
"check:production-is-default": "node scripts/check-production-is-default.mjs",
|
|
128
129
|
"check:docs-content": "node scripts/sync-docs.mjs --check",
|
|
129
130
|
"check:token-contract": "node scripts/check-token-contract.mjs",
|
|
131
|
+
"check:preset-themes": "node scripts/check-preset-themes.mjs",
|
|
130
132
|
"sync:component-defaults": "node scripts/sync-component-defaults.mjs --write",
|
|
131
133
|
"sync:docs": "node scripts/sync-docs.mjs --write",
|
|
132
|
-
"
|
|
134
|
+
"seed:preset-theme": "node scripts/seed-preset-theme.mjs",
|
|
133
135
|
"collapse:theme": "node scripts/collapse-theme-to-default.mjs",
|
|
134
136
|
"check:smoke-install": "bash scripts/smoke-install.sh",
|
|
135
137
|
"check:smoke-create": "bash scripts/smoke-create.sh",
|
|
136
|
-
"prepublishOnly": "npm run check:no-style-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:token-contract && npm run check:smoke-install && npm run check:smoke-create"
|
|
138
|
+
"prepublishOnly": "npm run check:no-style-imports && npm run check:no-tooling-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:token-contract && npm run check:preset-themes && npm run check:smoke-install && npm run check:smoke-create"
|
|
137
139
|
},
|
|
138
140
|
"peerDependencies": {
|
|
139
141
|
"@sveltejs/vite-plugin-svelte": "^7.0",
|
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
<script lang="ts">
|
|
6
6
|
import { onMount, onDestroy } from 'svelte';
|
|
7
7
|
import UIInfoPopover from '../../ui/UIInfoPopover.svelte';
|
|
8
|
-
import type {
|
|
9
|
-
ComponentConfigMeta,
|
|
10
|
-
LiveSource,
|
|
11
|
-
} from '../../core/themes/themeTypes';
|
|
8
|
+
import type { ComponentConfigMeta } from '../../core/themes/themeTypes';
|
|
12
9
|
import { componentSourceFile } from './componentSources';
|
|
13
10
|
import {
|
|
14
11
|
getActiveComponentConfig,
|
|
@@ -73,7 +70,6 @@
|
|
|
73
70
|
// Saved presets. Nothing live points at one: the component runs its working
|
|
74
71
|
// buffer, and the theme it belongs to carries the copy that ships.
|
|
75
72
|
let files: ComponentConfigMeta[] = $state([]);
|
|
76
|
-
let liveSource: LiveSource = $state('default');
|
|
77
73
|
let openTheme: { fileName: string; name: string } = $state({
|
|
78
74
|
fileName: 'default',
|
|
79
75
|
name: 'Motion Proto',
|
|
@@ -94,18 +90,13 @@
|
|
|
94
90
|
let compDirty = $derived($componentDirty[component] ?? false);
|
|
95
91
|
let openIsProtected = $derived(openTheme.fileName === 'default');
|
|
96
92
|
// Both rows name a document, because that is what a config belongs to now:
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
let editorName = $derived(liveSource === 'default' ? 'Default' : openTheme.name);
|
|
106
|
-
let productionName = $derived(
|
|
107
|
-
productionDoc === null ? '—' : productionDoc === 'default' ? 'Default' : $productionTheme?.name ?? '—',
|
|
108
|
-
);
|
|
93
|
+
// every theme carries every component by value, so the component always
|
|
94
|
+
// runs the open theme's config, and production always runs the published
|
|
95
|
+
// theme's.
|
|
96
|
+
let editorDoc = $derived(openTheme.fileName);
|
|
97
|
+
let productionDoc = $derived($productionTheme?._fileName ?? null);
|
|
98
|
+
let editorName = $derived(openTheme.name);
|
|
99
|
+
let productionName = $derived(productionDoc === null ? '—' : $productionTheme?.name ?? '—');
|
|
109
100
|
// Saving writes the buffer, which the bake has not seen: the document is
|
|
110
101
|
// still the published one, so only the shared signal can say the config on
|
|
111
102
|
// screen is not what production runs.
|
|
@@ -123,11 +114,8 @@
|
|
|
123
114
|
if (data) files = data.files;
|
|
124
115
|
}
|
|
125
116
|
|
|
126
|
-
/**
|
|
127
|
-
* belongs to. Both ride on the live read door's markers. */
|
|
117
|
+
/** The theme the component belongs to, by the open document's own markers. */
|
|
128
118
|
async function refreshLive() {
|
|
129
|
-
const cfg = await getActiveComponentConfig(component);
|
|
130
|
-
if (cfg) liveSource = cfg._source ?? 'default';
|
|
131
119
|
try {
|
|
132
120
|
const theme = await getActiveTheme();
|
|
133
121
|
if (theme) openTheme = { fileName: theme._fileName ?? 'default', name: theme.name };
|
|
@@ -186,7 +174,6 @@
|
|
|
186
174
|
component,
|
|
187
175
|
componentConfigFromState($editorState, component, openTheme.name),
|
|
188
176
|
);
|
|
189
|
-
liveSource = 'working';
|
|
190
177
|
markComponentSaved(component);
|
|
191
178
|
bumpComponentActiveRevision();
|
|
192
179
|
}
|
|
@@ -233,7 +220,6 @@
|
|
|
233
220
|
const { _fileName, _source, ...cfg } = await loadComponentConfig(component, file.fileName);
|
|
234
221
|
await writeWorkingComponentConfig(component, { ...cfg, component });
|
|
235
222
|
loadComponentActive(component, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
|
|
236
|
-
liveSource = 'working';
|
|
237
223
|
bumpComponentActiveRevision();
|
|
238
224
|
} catch {
|
|
239
225
|
// intentional: see comment above
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { fade
|
|
2
|
+
import { fade } from 'svelte/transition';
|
|
3
3
|
import TokenLayout from './TokenLayout.svelte';
|
|
4
|
+
import UIReveal from '../../ui/UIReveal.svelte';
|
|
4
5
|
import LinkageChart from './LinkageChart.svelte';
|
|
5
6
|
import { getEditorContext } from './editorContext';
|
|
6
7
|
import type { LinkedBlockResult, LinkedGroup } from './linkedBlock';
|
|
@@ -11,7 +12,6 @@
|
|
|
11
12
|
const reduceMotion = typeof window !== 'undefined'
|
|
12
13
|
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
|
|
13
14
|
const fadeDur = reduceMotion ? 0 : 140;
|
|
14
|
-
const slideDur = reduceMotion ? 0 : 200;
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
17
17
|
component: string;
|
|
@@ -139,8 +139,8 @@
|
|
|
139
139
|
{/if}
|
|
140
140
|
</span>
|
|
141
141
|
</button>
|
|
142
|
-
{
|
|
143
|
-
<div class="linked-grid"
|
|
142
|
+
<UIReveal open={sectionExpanded}>
|
|
143
|
+
<div class="linked-grid">
|
|
144
144
|
{#each cards as card (card.row.variable)}
|
|
145
145
|
{@const cardExpanded = expandedCards[card.row.variable] === true}
|
|
146
146
|
{@const cardHovered = hoveredVar === card.row.variable}
|
|
@@ -176,8 +176,8 @@
|
|
|
176
176
|
>({card.unlinkedText})</span>
|
|
177
177
|
{/if}
|
|
178
178
|
</button>
|
|
179
|
-
{
|
|
180
|
-
<div class="chart-wrap"
|
|
179
|
+
<UIReveal open={cardExpanded}>
|
|
180
|
+
<div class="chart-wrap">
|
|
181
181
|
<LinkageChart
|
|
182
182
|
contexts={card.contexts}
|
|
183
183
|
broken={card.brokenContexts}
|
|
@@ -187,11 +187,11 @@
|
|
|
187
187
|
onselect={handleChartSelect}
|
|
188
188
|
/>
|
|
189
189
|
</div>
|
|
190
|
-
|
|
190
|
+
</UIReveal>
|
|
191
191
|
</article>
|
|
192
192
|
{/each}
|
|
193
193
|
</div>
|
|
194
|
-
|
|
194
|
+
</UIReveal>
|
|
195
195
|
</section>
|
|
196
196
|
{/if}
|
|
197
197
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { matchesKind } from './aliasKinds';
|
|
1
|
+
import { matchesKind, stripSide } from './aliasKinds';
|
|
2
2
|
import type { AliasDiskValue, ComponentConfig } from '../themes/themeTypes';
|
|
3
3
|
|
|
4
4
|
export type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
|
|
@@ -52,12 +52,21 @@ const RADIUS_RUNGS = [
|
|
|
52
52
|
/** The editor picker's 12-step subset (`UIPaddingSelector`), not the full
|
|
53
53
|
`--space-*` scale: every value this engine writes must stay re-editable
|
|
54
54
|
by hand in the picker. */
|
|
55
|
-
const
|
|
55
|
+
const SPACE_SETTABLE = [
|
|
56
56
|
'--space-0', '--space-2', '--space-4', '--space-6', '--space-8', '--space-10',
|
|
57
57
|
'--space-12', '--space-16', '--space-20', '--space-24', '--space-32', '--space-48',
|
|
58
58
|
];
|
|
59
59
|
|
|
60
|
-
const
|
|
60
|
+
const SPACE_RUNGS = SPACE_SETTABLE;
|
|
61
|
+
|
|
62
|
+
/** Content insets stop at `--space-4`. Below it the text sits against its own
|
|
63
|
+
edge, which is a deliberate choice rather than somewhere a relative
|
|
64
|
+
"tighter" should deposit you. Outer space is exempt: a 2px gap between an
|
|
65
|
+
icon and its label, or a 2px margin under a bar, is ordinary design. Both
|
|
66
|
+
rungs stay pickable by hand and settable by name. */
|
|
67
|
+
const INSET_RUNGS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf('--space-4'));
|
|
68
|
+
|
|
69
|
+
const SPACE_FAMILY = [...SPACE_SETTABLE, '--space-40', '--space-64', '--space-96', '--space-128'];
|
|
61
70
|
|
|
62
71
|
const BORDER_WIDTH_RUNGS = [
|
|
63
72
|
'--border-width-0', '--border-width-1', '--border-width-2', '--border-width-3',
|
|
@@ -66,48 +75,66 @@ const BORDER_WIDTH_RUNGS = [
|
|
|
66
75
|
'--border-width-24',
|
|
67
76
|
];
|
|
68
77
|
|
|
69
|
-
const LADDERS: Record<AdjustKind, { rungs: string[]; family: string[] }> = {
|
|
70
|
-
radius: {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
const LADDERS: Record<AdjustKind, { rungs: string[]; settable: string[]; family: string[] }> = {
|
|
79
|
+
radius: {
|
|
80
|
+
rungs: RADIUS_RUNGS,
|
|
81
|
+
settable: [...RADIUS_RUNGS, RADIUS_FULL],
|
|
82
|
+
family: [...RADIUS_RUNGS, RADIUS_FULL],
|
|
83
|
+
},
|
|
84
|
+
padding: { rungs: SPACE_RUNGS, settable: SPACE_SETTABLE, family: SPACE_FAMILY },
|
|
85
|
+
gap: { rungs: SPACE_RUNGS, settable: SPACE_SETTABLE, family: SPACE_FAMILY },
|
|
86
|
+
'border-width': {
|
|
87
|
+
rungs: BORDER_WIDTH_RUNGS,
|
|
88
|
+
settable: BORDER_WIDTH_RUNGS,
|
|
89
|
+
family: BORDER_WIDTH_RUNGS,
|
|
90
|
+
},
|
|
74
91
|
};
|
|
75
92
|
|
|
76
93
|
const TOKEN_NAME = /^--[a-z0-9-]+$/;
|
|
77
94
|
|
|
78
95
|
type Outcome = { from: string; to: string } | { skip: SkipReason };
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
/** `-margin` shares the padding kind, so the inset floor keys off the variable
|
|
98
|
+
rather than the kind. */
|
|
99
|
+
function rungsFor(op: AdjustOp, variable: string): string[] {
|
|
100
|
+
if (op.kind === 'radius') return op.full ? [...RADIUS_RUNGS, RADIUS_FULL] : RADIUS_RUNGS;
|
|
101
|
+
if (op.kind === 'padding' && stripSide(variable).endsWith('-padding')) return INSET_RUNGS;
|
|
102
|
+
return LADDERS[op.kind].rungs;
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
function spaceStep(token: string): number {
|
|
85
106
|
return Number(token.slice('--space-'.length));
|
|
86
107
|
}
|
|
87
108
|
|
|
88
|
-
/**
|
|
89
|
-
|
|
109
|
+
/** An off-rung value has no step to count from, so landing on the first rung
|
|
110
|
+
the shift points at is itself that shift's opening step. Snapping to the
|
|
111
|
+
nearest rung instead would spend two visible steps on a one-step request.
|
|
112
|
+
Returns -1 when the ladder holds nothing in that direction. */
|
|
113
|
+
function snapRung(token: string, rungs: string[], direction: number): number {
|
|
90
114
|
const step = spaceStep(token);
|
|
91
|
-
|
|
92
|
-
for (let
|
|
93
|
-
if (
|
|
115
|
+
if (direction > 0) return rungs.findIndex((rung) => spaceStep(rung) > step);
|
|
116
|
+
for (let index = rungs.length - 1; index >= 0; index--) {
|
|
117
|
+
if (spaceStep(rungs[index]) < step) return index;
|
|
94
118
|
}
|
|
95
|
-
return
|
|
119
|
+
return -1;
|
|
96
120
|
}
|
|
97
121
|
|
|
98
|
-
function resolve(op: AdjustOp, value: AliasDiskValue): Outcome {
|
|
122
|
+
function resolve(op: AdjustOp, variable: string, value: AliasDiskValue): Outcome {
|
|
99
123
|
if (typeof value !== 'string' || !TOKEN_NAME.test(value)) return { skip: 'raw-value' };
|
|
100
124
|
if (!LADDERS[op.kind].family.includes(value)) return { skip: 'off-ladder' };
|
|
101
125
|
if (op.set !== undefined) return { from: value, to: op.set };
|
|
102
126
|
|
|
103
|
-
const rungs = rungsFor(op);
|
|
127
|
+
const rungs = rungsFor(op, variable);
|
|
104
128
|
let index = rungs.indexOf(value);
|
|
129
|
+
let shift = op.shift!;
|
|
105
130
|
if (index < 0) {
|
|
106
131
|
if (value === RADIUS_FULL) return { skip: 'pill-preserved' };
|
|
107
|
-
index =
|
|
132
|
+
index = snapRung(value, rungs, shift);
|
|
133
|
+
if (index < 0) return { skip: 'clamped' };
|
|
134
|
+
shift -= Math.sign(shift);
|
|
108
135
|
}
|
|
109
136
|
|
|
110
|
-
const wanted = index +
|
|
137
|
+
const wanted = index + shift;
|
|
111
138
|
const landed = Math.min(Math.max(wanted, 0), rungs.length - 1);
|
|
112
139
|
if (landed !== wanted && rungs[landed] === value) return { skip: 'clamped' };
|
|
113
140
|
return { from: value, to: rungs[landed] };
|
|
@@ -123,7 +150,7 @@ function validateOp(op: AdjustOp, configs: Record<string, ComponentConfig>): voi
|
|
|
123
150
|
const hasSet = op.set !== undefined;
|
|
124
151
|
const hasShift = op.shift !== undefined;
|
|
125
152
|
if (hasSet === hasShift) throw new Error(`A ${op.kind} op needs exactly one of "set" or "shift"`);
|
|
126
|
-
const settable =
|
|
153
|
+
const settable = ladder.settable;
|
|
127
154
|
if (hasSet && !settable.includes(op.set!)) {
|
|
128
155
|
throw new Error(`"${op.set}" is not on the ${op.kind} ladder`);
|
|
129
156
|
}
|
|
@@ -161,7 +188,7 @@ export function adjustAliases(
|
|
|
161
188
|
|
|
162
189
|
for (const [variable, value] of Object.entries(config.aliases)) {
|
|
163
190
|
if (!matchesKind(variable, op.kind)) continue;
|
|
164
|
-
const outcome = resolve(op, value);
|
|
191
|
+
const outcome = resolve(op, variable, value);
|
|
165
192
|
if ('skip' in outcome) {
|
|
166
193
|
reportFor(component).skips.push({ variable, value, reason: outcome.skip });
|
|
167
194
|
continue;
|
|
@@ -64,10 +64,15 @@ const MATCHER_BY_KIND = Object.fromEntries(
|
|
|
64
64
|
|
|
65
65
|
const SIDE_SUFFIXES = ['-top', '-right', '-bottom', '-left'];
|
|
66
66
|
|
|
67
|
+
/** Drops a per-side suffix so callers can test the parent name. */
|
|
68
|
+
export function stripSide(variable: string): string {
|
|
69
|
+
const side = SIDE_SUFFIXES.find((s) => variable.endsWith(s));
|
|
70
|
+
return side ? variable.slice(0, -side.length) : variable;
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
/** Side-aware kind test. Per-side paddings (`--card-default-body-padding-top`)
|
|
68
74
|
are written by UIPaddingSelector and never declared as editor tokens, so
|
|
69
75
|
`rawKind` never meets one; the CLI does, and they belong with their parent. */
|
|
70
76
|
export function matchesKind(variable: string, kind: TokenKind): boolean {
|
|
71
|
-
|
|
72
|
-
return MATCHER_BY_KIND[kind](side ? variable.slice(0, -side.length) : variable);
|
|
77
|
+
return MATCHER_BY_KIND[kind](stripSide(variable));
|
|
73
78
|
}
|
|
@@ -58,7 +58,7 @@ export const deleteComponentConfig = (component: string, fileName: string): Prom
|
|
|
58
58
|
resourceFor(component).remove(fileName);
|
|
59
59
|
|
|
60
60
|
/** The config the component is running: the buffer, else the open theme's
|
|
61
|
-
* copy
|
|
61
|
+
* copy (every theme carries every component by value). `_source` says which. */
|
|
62
62
|
export const getActiveComponentConfig = (component: string): Promise<ComponentConfig | null> =>
|
|
63
63
|
resourceFor(component).getActive();
|
|
64
64
|
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
29
29
|
runMigrations,
|
|
30
30
|
} from '../themes/migrations';
|
|
31
|
+
import { migrateComponentConfig } from '../themes/migrateComponentConfig';
|
|
31
32
|
import { renamePrimaryPaletteKey } from '../themes/migrations/2026-05-13-primary-to-brand';
|
|
32
33
|
import {
|
|
33
34
|
renameBackgroundPaletteKey,
|
|
@@ -220,202 +221,11 @@ export {
|
|
|
220
221
|
// `fromVersion >= file.schemaVersion`, then `splitAliasesAndConfig` routes
|
|
221
222
|
// literal-valued knobs (per `KNOWN_COMPONENT_CONFIG_KEYS`) into the config
|
|
222
223
|
// bucket and wraps the remainder as `CssVarRef` discriminated unions.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* key/value string rewrites). Object-valued entries — currently just
|
|
229
|
-
* inline `{ kind: 'gradient', value: ... }` payloads — pass through
|
|
230
|
-
* untouched: we split the bag into string/object subsets, run the runner
|
|
231
|
-
* on the string subset, then merge.
|
|
232
|
-
*
|
|
233
|
-
* For sectiondivider's legacy 7-flat-token gradients we run a synthesis
|
|
234
|
-
* step BEFORE the migration runner that would strip those tokens. The
|
|
235
|
-
* synthesized gradient object lands in the object subset and survives
|
|
236
|
-
* the stripping pass.
|
|
237
|
-
*/
|
|
238
|
-
function migrateComponentAliases(
|
|
239
|
-
component: string,
|
|
240
|
-
aliasesIn: Record<string, AliasDiskValue>,
|
|
241
|
-
rawConfig: Record<string, unknown> | undefined,
|
|
242
|
-
fileVersion: number,
|
|
243
|
-
): { aliases: Record<string, AliasDiskValue>; config: Record<string, unknown> } {
|
|
244
|
-
const stringPart: Record<string, string> = {};
|
|
245
|
-
const objectPart: Record<string, AliasDiskValue> = {};
|
|
246
|
-
for (const [k, v] of Object.entries(aliasesIn)) {
|
|
247
|
-
if (typeof v === 'string') stringPart[k] = v;
|
|
248
|
-
else objectPart[k] = v;
|
|
249
|
-
}
|
|
250
|
-
// String-valued config entries join the migration input so a migration can
|
|
251
|
-
// rename or relocate them. After migration the bag flows through
|
|
252
|
-
// `splitAliasesAndConfig` which routes KNOWN keys back to config and the
|
|
253
|
-
// rest to aliases — this is what lets a migration *move* a key from the
|
|
254
|
-
// config bucket to the aliases bucket by also removing it from KNOWN.
|
|
255
|
-
// Config wins over an aliases-bucket collision on the same key: the legacy
|
|
256
|
-
// single-bucket → split-bucket move preserved "explicit config field beats
|
|
257
|
-
// legacy alias-bucketed value", and that contract still holds.
|
|
258
|
-
// Non-string config values (rare; reserved for future structured payloads)
|
|
259
|
-
// pass through untouched.
|
|
260
|
-
const configOut: Record<string, unknown> = {};
|
|
261
|
-
for (const [k, v] of Object.entries(rawConfig ?? {})) {
|
|
262
|
-
if (typeof v === 'string') stringPart[k] = v;
|
|
263
|
-
else configOut[k] = v;
|
|
264
|
-
}
|
|
265
|
-
if (component === 'sectiondivider') {
|
|
266
|
-
synthesizeSectionDividerGradients(stringPart, objectPart);
|
|
267
|
-
}
|
|
268
|
-
const migratedString = runMigrations('component-config', fileVersion, stringPart, { component });
|
|
269
|
-
const migratedObject = component === 'sectiondivider'
|
|
270
|
-
? renameSectionDividerObjectSlots(objectPart)
|
|
271
|
-
: objectPart;
|
|
272
|
-
return {
|
|
273
|
-
aliases: { ...migratedString, ...migratedObject },
|
|
274
|
-
config: configOut,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* v8→v9 companion to `componentMigration_2026_05_20_sectiondividerSlimVariants`:
|
|
280
|
-
* the structured background payload is reshaped from six color families →
|
|
281
|
-
* three size variants (lg/md/sm). Canvas's gradient is taken as the canonical
|
|
282
|
-
* seed and fanned across all three new variants; other families' gradients
|
|
283
|
-
* are dropped. Idempotent — keys already at the final shape pass through.
|
|
284
|
-
*
|
|
285
|
-
* Sources accepted (in priority order, first-found wins for canvas's slot):
|
|
286
|
-
* --sectiondivider-canvas-background (current/in-progress shape)
|
|
287
|
-
* --sectiondivider-canvas-gradient (v7-era shape)
|
|
288
|
-
* --sectiondivider-color-canvas-background (intermediate shape that landed
|
|
289
|
-
* briefly during the color-prop pivot)
|
|
290
|
-
*
|
|
291
|
-
* The migration runner only sees string keys, so this object-shape rename
|
|
292
|
-
* has to live alongside the runner call.
|
|
293
|
-
*/
|
|
294
|
-
const FAMILIES = ['canvas', 'neutral', 'alternate', 'primary', 'accent', 'special'] as const;
|
|
295
|
-
const SIZE_VARIANTS = ['lg', 'md', 'sm'] as const;
|
|
296
|
-
function renameSectionDividerObjectSlots(
|
|
297
|
-
objectPart: Record<string, AliasDiskValue>,
|
|
298
|
-
): Record<string, AliasDiskValue> {
|
|
299
|
-
// Find canvas's gradient under any historical key shape.
|
|
300
|
-
const canvasSourceKeys = [
|
|
301
|
-
'--sectiondivider-canvas-background',
|
|
302
|
-
'--sectiondivider-canvas-gradient',
|
|
303
|
-
'--sectiondivider-color-canvas-background',
|
|
304
|
-
];
|
|
305
|
-
let canvasGradient: AliasDiskValue | undefined;
|
|
306
|
-
for (const key of canvasSourceKeys) {
|
|
307
|
-
if (objectPart[key] !== undefined) {
|
|
308
|
-
canvasGradient = objectPart[key];
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// If we found a canvas gradient (any era), fan it into the three new
|
|
314
|
-
// per-variant background slots and drop every other family's gradient.
|
|
315
|
-
// If no canvas gradient is found, pass through non-family keys verbatim
|
|
316
|
-
// so non-migration data (theme tokens, other components) isn't touched.
|
|
317
|
-
const out: Record<string, AliasDiskValue> = {};
|
|
318
|
-
for (const [key, value] of Object.entries(objectPart)) {
|
|
319
|
-
// Skip any sectiondivider per-family gradient; the canvas fan-out emits
|
|
320
|
-
// the canonical set below.
|
|
321
|
-
let isFamilyGradient = false;
|
|
322
|
-
for (const f of FAMILIES) {
|
|
323
|
-
if (
|
|
324
|
-
key === `--sectiondivider-${f}-background`
|
|
325
|
-
|| key === `--sectiondivider-${f}-gradient`
|
|
326
|
-
|| key === `--sectiondivider-color-${f}-background`
|
|
327
|
-
) {
|
|
328
|
-
isFamilyGradient = true;
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
// Pass through anything that's already at the final lg/md/sm shape so
|
|
333
|
-
// re-running the migration is idempotent.
|
|
334
|
-
if (isFamilyGradient) continue;
|
|
335
|
-
out[key] = value;
|
|
336
|
-
}
|
|
337
|
-
if (canvasGradient !== undefined) {
|
|
338
|
-
for (const v of SIZE_VARIANTS) {
|
|
339
|
-
out[`--sectiondivider-${v}-background`] = canvasGradient;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return out;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** SectionDivider variants that ship gradient aliases. */
|
|
346
|
-
const SECTIONDIVIDER_VARIANTS = ['canvas', 'neutral', 'alternate', 'primary', 'accent', 'special'] as const;
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Collapse legacy 7-flat-token gradients (`{angle, stop-{1,2,3}-{color,position}}`)
|
|
350
|
-
* for each sectiondivider variant into one structured gradient alias at
|
|
351
|
-
* `--sectiondivider-{v}-gradient`. The flat tokens stay in `stringPart`
|
|
352
|
-
* for the migration runner to strip; the structured payload joins
|
|
353
|
-
* `objectPart` so the merge produces the new shape.
|
|
354
|
-
*
|
|
355
|
-
* Skips variants that already declare a gradient alias (object form) so
|
|
356
|
-
* round-tripping doesn't overwrite user edits.
|
|
357
|
-
*/
|
|
358
|
-
function synthesizeSectionDividerGradients(
|
|
359
|
-
stringPart: Record<string, string>,
|
|
360
|
-
objectPart: Record<string, AliasDiskValue>,
|
|
361
|
-
): void {
|
|
362
|
-
const GRADIENT_ANGLE_TOKENS: Record<string, number> = {
|
|
363
|
-
'--gradient-angle-horizontal': 90,
|
|
364
|
-
'--gradient-angle-vertical': 180,
|
|
365
|
-
'--gradient-angle-diagonal': 135,
|
|
366
|
-
'--gradient-angle-counter-diagonal': 45,
|
|
367
|
-
};
|
|
368
|
-
const GRADIENT_STOP_POSITIONS: Record<string, number> = {
|
|
369
|
-
'--gradient-stop-start': 0,
|
|
370
|
-
'--gradient-stop-mid': 50,
|
|
371
|
-
'--gradient-stop-end': 100,
|
|
372
|
-
};
|
|
373
|
-
const parseAngle = (raw: string | undefined): number => {
|
|
374
|
-
if (!raw) return 135;
|
|
375
|
-
if (raw.startsWith('--')) return GRADIENT_ANGLE_TOKENS[raw] ?? 135;
|
|
376
|
-
const m = raw.match(/^(-?\d+(?:\.\d+)?)\s*deg$/i);
|
|
377
|
-
return m ? parseFloat(m[1]) : 135;
|
|
378
|
-
};
|
|
379
|
-
const parsePosition = (raw: string | undefined, fallback: number): number => {
|
|
380
|
-
if (!raw) return fallback;
|
|
381
|
-
if (raw.startsWith('--')) return GRADIENT_STOP_POSITIONS[raw] ?? fallback;
|
|
382
|
-
const m = raw.match(/^(-?\d+(?:\.\d+)?)\s*%$/);
|
|
383
|
-
return m ? parseFloat(m[1]) : fallback;
|
|
384
|
-
};
|
|
385
|
-
const parseStopColor = (raw: string | undefined): { color: string; opacity?: number } | null => {
|
|
386
|
-
if (!raw) return null;
|
|
387
|
-
if (raw.startsWith('--')) return { color: raw };
|
|
388
|
-
const mix = raw.match(/^color-mix\(\s*in\s+srgb\s*,\s*var\((--[a-z0-9-]+)\)\s+([\d.]+)%\s*,\s*transparent\s*\)$/i);
|
|
389
|
-
if (mix) return { color: mix[1], opacity: parseFloat(mix[2]) };
|
|
390
|
-
return null;
|
|
391
|
-
};
|
|
392
|
-
for (const v of SECTIONDIVIDER_VARIANTS) {
|
|
393
|
-
const slot = `--sectiondivider-${v}-gradient`;
|
|
394
|
-
if (slot in objectPart) continue;
|
|
395
|
-
const angleKey = `${slot}-angle`;
|
|
396
|
-
const has1 = stringPart[`${slot}-stop-1-color`] !== undefined;
|
|
397
|
-
const has2 = stringPart[`${slot}-stop-2-color`] !== undefined;
|
|
398
|
-
const has3 = stringPart[`${slot}-stop-3-color`] !== undefined;
|
|
399
|
-
if (!has1 && !has2 && !has3) continue;
|
|
400
|
-
const stops: { position: number; color: string; opacity?: number }[] = [];
|
|
401
|
-
const positionDefaults = [0, 50, 100];
|
|
402
|
-
for (let i = 1; i <= 3; i++) {
|
|
403
|
-
const c = parseStopColor(stringPart[`${slot}-stop-${i}-color`]);
|
|
404
|
-
if (!c) continue;
|
|
405
|
-
const pos = parsePosition(stringPart[`${slot}-stop-${i}-position`], positionDefaults[i - 1]);
|
|
406
|
-
stops.push({ position: pos, color: c.color, ...(c.opacity !== undefined ? { opacity: c.opacity } : {}) });
|
|
407
|
-
}
|
|
408
|
-
if (stops.length < 2) continue;
|
|
409
|
-
objectPart[slot] = {
|
|
410
|
-
kind: 'gradient',
|
|
411
|
-
value: {
|
|
412
|
-
type: 'linear',
|
|
413
|
-
angle: parseAngle(stringPart[angleKey]),
|
|
414
|
-
stops,
|
|
415
|
-
},
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
224
|
+
//
|
|
225
|
+
// The migration itself — `migrateComponentConfig`, plus its sectiondivider
|
|
226
|
+
// gradient helpers — lives in `../themes/migrateComponentConfig` so the
|
|
227
|
+
// server (`normalizeTheme`) can run the same pass over an embedded theme
|
|
228
|
+
// config instead of reading it raw.
|
|
419
229
|
|
|
420
230
|
/**
|
|
421
231
|
* Disk-shape → in-memory split. Routes legacy single-bucket aliases that
|
|
@@ -459,7 +269,7 @@ export function toComponentSlice(
|
|
|
459
269
|
config?: Record<string, unknown>,
|
|
460
270
|
schemaVersion: number = 0,
|
|
461
271
|
): { aliases: Record<string, CssVarRef>; config: Record<string, unknown> } {
|
|
462
|
-
const migrated =
|
|
272
|
+
const migrated = migrateComponentConfig(component, aliases, config, schemaVersion);
|
|
463
273
|
return splitAliasesAndConfig(migrated.aliases, migrated.config);
|
|
464
274
|
}
|
|
465
275
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PaletteConfig, FontSource, FontStack } from '../themes/themeTypes';
|
|
2
|
+
import type { GradientStop, GradientValue } from '../themes/parsers/gradient';
|
|
2
3
|
import type { HarmonyAxis } from '../palettes/colorHarmony';
|
|
3
4
|
|
|
4
5
|
export interface ShadowGlobals {
|
|
@@ -46,20 +47,9 @@ export interface ColumnsState {
|
|
|
46
47
|
* round-trip when the user toggles back to a real gradient. */
|
|
47
48
|
export type GradientType = 'linear' | 'radial' | 'solid' | 'none';
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/** CSS variable name the stop resolves through (e.g. '--color-brand-500'). */
|
|
53
|
-
color: string;
|
|
54
|
-
/** When `false`, this stop is an explicit off-palette override: the stop
|
|
55
|
-
* picker opens to any color (not family-filtered), and the stop is skipped
|
|
56
|
-
* by family-swap rewrites so its color survives a variant family change.
|
|
57
|
-
* Defaults to true (follow family) on read; absence is the same as `true`.
|
|
58
|
-
* Only meaningful for gradients rendered inside a family-aware context. */
|
|
59
|
-
monochrome?: boolean;
|
|
60
|
-
/** 0–100 alpha applied to the stop's color. Defaults to 100 (fully opaque). */
|
|
61
|
-
opacity?: number;
|
|
62
|
-
}
|
|
50
|
+
/** One stop of a gradient. Defined by `themes/parsers/gradient`, which owns
|
|
51
|
+
* the shape so the CSS form and the persisted form cannot drift apart. */
|
|
52
|
+
export type GradientTokenStop = GradientStop;
|
|
63
53
|
|
|
64
54
|
export interface GradientToken {
|
|
65
55
|
/** Output CSS variable, e.g. '--gradient-1'. */
|
|
@@ -83,22 +73,11 @@ export interface GradientToken {
|
|
|
83
73
|
}
|
|
84
74
|
|
|
85
75
|
/** Structured gradient payload carried inline on a component alias.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
angle: number;
|
|
92
|
-
/** See GradientToken.radius. */
|
|
93
|
-
radius?: number;
|
|
94
|
-
/** See GradientToken.centerX. */
|
|
95
|
-
centerX?: number;
|
|
96
|
-
/** See GradientToken.aspectX. */
|
|
97
|
-
aspectX?: number;
|
|
98
|
-
/** See GradientToken.aspectY. */
|
|
99
|
-
aspectY?: number;
|
|
100
|
-
stops: GradientTokenStop[];
|
|
101
|
-
}
|
|
76
|
+
* `GradientToken` minus `variable` (the alias key itself is the binding).
|
|
77
|
+
* Used when a component owns a per-instance gradient that doesn't share the
|
|
78
|
+
* theme-level `--gradient-N` library. Defined by `themes/parsers/gradient`,
|
|
79
|
+
* the module that serializes and parses this shape. */
|
|
80
|
+
export type GradientAliasValue = GradientValue;
|
|
102
81
|
|
|
103
82
|
export type CssVarRef =
|
|
104
83
|
/** An alias to a design token. `opacity` is an optional integer percent set
|