@motion-proto/live-tokens 0.68.1 → 0.70.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-build-page/SKILL.md +16 -5
- package/.claude/skills/live-tokens-create-component/SKILL.md +55 -19
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +30 -1
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +133 -0
- package/.claude/skills/live-tokens-pick-component/SKILL.md +10 -1
- package/CHANGELOG.md +236 -0
- package/README.md +13 -4
- package/bin/check-component.mjs +367 -63
- package/bin/check-page.mjs +409 -0
- package/bin/cli.mjs +107 -9
- package/bin/lib/catalogue.mjs +123 -0
- package/bin/lib/cssValues.mjs +50 -0
- package/bin/lib/findings.mjs +106 -0
- package/bin/lib/tokenVocabulary.mjs +240 -0
- package/dist-plugin/adjust/index.cjs +174 -23
- package/dist-plugin/adjust/index.js +68 -23
- package/dist-plugin/{chunk-2UX6EVVA.js → chunk-2YNERPXY.js} +1 -1
- package/dist-plugin/{chunk-NE6N66EE.js → chunk-GPIBU44G.js} +107 -1
- package/dist-plugin/{chunk-ZHPX7ZYQ.js → chunk-RFVYPNRO.js} +39 -1
- package/dist-plugin/generateColorsAndType/index.cjs +107 -1
- package/dist-plugin/generateColorsAndType/index.js +1 -1
- package/dist-plugin/index.cjs +146 -2
- package/dist-plugin/index.js +3 -3
- package/dist-plugin/migrateData/index.cjs +107 -1
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/tokensCssMigrations/index.cjs +39 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +3 -2
- package/src/app/site.css +4 -4
- package/src/editor/component-editor/ButtonEditor.svelte +71 -4
- package/src/editor/component-editor/CardEditor.svelte +6 -6
- package/src/editor/component-editor/DialogEditor.svelte +3 -3
- package/src/editor/component-editor/IconButtonEditor.svelte +68 -3
- package/src/editor/component-editor/ImageEditor.svelte +8 -8
- package/src/editor/component-editor/ImageLightboxEditor.svelte +12 -1
- package/src/editor/component-editor/MenuSelectEditor.svelte +63 -3
- package/src/editor/component-editor/SegmentedControlEditor.svelte +63 -3
- package/src/editor/component-editor/SideNavigationEditor.svelte +67 -3
- package/src/editor/component-editor/SliderEditor.svelte +186 -0
- package/src/editor/component-editor/TabBarEditor.svelte +63 -3
- package/src/editor/component-editor/registry.ts +10 -0
- package/src/editor/core/components/aliasKinds.ts +51 -28
- package/src/editor/core/sketch/sketchLayer.ts +16 -0
- package/src/editor/core/store/editorPersistence.ts +44 -1
- package/src/editor/core/store/editorRenderer.ts +2 -2
- package/src/editor/core/store/editorStore.ts +18 -18
- package/src/editor/core/store/editorTypes.ts +7 -6
- package/src/editor/core/themes/migrations/2026-09-01-gate-suffix-enabled.ts +31 -0
- package/src/editor/core/themes/migrations/2026-09-01-scrim-rename.ts +52 -0
- package/src/editor/core/themes/migrations/2026-09-01-tabbar-active-tint.ts +27 -0
- package/src/editor/core/themes/migrations/2026-09-01-tint-rename.ts +42 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/slices/domainVars.ts +2 -2
- package/src/editor/core/themes/slices/washes.ts +107 -0
- package/src/editor/docs/content/editing-tokens.md +5 -3
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/index.ts +1 -1
- package/src/editor/pages/EditorShell.svelte +1 -1
- package/src/editor/ui/SurfacesTab.svelte +3 -3
- package/src/editor/ui/UITokenSelector.svelte +1 -0
- package/src/editor/ui/VariablesTab.svelte +2 -2
- package/src/editor/ui/sections/{OverlaysSection.svelte → WashesSection.svelte} +44 -43
- package/src/live-tokens/data/colors-and-type/autumn.json +6 -6
- package/src/live-tokens/data/colors-and-type/default.json +6 -6
- package/src/live-tokens/data/colors-and-type/halloween.json +6 -6
- package/src/live-tokens/data/colors-and-type/midnight-study.json +6 -6
- package/src/live-tokens/data/colors-and-type/ocean.json +6 -6
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +6 -6
- package/src/live-tokens/data/colors-and-type/sketchy.json +6 -6
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +6 -6
- package/src/live-tokens/data/colors-and-type/sunset.json +6 -6
- package/src/live-tokens/data/themes/autumn.json +81 -13
- package/src/live-tokens/data/themes/halloween.json +81 -13
- package/src/live-tokens/data/themes/midnight-study.json +81 -13
- package/src/live-tokens/data/themes/ocean.json +81 -13
- package/src/live-tokens/data/themes/royal-velvet.json +81 -13
- package/src/live-tokens/data/themes/sketchy.json +81 -13
- package/src/live-tokens/data/themes/spring-meadow.json +81 -13
- package/src/live-tokens/data/themes/sunset.json +81 -13
- package/src/live-tokens/data/tokens.generated.css +6 -6
- package/src/system/components/Button.svelte +28 -10
- package/src/system/components/Card.svelte +6 -6
- package/src/system/components/Dialog.svelte +3 -3
- package/src/system/components/IconButton.svelte +23 -7
- package/src/system/components/Image.svelte +6 -6
- package/src/system/components/MenuSelect.svelte +17 -1
- package/src/system/components/SegmentedControl.svelte +20 -2
- package/src/system/components/SideNavigation.svelte +22 -2
- package/src/system/components/Slider.svelte +348 -0
- package/src/system/components/TabBar.svelte +20 -2
- package/src/system/styles/CONVENTIONS.md +2 -2
- package/src/system/styles/tokens.css +12 -4
- package/template/package.json +3 -2
- package/template/src/pages/Home.svelte +1 -1
- package/src/editor/core/themes/slices/overlays.ts +0 -101
|
@@ -21,36 +21,59 @@ export type TokenKind =
|
|
|
21
21
|
| 'easing'
|
|
22
22
|
| 'text-color';
|
|
23
23
|
|
|
24
|
-
/** Suffix
|
|
25
|
-
editor's selector layout
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{ kind: '
|
|
40
|
-
{ kind: '
|
|
41
|
-
{ kind: '
|
|
42
|
-
{ kind: '
|
|
43
|
-
{ kind: '
|
|
44
|
-
|
|
45
|
-
{ kind: '
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{ kind: '
|
|
49
|
-
{ kind: '
|
|
50
|
-
{ kind: '
|
|
51
|
-
{ kind: '
|
|
24
|
+
/** Suffix and prefix data mapped to kinds — the one source of truth for the
|
|
25
|
+
editor's selector layout, the `adjust` CLI, and `check-component`'s naming
|
|
26
|
+
rule, so the three cannot drift. `bin/check-component.mjs` reads the
|
|
27
|
+
`suffix:` arrays out of this file, which is why they are plain literals.
|
|
28
|
+
|
|
29
|
+
Order matters: `-text` must run before `-border`/`-surface`, and
|
|
30
|
+
`-accent-width` before `-accent`, because the first match wins. A variable
|
|
31
|
+
matching nothing falls through to `text-color` (a palette picker), but that
|
|
32
|
+
fall-through is a smell — `check-component` rejects an unrecognised suffix,
|
|
33
|
+
so add the name here rather than letting it drift. */
|
|
34
|
+
export const KIND_RULES: ReadonlyArray<{
|
|
35
|
+
kind: TokenKind;
|
|
36
|
+
suffix?: readonly string[];
|
|
37
|
+
prefix?: readonly string[];
|
|
38
|
+
}> = [
|
|
39
|
+
{ kind: 'font-family', suffix: ['-font-family'] },
|
|
40
|
+
{ kind: 'font-weight', suffix: ['-font-weight'] },
|
|
41
|
+
{ kind: 'font-size', suffix: ['-font-size', '-icon-size', '-thumb-size'] },
|
|
42
|
+
{ kind: 'line-height', suffix: ['-line-height'] },
|
|
43
|
+
{ kind: 'letter-spacing', suffix: ['-letter-spacing'] },
|
|
44
|
+
// Element-named colour roles: the property *is* the element it paints.
|
|
45
|
+
{ kind: 'text-color', suffix: ['-text', '-label', '-icon', '-title', '-body', '-eyebrow',
|
|
46
|
+
'-description', '-hint', '-error', '-placeholder', '-value'],
|
|
47
|
+
prefix: ['--text-'] },
|
|
48
|
+
{ kind: 'radius', suffix: ['-radius'], prefix: ['--radius-'] },
|
|
49
|
+
{ kind: 'divider-width', suffix: ['-divider-width', '-divider-thickness'] },
|
|
50
|
+
{ kind: 'divider-height', suffix: ['-divider-height', '-track-height'] },
|
|
51
|
+
{ kind: 'divider-inset', suffix: ['-divider-inset', '-inset'] },
|
|
52
|
+
{ kind: 'dot-size', suffix: ['-dot-size'] },
|
|
53
|
+
{ kind: 'blur', suffix: ['-blur'], prefix: ['--blur-'] },
|
|
54
|
+
{ kind: 'scale', suffix: ['-scale'], prefix: ['--scale-'] },
|
|
55
|
+
{ kind: 'shadow', suffix: ['-shadow'], prefix: ['--shadow-'] },
|
|
56
|
+
{ kind: 'padding', suffix: ['-padding', '-margin'] },
|
|
57
|
+
{ kind: 'gap', suffix: ['-gap'] },
|
|
58
|
+
{ kind: 'duration', suffix: ['-duration'], prefix: ['--duration-'] },
|
|
59
|
+
{ kind: 'easing', suffix: ['-easing'], prefix: ['--ease-'] },
|
|
60
|
+
{ kind: 'border-width', suffix: ['-border-width', '-accent-width', '-hairline-thickness', '-thickness'],
|
|
61
|
+
prefix: ['--border-width-'] },
|
|
62
|
+
{ kind: 'border', suffix: ['-border'], prefix: ['--border-'] },
|
|
63
|
+
// Fills. A tint is a wash over a surface, so it takes the surface picker: the
|
|
64
|
+
// full palette with an alpha, not just the tint stops it defaults to.
|
|
65
|
+
{ kind: 'surface', suffix: ['-surface', '-fill', '-divider', '-background', '-indicator',
|
|
66
|
+
'-thumb', '-accent', '-color', '-tint', '-opacity',
|
|
67
|
+
'-width', '-height', '-size'],
|
|
68
|
+
prefix: ['--surface-', '--tint', '--color-'] },
|
|
52
69
|
];
|
|
53
70
|
|
|
71
|
+
export const KIND_PATTERNS: ReadonlyArray<{ kind: TokenKind; matches: (v: string) => boolean }> =
|
|
72
|
+
KIND_RULES.map(({ kind, suffix = [], prefix = [] }) => ({
|
|
73
|
+
kind,
|
|
74
|
+
matches: (v: string) => suffix.some((s) => v.endsWith(s)) || prefix.some((p) => v.startsWith(p)),
|
|
75
|
+
}));
|
|
76
|
+
|
|
54
77
|
export function rawKind(variable: string): TokenKind {
|
|
55
78
|
for (const { kind, matches } of KIND_PATTERNS) {
|
|
56
79
|
if (matches(variable)) return kind;
|
|
@@ -227,6 +227,22 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
227
227
|
// Holds the picture inside the frame; without it a zoom spills out square.
|
|
228
228
|
clips: true,
|
|
229
229
|
},
|
|
230
|
+
...['single', 'range'].map((v) => ({
|
|
231
|
+
sel: `.slider.${v} .slider-track`,
|
|
232
|
+
fill: `var(--slider-${v}-track-surface)`,
|
|
233
|
+
stroke: `var(--slider-${v}-track-border)`,
|
|
234
|
+
radius: `var(--slider-${v}-track-radius, 0px)`,
|
|
235
|
+
})),
|
|
236
|
+
// The fill rides inside the track, so it is stroked by nothing and hatched
|
|
237
|
+
// in the track's ink.
|
|
238
|
+
...['single', 'range'].map((v) => ({
|
|
239
|
+
sel: `.slider.${v} .slider-fill`,
|
|
240
|
+
fill: `var(--slider-${v}-fill)`,
|
|
241
|
+
stroke: 'transparent',
|
|
242
|
+
hatch: `var(--slider-${v}-track-border)`,
|
|
243
|
+
radius: `var(--slider-${v}-track-radius, 0px)`,
|
|
244
|
+
positioned: true,
|
|
245
|
+
})),
|
|
230
246
|
{ sel: '.toggle .track', stem: 'toggle-track' },
|
|
231
247
|
{ sel: '.toggle.on .track', stem: 'toggle-on-track', radius: 'var(--toggle-track-radius, 0px)' },
|
|
232
248
|
|
|
@@ -20,6 +20,7 @@ import { store } from './editorCore';
|
|
|
20
20
|
import { quietGet, quietSet } from '../storage/storage';
|
|
21
21
|
import { isGradientSlot, makeDefaultGradients } from '../themes/slices/gradients';
|
|
22
22
|
import { seedShadowsFromDom } from '../themes/slices/shadows';
|
|
23
|
+
import { makeDefaultScrimTokens, makeDefaultTintTokens } from '../themes/slices/washes';
|
|
23
24
|
import { sanitizeHarmonyAxes } from '../palettes/colorHarmony';
|
|
24
25
|
import {
|
|
25
26
|
renameBackgroundPaletteKey,
|
|
@@ -77,6 +78,48 @@ function migrateGradients(state: EditorState): EditorState {
|
|
|
77
78
|
// calls `Object.entries(slice.config)` unconditionally, so backfill the
|
|
78
79
|
// required fields and drop any non-object slice before the state reaches the
|
|
79
80
|
// renderer. Spread preserves optional fields like `unlinked`.
|
|
81
|
+
// The washes slice was `overlays: { tokens, hoverTokens }` before the scrim and
|
|
82
|
+
// tint renames, and `hydrate` shallow-merges, so a persisted state from an
|
|
83
|
+
// older build replaces `washes` wholesale and arrives without `tints`.
|
|
84
|
+
// `washesToVars` iterates both lists unconditionally, so reshape here: carry the
|
|
85
|
+
// stops across under their new names, and rewrite the variable each one carries.
|
|
86
|
+
const WASH_VAR_RENAMES: Record<string, string> = {
|
|
87
|
+
'--overlay-low': '--scrim-low',
|
|
88
|
+
'--overlay': '--scrim',
|
|
89
|
+
'--overlay-high': '--scrim-high',
|
|
90
|
+
'--hover-low': '--tint-low',
|
|
91
|
+
'--hover': '--tint',
|
|
92
|
+
'--hover-high': '--tint-high',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type LegacyWashes = {
|
|
96
|
+
scrims?: unknown;
|
|
97
|
+
tints?: unknown;
|
|
98
|
+
hoverTokens?: unknown;
|
|
99
|
+
tokens?: unknown;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Carry a persisted stop list forward, or fall back to the shipped defaults. */
|
|
103
|
+
function washList(raw: unknown, fallback: () => EditorState['washes']['scrims']) {
|
|
104
|
+
if (!Array.isArray(raw) || raw.length === 0) return fallback();
|
|
105
|
+
const carried = raw
|
|
106
|
+
.filter((t): t is EditorState['washes']['scrims'][number] => !!t && typeof t === 'object' && 'variable' in t)
|
|
107
|
+
.map((t) => ({ ...t, variable: WASH_VAR_RENAMES[t.variable] ?? t.variable }));
|
|
108
|
+
return carried.length > 0 ? carried : fallback();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeWashes(state: EditorState): EditorState {
|
|
112
|
+
const washes = (state.washes ?? {}) as LegacyWashes;
|
|
113
|
+
const legacy = ((state as unknown as { overlays?: LegacyWashes }).overlays ?? {}) as LegacyWashes;
|
|
114
|
+
const next = { ...state } as EditorState & { overlays?: unknown };
|
|
115
|
+
next.washes = {
|
|
116
|
+
scrims: washList(washes.scrims ?? legacy.tokens, makeDefaultScrimTokens),
|
|
117
|
+
tints: washList(washes.tints ?? washes.hoverTokens ?? legacy.hoverTokens, makeDefaultTintTokens),
|
|
118
|
+
};
|
|
119
|
+
delete next.overlays;
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
|
|
80
123
|
export function normalizeComponents(state: EditorState): EditorState {
|
|
81
124
|
const raw = state.components;
|
|
82
125
|
if (!raw || typeof raw !== 'object') return { ...state, components: {} };
|
|
@@ -134,7 +177,7 @@ export function hydrate(): void {
|
|
|
134
177
|
store.set(
|
|
135
178
|
normalizeHarmonyAxes(
|
|
136
179
|
normalizeBaseAnchors(
|
|
137
|
-
normalizePaletteBasis(normalizePaletteLabels(normalizeComponents(migrateGradients(merged)))),
|
|
180
|
+
normalizePaletteBasis(normalizePaletteLabels(normalizeComponents(normalizeWashes(migrateGradients(merged))))),
|
|
138
181
|
),
|
|
139
182
|
),
|
|
140
183
|
);
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
editorState,
|
|
20
20
|
columnsToVars,
|
|
21
21
|
columnsEqualsDefault,
|
|
22
|
-
|
|
22
|
+
washesToVars,
|
|
23
23
|
shadowsToVars,
|
|
24
24
|
gradientsToVars,
|
|
25
25
|
componentsToVars,
|
|
@@ -30,7 +30,7 @@ export function deriveCssVars(state: EditorState): Record<string, string> {
|
|
|
30
30
|
if (!columnsEqualsDefault(state.columns)) {
|
|
31
31
|
Object.assign(out, columnsToVars(state.columns));
|
|
32
32
|
}
|
|
33
|
-
Object.assign(out,
|
|
33
|
+
Object.assign(out, washesToVars(state.washes));
|
|
34
34
|
if (state.shadows.tokens.length > 0) {
|
|
35
35
|
Object.assign(out, shadowsToVars(state.shadows));
|
|
36
36
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central editor state store — the single mutation funnel.
|
|
3
3
|
*
|
|
4
|
-
* All editor state (palettes, fonts, shadows,
|
|
4
|
+
* All editor state (palettes, fonts, shadows, washes, columns, ad-hoc CSS
|
|
5
5
|
* vars) lives in one `EditorState` tree. Every change must go through
|
|
6
6
|
* `mutate` (or a transaction). History is captured automatically; the
|
|
7
7
|
* renderer subscribes and writes derived CSS vars to :root via `cssVarSync`
|
|
@@ -62,10 +62,10 @@ import {
|
|
|
62
62
|
loadColumnsFromVars,
|
|
63
63
|
} from '../themes/slices/columns';
|
|
64
64
|
import {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} from '../themes/slices/
|
|
65
|
+
loadWashesFromVars,
|
|
66
|
+
makeDefaultWashesState,
|
|
67
|
+
washesToVars,
|
|
68
|
+
} from '../themes/slices/washes';
|
|
69
69
|
import {
|
|
70
70
|
loadShadowsFromVars,
|
|
71
71
|
shadowsToVars,
|
|
@@ -99,7 +99,7 @@ function emptyState(): EditorState {
|
|
|
99
99
|
tokens: [],
|
|
100
100
|
overrides: {},
|
|
101
101
|
},
|
|
102
|
-
|
|
102
|
+
washes: makeDefaultWashesState(),
|
|
103
103
|
columns: { ...DEFAULT_COLUMNS },
|
|
104
104
|
components: {},
|
|
105
105
|
gradients: { tokens: makeDefaultGradients() },
|
|
@@ -150,15 +150,15 @@ export {
|
|
|
150
150
|
} from '../themes/slices/columns';
|
|
151
151
|
|
|
152
152
|
export {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
} from '../themes/slices/
|
|
153
|
+
washesToVars,
|
|
154
|
+
WASH_VAR_NAMES,
|
|
155
|
+
applyWashVarsToState,
|
|
156
|
+
makeDefaultWashesState,
|
|
157
|
+
makeDefaultScrimTokens,
|
|
158
|
+
makeDefaultTintTokens,
|
|
159
|
+
washTokenToCss,
|
|
160
|
+
parseWashCss,
|
|
161
|
+
} from '../themes/slices/washes';
|
|
162
162
|
|
|
163
163
|
export {
|
|
164
164
|
shadowsToVars,
|
|
@@ -364,7 +364,7 @@ type DomainLoader = (next: EditorState, rawVars: Record<string, string>) => void
|
|
|
364
364
|
|
|
365
365
|
const domainLoaders: Record<string, DomainLoader> = {
|
|
366
366
|
columns: loadColumnsFromVars,
|
|
367
|
-
|
|
367
|
+
washes: loadWashesFromVars,
|
|
368
368
|
shadows: loadShadowsFromVars,
|
|
369
369
|
components: loadComponentsFromVars,
|
|
370
370
|
};
|
|
@@ -428,7 +428,7 @@ function colorsAndTypeContent(state: EditorState): Omit<ColorsAndType, 'name' |
|
|
|
428
428
|
if (!columnsEqualsDefault(state.columns)) {
|
|
429
429
|
Object.assign(cssVariables, columnsToVars(state.columns));
|
|
430
430
|
}
|
|
431
|
-
Object.assign(cssVariables,
|
|
431
|
+
Object.assign(cssVariables, washesToVars(state.washes));
|
|
432
432
|
if (state.shadows.tokens.length > 0) {
|
|
433
433
|
Object.assign(cssVariables, shadowsToVars(state.shadows));
|
|
434
434
|
}
|
|
@@ -450,7 +450,7 @@ function colorsAndTypeContent(state: EditorState): Omit<ColorsAndType, 'name' |
|
|
|
450
450
|
|
|
451
451
|
/**
|
|
452
452
|
* Serialize current state for saving. Domains with their own typed state
|
|
453
|
-
* (columns,
|
|
453
|
+
* (columns, washes, shadows) fold derived vars into `cssVariables` only
|
|
454
454
|
* when they diverge from defaults; the catch-all `cssVars` bag carries
|
|
455
455
|
* everything not yet migrated to a typed domain.
|
|
456
456
|
*
|
|
@@ -23,9 +23,10 @@ export interface ShadowOverrideFlags {
|
|
|
23
23
|
distance: boolean; blur: boolean; size: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* `color-mix(in srgb, var(<alias>) <opacity%>, transparent)`.
|
|
28
|
-
|
|
26
|
+
/** A wash: an aliased color token + an opacity. Emits as
|
|
27
|
+
* `color-mix(in srgb, var(<alias>) <opacity%>, transparent)`. Scrims dim what
|
|
28
|
+
* is behind them; tints shade the surface they sit on. */
|
|
29
|
+
export interface WashToken {
|
|
29
30
|
variable: string;
|
|
30
31
|
label: string;
|
|
31
32
|
alias: string;
|
|
@@ -111,9 +112,9 @@ export interface EditorState {
|
|
|
111
112
|
tokens: ShadowToken[];
|
|
112
113
|
overrides: Record<string, ShadowOverrideFlags>;
|
|
113
114
|
};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
washes: {
|
|
116
|
+
scrims: WashToken[];
|
|
117
|
+
tints: WashToken[];
|
|
117
118
|
};
|
|
118
119
|
columns: ColumnsState;
|
|
119
120
|
components: Record<string, ComponentSlice>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gate suffix, state word to `-enabled` (2026-09-01).
|
|
5
|
+
*
|
|
6
|
+
* An optional interaction is switched by a gate variable. Those gates were named
|
|
7
|
+
* with a state word at the end (`--card-hover-border-active`,
|
|
8
|
+
* `--image-zoom-hover`), which reads as state-after-property and fails
|
|
9
|
+
* `check-component`. A gate is not a state: it says whether the interaction is
|
|
10
|
+
* on at all, so it takes `-enabled`.
|
|
11
|
+
*
|
|
12
|
+
* Values are unchanged; a gate still holds either its off value or its on value.
|
|
13
|
+
*/
|
|
14
|
+
const RENAMED: Record<string, string> = {
|
|
15
|
+
'--card-hover-border-active': '--card-hover-border-enabled',
|
|
16
|
+
'--card-hover-shadow-active': '--card-hover-shadow-enabled',
|
|
17
|
+
'--image-zoom-hover': '--image-zoom-enabled',
|
|
18
|
+
'--image-grow-hover': '--image-grow-enabled',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const componentMigration_2026_09_01_gateSuffixEnabled: Migration = {
|
|
22
|
+
id: '2026-09-01-gate-suffix-enabled',
|
|
23
|
+
fromVersion: 24,
|
|
24
|
+
toVersion: 25,
|
|
25
|
+
appliesTo: 'component-config',
|
|
26
|
+
apply(rawVars) {
|
|
27
|
+
const out: Record<string, string> = {};
|
|
28
|
+
for (const [key, value] of Object.entries(rawVars)) out[RENAMED[key] ?? key] = value;
|
|
29
|
+
return out;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scrim rename (2026-09-01): `--overlay-*` became `--scrim-*`.
|
|
5
|
+
*
|
|
6
|
+
* An overlay is a scrim, a translucent layer that dims what is behind it. The
|
|
7
|
+
* name had spread to cover surface tints too, which are the opposite operation,
|
|
8
|
+
* and it collided with `backdrop`, an exported concept about what paints behind
|
|
9
|
+
* an element. Each name now means one thing.
|
|
10
|
+
*
|
|
11
|
+
* Values are unchanged, so this is a pure key rename in colors-and-type files.
|
|
12
|
+
* Component configs need both halves: Dialog's `--dialog-overlay-surface` is a
|
|
13
|
+
* key, and any alias *value* pointing at a scrim stop is a bare token name that
|
|
14
|
+
* has to be rebound.
|
|
15
|
+
*/
|
|
16
|
+
const RENAMED: Record<string, string> = {
|
|
17
|
+
'--overlay-low': '--scrim-low',
|
|
18
|
+
'--overlay': '--scrim',
|
|
19
|
+
'--overlay-high': '--scrim-high',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const RENAMED_COMPONENT_KEYS: Record<string, string> = {
|
|
23
|
+
'--dialog-overlay-surface': '--dialog-scrim-surface',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const colorsAndTypeMigration_2026_09_01_scrimRename: Migration = {
|
|
27
|
+
id: '2026-09-01-scrim-rename-theme',
|
|
28
|
+
fromVersion: 5,
|
|
29
|
+
toVersion: 6,
|
|
30
|
+
appliesTo: 'colors-and-type',
|
|
31
|
+
apply(rawVars) {
|
|
32
|
+
const out: Record<string, string> = {};
|
|
33
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
34
|
+
out[RENAMED[key] ?? key] = value;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const componentMigration_2026_09_01_scrimRename: Migration = {
|
|
41
|
+
id: '2026-09-01-scrim-rename-component',
|
|
42
|
+
fromVersion: 21,
|
|
43
|
+
toVersion: 22,
|
|
44
|
+
appliesTo: 'component-config',
|
|
45
|
+
apply(rawVars) {
|
|
46
|
+
const out: Record<string, string> = {};
|
|
47
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
48
|
+
out[RENAMED_COMPONENT_KEYS[key] ?? key] = RENAMED[value] ?? value;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TabBar active surface, scrim to tint (2026-09-01, tabbar only).
|
|
5
|
+
*
|
|
6
|
+
* The active tab is a wash *on* the bar, not a layer dimming what is behind it,
|
|
7
|
+
* so it belongs to the tint family. It only ever read a scrim because no tint
|
|
8
|
+
* family existed. Rebinding is a visible change: the 38% near-black scrim
|
|
9
|
+
* becomes a 10% white tint, so the active tab reads lighter rather than darker.
|
|
10
|
+
*
|
|
11
|
+
* Scoped to the shipped default value. A theme that pointed this alias
|
|
12
|
+
* somewhere else keeps its choice.
|
|
13
|
+
*/
|
|
14
|
+
export const componentMigration_2026_09_01_tabbarActiveTint: Migration = {
|
|
15
|
+
id: '2026-09-01-tabbar-active-tint',
|
|
16
|
+
fromVersion: 23,
|
|
17
|
+
toVersion: 24,
|
|
18
|
+
appliesTo: 'component-config',
|
|
19
|
+
apply(rawVars, meta) {
|
|
20
|
+
if (meta.component !== 'tabbar') return { ...rawVars };
|
|
21
|
+
const out = { ...rawVars };
|
|
22
|
+
if (out['--tabbar-active-surface'] === '--scrim-low') {
|
|
23
|
+
out['--tabbar-active-surface'] = '--tint-low';
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tint rename (2026-09-01): `--hover-*` became `--tint-*`.
|
|
5
|
+
*
|
|
6
|
+
* A state is a segment of a property name (`--button-outline-hover-surface`),
|
|
7
|
+
* not a token of its own, so the three stops are named for what they are: a
|
|
8
|
+
* tint shades the surface it sits on. Values are unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Nothing shipped read the old names, so this only touches saved files that
|
|
11
|
+
* captured them from the editor's own stops.
|
|
12
|
+
*/
|
|
13
|
+
const RENAMED: Record<string, string> = {
|
|
14
|
+
'--hover-low': '--tint-low',
|
|
15
|
+
'--hover': '--tint',
|
|
16
|
+
'--hover-high': '--tint-high',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const colorsAndTypeMigration_2026_09_01_tintRename: Migration = {
|
|
20
|
+
id: '2026-09-01-tint-rename-theme',
|
|
21
|
+
fromVersion: 6,
|
|
22
|
+
toVersion: 7,
|
|
23
|
+
appliesTo: 'colors-and-type',
|
|
24
|
+
apply(rawVars) {
|
|
25
|
+
const out: Record<string, string> = {};
|
|
26
|
+
for (const [key, value] of Object.entries(rawVars)) out[RENAMED[key] ?? key] = value;
|
|
27
|
+
return out;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const componentMigration_2026_09_01_tintRename: Migration = {
|
|
32
|
+
id: '2026-09-01-tint-rename-component',
|
|
33
|
+
fromVersion: 22,
|
|
34
|
+
toVersion: 23,
|
|
35
|
+
appliesTo: 'component-config',
|
|
36
|
+
apply(rawVars) {
|
|
37
|
+
const out: Record<string, string> = {};
|
|
38
|
+
// Alias values are bare token names; rebind any that pointed at a hover stop.
|
|
39
|
+
for (const [key, value] of Object.entries(rawVars)) out[key] = RENAMED[value] ?? value;
|
|
40
|
+
return out;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -62,6 +62,16 @@ import {
|
|
|
62
62
|
colorsAndTypeMigration_2026_08_15_lineHeightScaleRename,
|
|
63
63
|
componentMigration_2026_08_15_lineHeightScaleRename,
|
|
64
64
|
} from './2026-08-15-line-height-scale-rename';
|
|
65
|
+
import {
|
|
66
|
+
colorsAndTypeMigration_2026_09_01_scrimRename,
|
|
67
|
+
componentMigration_2026_09_01_scrimRename,
|
|
68
|
+
} from './2026-09-01-scrim-rename';
|
|
69
|
+
import {
|
|
70
|
+
colorsAndTypeMigration_2026_09_01_tintRename,
|
|
71
|
+
componentMigration_2026_09_01_tintRename,
|
|
72
|
+
} from './2026-09-01-tint-rename';
|
|
73
|
+
import { componentMigration_2026_09_01_tabbarActiveTint } from './2026-09-01-tabbar-active-tint';
|
|
74
|
+
import { componentMigration_2026_09_01_gateSuffixEnabled } from './2026-09-01-gate-suffix-enabled';
|
|
65
75
|
|
|
66
76
|
/**
|
|
67
77
|
* Registered migrations. Order in this array does not matter — the runner
|
|
@@ -94,6 +104,12 @@ export const MIGRATIONS: Migration[] = [
|
|
|
94
104
|
colorsAndTypeMigration_2026_08_13_dropLegacyShapeSpaceKeys,
|
|
95
105
|
colorsAndTypeMigration_2026_08_15_lineHeightScaleRename,
|
|
96
106
|
componentMigration_2026_08_15_lineHeightScaleRename,
|
|
107
|
+
colorsAndTypeMigration_2026_09_01_scrimRename,
|
|
108
|
+
componentMigration_2026_09_01_scrimRename,
|
|
109
|
+
colorsAndTypeMigration_2026_09_01_tintRename,
|
|
110
|
+
componentMigration_2026_09_01_tintRename,
|
|
111
|
+
componentMigration_2026_09_01_tabbarActiveTint,
|
|
112
|
+
componentMigration_2026_09_01_gateSuffixEnabled,
|
|
97
113
|
];
|
|
98
114
|
|
|
99
115
|
function countFor(kind: 'colors-and-type' | 'component-config'): number {
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* so the store stays the single source of truth.
|
|
6
6
|
*/
|
|
7
7
|
import { COLUMN_VAR_NAMES } from './columns';
|
|
8
|
-
import {
|
|
8
|
+
import { WASH_VAR_NAMES } from './washes';
|
|
9
9
|
import { SHADOW_VAR_NAMES } from './shadows';
|
|
10
10
|
|
|
11
11
|
export const DOMAIN_VAR_NAMES: readonly string[] = [
|
|
12
12
|
...COLUMN_VAR_NAMES,
|
|
13
|
-
...
|
|
13
|
+
...WASH_VAR_NAMES,
|
|
14
14
|
...SHADOW_VAR_NAMES,
|
|
15
15
|
];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Washes slice — a wash is a translucent layer of color, stored as
|
|
3
|
+
* `{alias, opacity}` and emitted as
|
|
4
|
+
* `color-mix(in srgb, var(<alias>) <pct>%, transparent)`, so it follows its
|
|
5
|
+
* aliased source token (a brand-color shift propagates without re-editing
|
|
6
|
+
* every stop).
|
|
7
|
+
*
|
|
8
|
+
* Two families, opposite jobs. A **scrim** dims what sits behind it, which is
|
|
9
|
+
* what a modal needs. A **tint** shades the surface it sits on, which is what
|
|
10
|
+
* a hover needs. They are not interchangeable: scrims alias a near-black
|
|
11
|
+
* surface, tints alias the text color, so a tint lightens a dark theme and
|
|
12
|
+
* darkens a light one without picking a direction.
|
|
13
|
+
*
|
|
14
|
+
* Defaults diverge from tokens.css by design: the editor starts from a
|
|
15
|
+
* neutral alias and tokens.css continues to win until first edit.
|
|
16
|
+
*/
|
|
17
|
+
import type { EditorState, WashToken } from '../../store/editorTypes';
|
|
18
|
+
|
|
19
|
+
export function makeDefaultScrimTokens(): WashToken[] {
|
|
20
|
+
return [
|
|
21
|
+
{ variable: '--scrim-low', label: 'Low', alias: '--surface-neutral-lowest', opacity: 0.38 },
|
|
22
|
+
{ variable: '--scrim', label: 'Base', alias: '--surface-neutral-lowest', opacity: 0.51 },
|
|
23
|
+
{ variable: '--scrim-high', label: 'High', alias: '--surface-neutral-lowest', opacity: 0.64 },
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function makeDefaultTintTokens(): WashToken[] {
|
|
28
|
+
return [
|
|
29
|
+
{ variable: '--tint-low', label: 'Low', alias: '--text-primary', opacity: 0.05 },
|
|
30
|
+
{ variable: '--tint', label: 'Base', alias: '--text-primary', opacity: 0.1 },
|
|
31
|
+
{ variable: '--tint-high', label: 'High', alias: '--text-primary', opacity: 0.15 },
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function makeDefaultWashesState(): EditorState['washes'] {
|
|
36
|
+
return {
|
|
37
|
+
scrims: makeDefaultScrimTokens(),
|
|
38
|
+
tints: makeDefaultTintTokens(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const WASH_VAR_NAMES = [
|
|
43
|
+
'--scrim-low', '--scrim', '--scrim-high',
|
|
44
|
+
'--tint-low', '--tint', '--tint-high',
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
export function washTokenToCss(t: WashToken): string {
|
|
48
|
+
const pct = Math.round(t.opacity * 100);
|
|
49
|
+
if (pct >= 100) return `var(${t.alias})`;
|
|
50
|
+
return `color-mix(in srgb, var(${t.alias}) ${pct}%, transparent)`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const COLOR_MIX_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+)%,\s*transparent\)$/i;
|
|
54
|
+
const PLAIN_VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
|
|
55
|
+
|
|
56
|
+
export function parseWashCss(raw: string): { alias: string; opacity: number } | null {
|
|
57
|
+
const s = raw.trim();
|
|
58
|
+
const mix = s.match(COLOR_MIX_RE);
|
|
59
|
+
if (mix) {
|
|
60
|
+
const pct = parseInt(mix[2], 10);
|
|
61
|
+
if (!Number.isFinite(pct)) return null;
|
|
62
|
+
return { alias: mix[1], opacity: Math.max(0, Math.min(100, pct)) / 100 };
|
|
63
|
+
}
|
|
64
|
+
const plain = s.match(PLAIN_VAR_RE);
|
|
65
|
+
if (plain) return { alias: plain[1], opacity: 1 };
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function washesToVars(w: EditorState['washes']): Record<string, string> {
|
|
70
|
+
const out: Record<string, string> = {};
|
|
71
|
+
for (const t of w.scrims) out[t.variable] = washTokenToCss(t);
|
|
72
|
+
for (const t of w.tints) out[t.variable] = washTokenToCss(t);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function applyWashVarsToState(washes: EditorState['washes'], vars: Record<string, string>): void {
|
|
77
|
+
const applyTo = (list: WashToken[]) => {
|
|
78
|
+
for (const t of list) {
|
|
79
|
+
const raw = vars[t.variable];
|
|
80
|
+
if (!raw) continue;
|
|
81
|
+
const parsed = parseWashCss(raw);
|
|
82
|
+
if (!parsed) continue;
|
|
83
|
+
t.alias = parsed.alias;
|
|
84
|
+
t.opacity = parsed.opacity;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
applyTo(washes.scrims);
|
|
88
|
+
applyTo(washes.tints);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Loader: route wash entries from a freshly-loaded theme's vars bag into
|
|
93
|
+
* `next.washes` and remove them from the bag — but only when the value parses
|
|
94
|
+
* as the new format (color-mix / plain var). Legacy rgba values pass through to
|
|
95
|
+
* the cssVars bag so the DOM still paints them; the user's next edit in the
|
|
96
|
+
* picker promotes them to the typed slice.
|
|
97
|
+
*/
|
|
98
|
+
export function loadWashesFromVars(
|
|
99
|
+
next: EditorState,
|
|
100
|
+
rawVars: Record<string, string>,
|
|
101
|
+
): void {
|
|
102
|
+
applyWashVarsToState(next.washes, rawVars);
|
|
103
|
+
for (const name of WASH_VAR_NAMES) {
|
|
104
|
+
const raw = rawVars[name];
|
|
105
|
+
if (raw && parseWashCss(raw) !== null) delete rawVars[name];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -57,10 +57,12 @@ Numeric scales with a slider per step.
|
|
|
57
57
|
|
|
58
58
|
Change a step and every element using it repaints.
|
|
59
59
|
|
|
60
|
-
##
|
|
60
|
+
## Washes and gradients
|
|
61
61
|
|
|
62
|
-
- **
|
|
63
|
-
|
|
62
|
+
- **Scrims** are translucent layers that dim what sits behind them, like the
|
|
63
|
+
one a dialog draws over the page. Set a colour and opacity per stop.
|
|
64
|
+
- **Tints** are the opposite operation: they shade the surface they sit on
|
|
65
|
+
rather than dimming what is behind it, which is what a hover needs.
|
|
64
66
|
- **Gradients** are reusable gradient tokens with a stop list and direction, for
|
|
65
67
|
hero panels and accent backgrounds.
|
|
66
68
|
|