@motion-proto/live-tokens 0.67.1 → 0.69.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.
Files changed (138) hide show
  1. package/.claude/skills/live-tokens-build-page/SKILL.md +16 -5
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +43 -8
  3. package/.claude/skills/live-tokens-create-component/references/token-naming.md +30 -1
  4. package/.claude/skills/live-tokens-fix-findings/SKILL.md +131 -0
  5. package/.claude/skills/live-tokens-pick-component/SKILL.md +2 -1
  6. package/CHANGELOG.md +239 -0
  7. package/README.md +11 -4
  8. package/bin/check-component.mjs +367 -63
  9. package/bin/check-page.mjs +409 -0
  10. package/bin/cli.mjs +57 -8
  11. package/bin/generate-theme.mjs +5 -4
  12. package/bin/lib/cssValues.mjs +50 -0
  13. package/bin/lib/findings.mjs +106 -0
  14. package/bin/lib/tokenVocabulary.mjs +213 -0
  15. package/dist-plugin/adjust/index.cjs +174 -23
  16. package/dist-plugin/adjust/index.d.cts +1 -1
  17. package/dist-plugin/adjust/index.d.ts +1 -1
  18. package/dist-plugin/adjust/index.js +68 -23
  19. package/dist-plugin/{chunk-J2JT4UEA.js → chunk-2YNERPXY.js} +26 -26
  20. package/dist-plugin/{chunk-NE6N66EE.js → chunk-GPIBU44G.js} +107 -1
  21. package/dist-plugin/{chunk-ZHPX7ZYQ.js → chunk-RFVYPNRO.js} +39 -1
  22. package/dist-plugin/{dataPaths-DZUzVv8H.d.ts → dataPaths-bJTCEO4H.d.cts} +1 -1
  23. package/dist-plugin/{dataPaths-DZUzVv8H.d.cts → dataPaths-bJTCEO4H.d.ts} +1 -1
  24. package/dist-plugin/fontPairing/index.d.cts +1 -1
  25. package/dist-plugin/fontPairing/index.d.ts +1 -1
  26. package/dist-plugin/generateColorsAndType/index.cjs +107 -1
  27. package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
  28. package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
  29. package/dist-plugin/generateColorsAndType/index.js +1 -1
  30. package/dist-plugin/index.cjs +173 -29
  31. package/dist-plugin/index.d.cts +1 -1
  32. package/dist-plugin/index.d.ts +1 -1
  33. package/dist-plugin/index.js +7 -7
  34. package/dist-plugin/migrateData/index.cjs +132 -26
  35. package/dist-plugin/migrateData/index.d.cts +1 -1
  36. package/dist-plugin/migrateData/index.d.ts +1 -1
  37. package/dist-plugin/migrateData/index.js +2 -2
  38. package/dist-plugin/tokensCssMigrations/index.cjs +39 -1
  39. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  40. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  41. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  42. package/package.json +6 -5
  43. package/src/app/site.css +4 -4
  44. package/src/editor/bootstrap.ts +6 -6
  45. package/src/editor/component-editor/ButtonEditor.svelte +71 -4
  46. package/src/editor/component-editor/CardEditor.svelte +6 -6
  47. package/src/editor/component-editor/DialogEditor.svelte +3 -3
  48. package/src/editor/component-editor/IconButtonEditor.svelte +68 -3
  49. package/src/editor/component-editor/ImageEditor.svelte +8 -8
  50. package/src/editor/component-editor/ImageLightboxEditor.svelte +13 -2
  51. package/src/editor/component-editor/MenuSelectEditor.svelte +63 -3
  52. package/src/editor/component-editor/SegmentedControlEditor.svelte +63 -3
  53. package/src/editor/component-editor/SideNavigationEditor.svelte +67 -3
  54. package/src/editor/component-editor/SliderEditor.svelte +186 -0
  55. package/src/editor/component-editor/TabBarEditor.svelte +63 -3
  56. package/src/editor/component-editor/registry.ts +10 -0
  57. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +4 -4
  58. package/src/editor/core/components/aliasKinds.ts +51 -28
  59. package/src/editor/core/fonts/fontLoader.ts +2 -2
  60. package/src/editor/core/preview/{lookPreview.ts → themePreview.ts} +27 -27
  61. package/src/editor/core/productionPulse.ts +3 -3
  62. package/src/editor/core/sketch/index.ts +52 -44
  63. package/src/editor/core/sketch/maskField.ts +9 -9
  64. package/src/editor/core/sketch/sketchLayer.ts +25 -9
  65. package/src/editor/core/sketch/sketchRegistry.ts +34 -34
  66. package/src/editor/core/sketch/sketchStore.ts +98 -83
  67. package/src/editor/core/sketch/sketchStyleService.ts +4 -4
  68. package/src/editor/core/sketch/sketchStyles.ts +28 -28
  69. package/src/editor/core/store/editorPersistence.ts +44 -1
  70. package/src/editor/core/store/editorRenderer.ts +2 -2
  71. package/src/editor/core/store/editorStore.ts +18 -18
  72. package/src/editor/core/store/editorTypes.ts +7 -6
  73. package/src/editor/core/themes/colorsAndTypeService.ts +1 -1
  74. package/src/editor/core/themes/loadRows.ts +8 -8
  75. package/src/editor/core/themes/migrations/2026-09-01-gate-suffix-enabled.ts +31 -0
  76. package/src/editor/core/themes/migrations/2026-09-01-scrim-rename.ts +52 -0
  77. package/src/editor/core/themes/migrations/2026-09-01-tabbar-active-tint.ts +27 -0
  78. package/src/editor/core/themes/migrations/2026-09-01-tint-rename.ts +42 -0
  79. package/src/editor/core/themes/migrations/index.ts +16 -0
  80. package/src/editor/core/themes/slices/domainVars.ts +2 -2
  81. package/src/editor/core/themes/slices/washes.ts +107 -0
  82. package/src/editor/core/themes/themeDocumentSync.ts +2 -2
  83. package/src/editor/core/themes/themeInit.ts +2 -2
  84. package/src/editor/core/themes/themeService.ts +17 -17
  85. package/src/editor/core/themes/{lookSummary.ts → themeSummary.ts} +9 -9
  86. package/src/editor/core/themes/themeTypes.ts +10 -6
  87. package/src/editor/docs/content/editing-tokens.md +5 -3
  88. package/src/editor/docs/content/getting-started.md +1 -1
  89. package/src/editor/docs/content/sketch-mode.md +22 -17
  90. package/src/editor/docs/content/themes-workflow.md +11 -11
  91. package/src/editor/docs/content/where-themes-live.md +6 -6
  92. package/src/editor/docs/content.generated.ts +5 -5
  93. package/src/editor/index.ts +2 -2
  94. package/src/editor/pages/EditorShell.svelte +1 -1
  95. package/src/editor/ui/SurfacesTab.svelte +3 -3
  96. package/src/editor/ui/ThemePanel.svelte +52 -52
  97. package/src/editor/ui/UITokenSelector.svelte +1 -0
  98. package/src/editor/ui/VariablesTab.svelte +2 -2
  99. package/src/editor/ui/sections/{OverlaysSection.svelte → WashesSection.svelte} +44 -43
  100. package/src/editor/ui/sketch/SketchPreview.svelte +2 -2
  101. package/src/editor/ui/sketch/SketchTab.svelte +64 -38
  102. package/src/live-tokens/data/colors-and-type/autumn.json +6 -6
  103. package/src/live-tokens/data/colors-and-type/default.json +6 -6
  104. package/src/live-tokens/data/colors-and-type/halloween.json +6 -6
  105. package/src/live-tokens/data/colors-and-type/midnight-study.json +6 -6
  106. package/src/live-tokens/data/colors-and-type/ocean.json +6 -6
  107. package/src/live-tokens/data/colors-and-type/royal-velvet.json +6 -6
  108. package/src/live-tokens/data/colors-and-type/sketchy.json +6 -6
  109. package/src/live-tokens/data/colors-and-type/spring-meadow.json +6 -6
  110. package/src/live-tokens/data/colors-and-type/sunset.json +6 -6
  111. package/src/live-tokens/data/sketch-styles/dry.json +5 -5
  112. package/src/live-tokens/data/sketch-styles/hatched.json +9 -9
  113. package/src/live-tokens/data/themes/autumn.json +82 -14
  114. package/src/live-tokens/data/themes/halloween.json +82 -14
  115. package/src/live-tokens/data/themes/midnight-study.json +82 -14
  116. package/src/live-tokens/data/themes/ocean.json +82 -14
  117. package/src/live-tokens/data/themes/royal-velvet.json +82 -14
  118. package/src/live-tokens/data/themes/sketchy.json +82 -14
  119. package/src/live-tokens/data/themes/spring-meadow.json +82 -14
  120. package/src/live-tokens/data/themes/sunset.json +82 -14
  121. package/src/live-tokens/data/tokens.generated.css +6 -6
  122. package/src/system/backdrop/backdrop.ts +1 -1
  123. package/src/system/components/Button.svelte +28 -10
  124. package/src/system/components/Card.svelte +6 -6
  125. package/src/system/components/Dialog.svelte +3 -3
  126. package/src/system/components/IconButton.svelte +23 -7
  127. package/src/system/components/Image.svelte +6 -6
  128. package/src/system/components/MenuSelect.svelte +17 -1
  129. package/src/system/components/SectionDivider.svelte +1 -1
  130. package/src/system/components/SegmentedControl.svelte +20 -2
  131. package/src/system/components/SideNavigation.svelte +22 -2
  132. package/src/system/components/Slider.svelte +348 -0
  133. package/src/system/components/TabBar.svelte +20 -2
  134. package/src/system/styles/CONVENTIONS.md +2 -2
  135. package/src/system/styles/tokens.css +12 -4
  136. package/template/package.json +3 -2
  137. package/template/src/pages/Home.svelte +1 -1
  138. package/src/editor/core/themes/slices/overlays.ts +0 -101
@@ -6,7 +6,7 @@ import dashed from '../../../live-tokens/data/sketch-styles/dashed.json';
6
6
  import napkin from '../../../live-tokens/data/sketch-styles/napkin.json';
7
7
  import dry from '../../../live-tokens/data/sketch-styles/dry.json';
8
8
 
9
- export interface SketchStyle {
9
+ export interface SketchStyleSettings {
10
10
  label: string;
11
11
  blurb: string;
12
12
  /** How far the fill's edge travels at its furthest, in px. Every dial that
@@ -80,7 +80,7 @@ export interface SketchStyle {
80
80
  /** Whether the two move together. Unlinked they part, and the field comes out
81
81
  stretched: blobs wider than they are tall read as a wash dragged sideways,
82
82
  the way ink pulled across a page does. Stored rather than inferred from
83
- the pair matching, so a look that stretches to exactly square keeps its
83
+ the pair matching, so a sketchstyle that stretches to exactly square keeps its
84
84
  dials apart. */
85
85
  maskBlobLinked: boolean;
86
86
  /** Output levels on the coverage field, 0 to 1: the palest the fill gets and
@@ -144,40 +144,40 @@ export interface SketchStyle {
144
144
  /**
145
145
  * The shipped sketchstyles, read from the files the package distributes. The
146
146
  * files are the source: a project shadows one by saving a sketchstyle under the
147
- * same id, the editor restores a shipped look by deleting that file, and
147
+ * same id, the editor restores a shipped sketchstyle by deleting that file, and
148
148
  * `themeFileApi` serves these as the read-only fallback behind the project's own
149
- * directory. Editing a look here means editing its JSON, which is what the
149
+ * directory. Editing a sketchstyle here means editing its JSON, which is what the
150
150
  * Sketchstyle view already writes.
151
151
  *
152
152
  * Each file carries every dial, so there is nothing to merge a default into.
153
153
  * `sketchStyles.test.ts` pins that: the seven key sets have to match, and a dial
154
- * added to `SketchStyle` has to reach all seven before the suite goes green.
154
+ * added to `SketchStyleSettings` has to reach all seven before the suite goes green.
155
155
  *
156
156
  * Order is picker order.
157
157
  */
158
158
  const SHIPPED_FILES = { pencil, marker, whiteboard, hatched, dashed, napkin, dry };
159
159
 
160
- export const SKETCH_STYLES: Record<string, SketchStyle> = Object.fromEntries(
161
- Object.entries(SHIPPED_FILES).map(([id, file]) => [id, file.settings as unknown as SketchStyle]),
160
+ export const SHIPPED_SKETCH_SETTINGS: Record<string, SketchStyleSettings> = Object.fromEntries(
161
+ Object.entries(SHIPPED_FILES).map(([id, file]) => [id, file.settings as unknown as SketchStyleSettings]),
162
162
  );
163
163
 
164
164
  export const DEFAULT_SKETCH_STYLE = 'marker';
165
165
 
166
- /** The id of the look a theme carries, in the same id namespace as the shipped
166
+ /** The id for sketch settings a theme carries, in the same id namespace as the shipped
167
167
  sketchstyles so one picker row and one `setSketch` call cover both. Never a
168
- key of `SKETCH_STYLES`: a shipped style claiming it would shadow the theme's
169
- own look in every picker. `index.test.ts` pins that. */
168
+ key of `SHIPPED_SKETCH_SETTINGS`: a shipped style claiming it would shadow the theme's
169
+ own sketchstyle in every picker. `index.test.ts` pins that. */
170
170
  export const THEME_SKETCH_ID = 'theme';
171
171
 
172
172
  /** Reconciled against a full sketchstyle in both directions: a value stored before a
173
173
  control existed picks up the default, and a value stored for a control since
174
174
  retired is dropped. Without the drop, a stale key survives every spread and
175
175
  makes the settings compare unequal to any baseline forever. */
176
- export function hydrateSketchStyle(raw: unknown): SketchStyle {
177
- const base = SKETCH_STYLES[DEFAULT_SKETCH_STYLE];
178
- const stored = (raw ?? {}) as Partial<SketchStyle>;
176
+ export function hydrateSketchSettings(raw: unknown): SketchStyleSettings {
177
+ const base = SHIPPED_SKETCH_SETTINGS[DEFAULT_SKETCH_STYLE];
178
+ const stored = (raw ?? {}) as Partial<SketchStyleSettings>;
179
179
  const out = { ...base };
180
- for (const key of Object.keys(base) as (keyof SketchStyle)[]) {
180
+ for (const key of Object.keys(base) as (keyof SketchStyleSettings)[]) {
181
181
  if (stored[key] !== undefined) (out[key] as unknown) = stored[key];
182
182
  }
183
183
  // A retired option: the fill's presence belongs to the theme, not the effect.
@@ -194,9 +194,9 @@ export function hydrateSketchStyle(raw: unknown): SketchStyle {
194
194
  }
195
195
 
196
196
  /** The second pass used to sit at a distance derived from the stroke width,
197
- with no dial of its own. A look stored before the dial comes back at that
197
+ with no dial of its own. A sketchstyle stored before the dial comes back at that
198
198
  distance rather than at whatever the fallback sketchstyle happens to carry. */
199
- function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchStyle): void {
199
+ function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchStyleSettings): void {
200
200
  if (stored.retraceOffset === undefined) {
201
201
  out.retraceOffset = Number(Math.max(1.2, out.strokeWidth * 0.55).toFixed(2));
202
202
  }
@@ -204,7 +204,7 @@ function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchStyle
204
204
 
205
205
  /** The four displacement dials used to be stated as the map's own `scale`,
206
206
  which is the full swing: the number on the dial was twice the furthest
207
- anything actually moved. They are peak travel in px now, so a look stored
207
+ anything actually moved. They are peak travel in px now, so a sketchstyle stored
208
208
  under the old names comes back halved and renders identically. */
209
209
  const SWING_DIALS = {
210
210
  fillScale: 'fillTravel',
@@ -216,11 +216,11 @@ const SWING_DIALS = {
216
216
  /** The pen wobble used to be stated as `frequency`, in cycles per px, which is
217
217
  the number the filter wants and not one anybody can picture. It is a
218
218
  wavelength in px now, the way the mask states its blobs. */
219
- function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchStyle): void {
219
+ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchStyleSettings): void {
220
220
  const cycles = stored.frequency;
221
221
  if (typeof cycles === 'number' && cycles > 0) out.wobble = Math.round(1 / cycles);
222
222
  // The layer count was `octaves`, and its dial ran to 5. The top two moved
223
- // nothing, so a look stored there comes back at the roughest that reads.
223
+ // nothing, so a sketchstyle stored there comes back at the roughest that reads.
224
224
  // Both were multipliers on the frequency, so they ran the other way.
225
225
  for (const [legacy, key] of [
226
226
  ['borderFrequency', 'borderWavelength'], ['iconFrequency', 'iconWavelength'],
@@ -234,9 +234,9 @@ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchS
234
234
  if (typeof layers === 'number') out.roughness = Math.min(3, Math.max(1, layers));
235
235
  }
236
236
 
237
- /** The blob size was one number for both axes. A look stored before the split
238
- comes back square, with the two dials linked, which is the look it had. */
239
- function splitBlobAxes(stored: Record<string, unknown>, out: SketchStyle): void {
237
+ /** The blob size was one number for both axes. A sketchstyle stored before the split
238
+ comes back square, with the two dials linked, which is the shape it had. */
239
+ function splitBlobAxes(stored: Record<string, unknown>, out: SketchStyleSettings): void {
240
240
  const blob = stored.maskBlob;
241
241
  if (typeof blob !== 'number') return;
242
242
  out.maskBlobX = blob;
@@ -249,13 +249,13 @@ function splitBlobAxes(stored: Record<string, unknown>, out: SketchStyle): void
249
249
  of the field, so it came out either untouched or gone. It is a share of the
250
250
  glyph now, and the old default reads as one period across the glyph, which
251
251
  is what that size was aiming at. */
252
- function convertIconTileToScale(stored: Record<string, unknown>, out: SketchStyle): void {
252
+ function convertIconTileToScale(stored: Record<string, unknown>, out: SketchStyleSettings): void {
253
253
  const tile = stored.iconMaskTile;
254
254
  if (typeof tile !== 'number') return;
255
255
  out.iconMaskScale = Math.min(5, Math.max(0.5, Number((tile / 90).toFixed(2))));
256
256
  }
257
257
 
258
- function halveSwingDials(stored: Record<string, unknown>, out: SketchStyle): void {
258
+ function halveSwingDials(stored: Record<string, unknown>, out: SketchStyleSettings): void {
259
259
  for (const [legacy, key] of Object.entries(SWING_DIALS)) {
260
260
  const value = stored[legacy];
261
261
  if (typeof value === 'number') out[key as 'fillTravel'] = value / 2;
@@ -265,7 +265,7 @@ function halveSwingDials(stored: Record<string, unknown>, out: SketchStyle): voi
265
265
  /** The mask used to be a 600-unit tile painted at `maskScale` px, with the
266
266
  coverage point buried in the hardness slope. Recover page-px blobs and
267
267
  softness, and the levels the old slope and floor put the edge at. */
268
- function convertTiledMask(stored: Record<string, unknown>, out: SketchStyle): void {
268
+ function convertTiledMask(stored: Record<string, unknown>, out: SketchStyleSettings): void {
269
269
  const scale = stored.maskScale;
270
270
  if (typeof scale !== 'number') return;
271
271
  const freq = stored.maskFrequency;
@@ -283,7 +283,7 @@ function convertTiledMask(stored: Record<string, unknown>, out: SketchStyle): vo
283
283
  * levels the pair put the edge between, and rescale them onto the field as it
284
284
  * is now: stretched onto its full range before the levels see it.
285
285
  */
286
- function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyle): void {
286
+ function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyleSettings): void {
287
287
  const contrast = stored.maskContrast;
288
288
  const coverage = stored.maskCoverage;
289
289
  if (typeof contrast !== 'number' || typeof coverage !== 'number') return;
@@ -297,9 +297,9 @@ function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyle):
297
297
 
298
298
  /** The pair were input levels, a cut through the field, and are output levels
299
299
  now, the palest and densest the fill gets. The same two numbers carry over:
300
- a look cut between 20% and 50% comes back as a wash between 20% and 50% ink,
300
+ a sketchstyle cut between 20% and 50% comes back as a wash between 20% and 50% ink,
301
301
  which keeps its spread and loses only the hole. */
302
- function carryLevelsToOutput(stored: Record<string, unknown>, out: SketchStyle): void {
302
+ function carryLevelsToOutput(stored: Record<string, unknown>, out: SketchStyleSettings): void {
303
303
  if (typeof stored.maskLevelMin === 'number') out.maskOutputMin = stored.maskLevelMin;
304
304
  if (typeof stored.maskLevelMax === 'number') out.maskOutputMax = stored.maskLevelMax;
305
305
  }
@@ -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
- overlaysToVars,
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, overlaysToVars(state.overlays));
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, overlays, columns, ad-hoc CSS
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
- loadOverlaysFromVars,
66
- makeDefaultOverlaysState,
67
- overlaysToVars,
68
- } from '../themes/slices/overlays';
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
- overlays: makeDefaultOverlaysState(),
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
- overlaysToVars,
154
- OVERLAY_VAR_NAMES,
155
- applyOverlayVarsToState,
156
- makeDefaultOverlaysState,
157
- makeDefaultOverlayTokens,
158
- makeDefaultHoverTokens,
159
- overlayTokenToCss,
160
- parseOverlayCss,
161
- } from '../themes/slices/overlays';
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
- overlays: loadOverlaysFromVars,
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, overlaysToVars(state.overlays));
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, overlays, shadows) fold derived vars into `cssVariables` only
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
- /** Overlay stop: an aliased color token + an opacity. Emits as
27
- * `color-mix(in srgb, var(<alias>) <opacity%>, transparent)`. */
28
- export interface OverlayToken {
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
- overlays: {
115
- tokens: OverlayToken[];
116
- hoverTokens: OverlayToken[];
115
+ washes: {
116
+ scrims: WashToken[];
117
+ tints: WashToken[];
117
118
  };
118
119
  columns: ColumnsState;
119
120
  components: Record<string, ComponentSlice>;
@@ -65,7 +65,7 @@ export const sanitizeFileName = sanitizeFileNameImpl;
65
65
 
66
66
  /** Flush the editor state to the buffer under `displayName`, the name of the
67
67
  * theme the buffer belongs to, and clear the dirty flag. A capture reads the
68
- * buffer back, so this is what makes Save and Adopt mean the look on screen. */
68
+ * buffer back, so this is what makes Save and Adopt mean the theme on screen. */
69
69
  export async function persistColorsAndType(
70
70
  state: EditorState,
71
71
  displayName: string,
@@ -1,7 +1,7 @@
1
1
  import type { ThemeMeta, ColorsAndTypeMeta } from './themeTypes';
2
2
 
3
3
  /**
4
- * The one Load list. A theme is the whole look; a colors and type file is a
4
+ * The one Load list. A theme is the whole theme; a colors and type file is a
5
5
  * preset holding that half of one. Both belong in the same window, told apart
6
6
  * by a badge rather than by living in separate managers.
7
7
  *
@@ -10,7 +10,7 @@ import type { ThemeMeta, ColorsAndTypeMeta } from './themeTypes';
10
10
  * that copy is exactly the file the list must keep reachable.
11
11
  */
12
12
 
13
- export type LoadRowKind = 'look' | 'layer';
13
+ export type LoadRowKind = 'theme' | 'layer';
14
14
 
15
15
  export interface LoadRow {
16
16
  /** `<kind>:<slug>`. The two kinds are separate resources with separate name
@@ -25,12 +25,12 @@ export interface LoadRow {
25
25
 
26
26
  export const loadRowId = (kind: LoadRowKind, slug: string): string => `${kind}:${slug}`;
27
27
 
28
- export function buildLoadRows(looks: ThemeMeta[], layers: ColorsAndTypeMeta[]): LoadRow[] {
29
- const lookRows: LoadRow[] = looks
28
+ export function buildLoadRows(themes: ThemeMeta[], layers: ColorsAndTypeMeta[]): LoadRow[] {
29
+ const themeRows: LoadRow[] = themes
30
30
  .map((f): LoadRow => ({
31
- fileName: loadRowId('look', f.fileName),
31
+ fileName: loadRowId('theme', f.fileName),
32
32
  slug: f.fileName,
33
- kind: 'look',
33
+ kind: 'theme',
34
34
  name: f.name,
35
35
  updatedAt: f.updatedAt,
36
36
  isProtected: f.isProtected,
@@ -46,12 +46,12 @@ export function buildLoadRows(looks: ThemeMeta[], layers: ColorsAndTypeMeta[]):
46
46
  updatedAt: f.updatedAt,
47
47
  isProtected: false,
48
48
  }));
49
- return [...lookRows, ...layerRows];
49
+ return [...themeRows, ...layerRows];
50
50
  }
51
51
 
52
52
  /**
53
53
  * Whether picking this row loads colors and type alone. A layer file holds
54
- * nothing else, so it ignores the toggle; a look honors it.
54
+ * nothing else, so it ignores the toggle; a theme honors it.
55
55
  */
56
56
  export function isColorsOnly(row: LoadRow | null, colorsOnly: boolean): boolean {
57
57
  if (row?.kind === 'layer') return true;
@@ -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 { OVERLAY_VAR_NAMES } from './overlays';
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
- ...OVERLAY_VAR_NAMES,
13
+ ...WASH_VAR_NAMES,
14
14
  ...SHADOW_VAR_NAMES,
15
15
  ];