@motion-proto/live-tokens 0.9.0 → 0.11.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 (65) hide show
  1. package/README.md +50 -29
  2. package/dist-plugin/index.cjs +177 -125
  3. package/dist-plugin/index.d.cts +3 -2
  4. package/dist-plugin/index.d.ts +3 -2
  5. package/dist-plugin/index.js +177 -125
  6. package/package.json +4 -1
  7. package/src/editor/component-editor/BadgeEditor.svelte +44 -42
  8. package/src/editor/component-editor/ButtonEditor.svelte +224 -0
  9. package/src/editor/component-editor/CardEditor.svelte +2 -0
  10. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
  11. package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
  12. package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
  13. package/src/editor/component-editor/InputEditor.svelte +272 -0
  14. package/src/editor/component-editor/NotificationEditor.svelte +44 -65
  15. package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
  16. package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
  17. package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
  18. package/src/editor/component-editor/registry.ts +35 -2
  19. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
  20. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +10 -1
  21. package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
  22. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
  23. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
  24. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
  25. package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
  26. package/src/editor/component-editor/scaffolding/types.ts +2 -1
  27. package/src/editor/core/components/componentConfigService.ts +7 -6
  28. package/src/editor/core/manifests/manifestService.ts +5 -4
  29. package/src/editor/core/storage/apiBase.ts +15 -0
  30. package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
  31. package/src/editor/core/store/editorRenderer.ts +1 -4
  32. package/src/editor/core/store/editorStore.ts +5 -9
  33. package/src/editor/core/store/editorTypes.ts +6 -13
  34. package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -29
  35. package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
  36. package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
  37. package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
  38. package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
  39. package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
  40. package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +46 -0
  41. package/src/editor/core/themes/migrations/index.ts +16 -0
  42. package/src/editor/core/themes/slices/overlays.ts +44 -75
  43. package/src/editor/core/themes/themeInit.ts +3 -2
  44. package/src/editor/core/themes/themeService.ts +3 -2
  45. package/src/editor/ui/SurfacesTab.svelte +3 -7
  46. package/src/editor/ui/UIEasingSelector.svelte +240 -0
  47. package/src/editor/ui/UIPaddingSelector.svelte +2 -2
  48. package/src/editor/ui/UIPaletteSelector.svelte +151 -36
  49. package/src/editor/ui/{UIRelinkConfirmPopover.svelte → UIRelinkConfirmDialog.svelte} +107 -75
  50. package/src/editor/ui/UITokenSelector.svelte +15 -2
  51. package/src/editor/ui/sections/OverlaysSection.svelte +107 -540
  52. package/src/editor/ui/variantScales.ts +34 -0
  53. package/src/system/components/Button.svelte +34 -85
  54. package/src/system/components/Card.svelte +2 -1
  55. package/src/system/components/CollapsibleSection.svelte +1 -48
  56. package/src/system/components/CornerBadge.svelte +72 -138
  57. package/src/system/components/ImageLightbox.svelte +578 -0
  58. package/src/system/components/Input.svelte +387 -0
  59. package/src/system/components/ProgressBar.svelte +62 -258
  60. package/src/system/components/SegmentedControl.svelte +81 -15
  61. package/src/system/components/SideNavigation.svelte +777 -0
  62. package/src/system/components/TabBar.svelte +1 -1
  63. package/src/system/styles/tokens.css +48 -5
  64. package/src/system/styles/tokens.generated.css +33 -185
  65. package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
@@ -0,0 +1,41 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Component-config migration (2026-05-24, progressbar only): ProgressBar
5
+ * collapsed from a per-variant token namespace (primary/success/warning/
6
+ * danger/info) to a single token set. Fill color is now a runtime `fill`
7
+ * prop on the consumer side, not a variant axis.
8
+ *
9
+ * Strategy: keep the `primary` namespace's values as the canonical defaults
10
+ * (rename `--progressbar-primary-X` → `--progressbar-X`); drop the four
11
+ * remaining variant namespaces entirely. Consumers that previously selected
12
+ * a non-primary variant lose their variant-specific surface/border/fill
13
+ * customizations and fall back to the merged single set.
14
+ */
15
+ const PRIMARY_PREFIX = '--progressbar-primary-';
16
+ const NEW_PREFIX = '--progressbar-';
17
+ const DROP_PREFIXES = [
18
+ '--progressbar-success-',
19
+ '--progressbar-warning-',
20
+ '--progressbar-danger-',
21
+ '--progressbar-info-',
22
+ ];
23
+
24
+ export const componentMigration_2026_05_24_progressbarCollapseVariants: Migration = {
25
+ id: '2026-05-24-progressbar-collapse-variants',
26
+ fromVersion: 13,
27
+ toVersion: 14,
28
+ appliesTo: 'component-config',
29
+ apply(rawVars, meta) {
30
+ if (meta.component !== 'progressbar') return { ...rawVars };
31
+ const out: Record<string, string> = {};
32
+ for (const [oldKey, value] of Object.entries(rawVars)) {
33
+ if (DROP_PREFIXES.some((p) => oldKey.startsWith(p))) continue;
34
+ const key = oldKey.startsWith(PRIMARY_PREFIX)
35
+ ? NEW_PREFIX + oldKey.slice(PRIMARY_PREFIX.length)
36
+ : oldKey;
37
+ if (!(key in out)) out[key] = value;
38
+ }
39
+ return out;
40
+ },
41
+ };
@@ -0,0 +1,59 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Component-config migration (2026-05-24, button + segmentedcontrol):
5
+ *
6
+ * Promotes shape/icon-size properties that don't actually vary per state into
7
+ * their own "base" part. The editor's per-state shape rows were dead UI — the
8
+ * runtime treated them as separate tokens that were always linked anyway. The
9
+ * runtime CSS now reads the default-state token in hover/disabled rules, so
10
+ * the per-state tokens are dropped entirely.
11
+ *
12
+ * Saved per-state overrides are dropped (rare case; users re-customize once).
13
+ * The default-state value remains the authoritative one.
14
+ *
15
+ * button drops --button-{v}-{hover|disabled}-{padding|radius|border-width}
16
+ * for v in primary/secondary/outline/success/danger/warning.
17
+ *
18
+ * segmentedcontrol drops --segmentedcontrol-{selected|option-hover|disabled}-icon-size.
19
+ */
20
+
21
+ const BUTTON_VARIANTS = ['primary', 'secondary', 'outline', 'success', 'danger', 'warning'] as const;
22
+ const BUTTON_STATES = ['hover', 'disabled'] as const;
23
+ const BUTTON_PROPS = ['padding', 'radius', 'border-width'] as const;
24
+
25
+ const buttonDeadKeys = new Set<string>(
26
+ BUTTON_VARIANTS.flatMap((v) =>
27
+ BUTTON_STATES.flatMap((s) =>
28
+ BUTTON_PROPS.map((p) => `--button-${v}-${s}-${p}`),
29
+ ),
30
+ ),
31
+ );
32
+
33
+ const segmentedcontrolDeadKeys = new Set<string>([
34
+ '--segmentedcontrol-selected-icon-size',
35
+ '--segmentedcontrol-option-hover-icon-size',
36
+ '--segmentedcontrol-disabled-icon-size',
37
+ ]);
38
+
39
+ export const componentMigration_2026_05_24_promoteStateSharedTokens: Migration = {
40
+ id: '2026-05-24-promote-state-shared-tokens',
41
+ fromVersion: 12,
42
+ toVersion: 13,
43
+ appliesTo: 'component-config',
44
+ apply(rawVars, meta) {
45
+ const dead =
46
+ meta.component === 'button'
47
+ ? buttonDeadKeys
48
+ : meta.component === 'segmentedcontrol'
49
+ ? segmentedcontrolDeadKeys
50
+ : null;
51
+ if (!dead) return { ...rawVars };
52
+ const out: Record<string, string> = {};
53
+ for (const [key, value] of Object.entries(rawVars)) {
54
+ if (dead.has(key)) continue;
55
+ out[key] = value;
56
+ }
57
+ return out;
58
+ },
59
+ };
@@ -0,0 +1,29 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Component-config migration (2026-05-24, segmentedcontrol only):
5
+ * `--segmentedcontrol-divider-height` was retired in favor of
6
+ * `--segmentedcontrol-divider-inset`. The old token set a percentage / pixel
7
+ * height on a centered flex item; with `Full` (100%) it collapsed to zero in
8
+ * auto-sized inline-flex parents. The new token is `margin-block` trimmed
9
+ * from a stretched divider, so Full = 0 inset = bar-height divider.
10
+ *
11
+ * Value semantic flipped (larger old value meant taller divider; larger new
12
+ * value means shorter divider), so the saved customization is dropped rather
13
+ * than copied across. Users fall back to the new default and re-pick once.
14
+ */
15
+ export const componentMigration_2026_05_24_segmentedcontrolDividerInset: Migration = {
16
+ id: '2026-05-24-segmentedcontrol-divider-inset',
17
+ fromVersion: 11,
18
+ toVersion: 12,
19
+ appliesTo: 'component-config',
20
+ apply(rawVars, meta) {
21
+ if (meta.component !== 'segmentedcontrol') return { ...rawVars };
22
+ const out: Record<string, string> = {};
23
+ for (const [key, value] of Object.entries(rawVars)) {
24
+ if (key === '--segmentedcontrol-divider-height') continue;
25
+ out[key] = value;
26
+ }
27
+ return out;
28
+ },
29
+ };
@@ -0,0 +1,46 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * 2026-05-25: drop the per-variant token axis from cornerbadge.
5
+ *
6
+ * CornerBadge's "variants" carried only shape + spacing + type aliases — never
7
+ * colors (those come from the composed Badge). With every variant's defaults
8
+ * identical, the axis was 10× duplication that the editor had to tile across
9
+ * a variant strip for no semantic gain. Collapsing to a single flat token set
10
+ * removes the strip and the duplication.
11
+ *
12
+ * For each `--corner-badge-{variant}-{prop}`, fold into `--corner-badge-{prop}`.
13
+ * First-occurrence wins on conflict — defaults were uniform so this is a no-op
14
+ * in the typical case, and any tuned divergence is rare enough that picking
15
+ * one is better than silently averaging.
16
+ */
17
+ const VARIANTS = ['primary', 'accent', 'neutral', 'alternate', 'canvas', 'special', 'success', 'warning', 'danger', 'info'] as const;
18
+ const RE = new RegExp(`^--corner-badge-(${VARIANTS.join('|')})-(.+)$`);
19
+
20
+ function flattenVariants(rawVars: Record<string, string>, meta: { component?: string }): Record<string, string> {
21
+ if (meta.component !== 'cornerbadge') return rawVars;
22
+ const out: Record<string, string> = {};
23
+ const collected: Map<string, string> = new Map();
24
+ for (const [key, value] of Object.entries(rawVars)) {
25
+ const m = key.match(RE);
26
+ if (m) {
27
+ const prop = m[2];
28
+ if (!collected.has(prop)) collected.set(prop, value);
29
+ continue;
30
+ }
31
+ out[key] = value;
32
+ }
33
+ for (const [prop, value] of collected) {
34
+ const flatKey = `--corner-badge-${prop}`;
35
+ if (!(flatKey in out)) out[flatKey] = value;
36
+ }
37
+ return out;
38
+ }
39
+
40
+ export const componentMigration_2026_05_25_cornerbadgeFlattenVariants: Migration = {
41
+ id: '2026-05-25-cornerbadge-flatten-variants',
42
+ fromVersion: 15,
43
+ toVersion: 16,
44
+ appliesTo: 'component-config',
45
+ apply: flattenVariants,
46
+ };
@@ -0,0 +1,46 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Overlay scale trim (2026-05-26): `--overlay-lowest`, `--overlay-lower`,
5
+ * `--overlay-higher`, and `--overlay-highest` were retired. The kept stops
6
+ * are `--overlay-low`, `--overlay`, and `--overlay-high`. Theme files
7
+ * stripped of the dropped keys; component configs whose aliases referenced
8
+ * a dropped stop rebind to the nearest survivor.
9
+ */
10
+ const DROPPED_TO_KEPT: Record<string, string> = {
11
+ '--overlay-lowest': '--overlay-low',
12
+ '--overlay-lower': '--overlay-low',
13
+ '--overlay-higher': '--overlay-high',
14
+ '--overlay-highest': '--overlay-high',
15
+ };
16
+
17
+ export const themeMigration_2026_05_26_dropOverlayExtraStops: Migration = {
18
+ id: '2026-05-26-drop-overlay-extra-stops-theme',
19
+ fromVersion: 2,
20
+ toVersion: 3,
21
+ appliesTo: 'theme',
22
+ apply(rawVars) {
23
+ const out: Record<string, string> = {};
24
+ for (const [key, value] of Object.entries(rawVars)) {
25
+ if (key in DROPPED_TO_KEPT) continue;
26
+ out[key] = value;
27
+ }
28
+ return out;
29
+ },
30
+ };
31
+
32
+ export const componentMigration_2026_05_26_dropOverlayExtraStops: Migration = {
33
+ id: '2026-05-26-drop-overlay-extra-stops-component',
34
+ fromVersion: 16,
35
+ toVersion: 17,
36
+ appliesTo: 'component-config',
37
+ apply(rawVars) {
38
+ const out: Record<string, string> = {};
39
+ for (const [key, value] of Object.entries(rawVars)) {
40
+ // Alias values are bare token names like "--overlay-higher" — rebind
41
+ // those to the nearest kept stop. Literal CSS values are left alone.
42
+ out[key] = DROPPED_TO_KEPT[value] ?? value;
43
+ }
44
+ return out;
45
+ },
46
+ };
@@ -45,6 +45,15 @@ import { componentMigration_2026_05_19_sectiondividerRichGradient } from './2026
45
45
  import { componentMigration_2026_05_20_sectiondividerSlimVariants } from './2026-05-20-sectiondivider-slim-variants';
46
46
  import { componentMigration_2026_05_21_sectiondividerSpacingToPadding } from './2026-05-21-sectiondivider-spacing-to-padding';
47
47
  import { componentMigration_2026_05_22_sectiondividerIntrinsicsToCss } from './2026-05-22-sectiondivider-intrinsics-to-css';
48
+ import { componentMigration_2026_05_24_segmentedcontrolDividerInset } from './2026-05-24-segmentedcontrol-divider-inset';
49
+ import { componentMigration_2026_05_24_promoteStateSharedTokens } from './2026-05-24-promote-state-shared-tokens';
50
+ import { componentMigration_2026_05_24_progressbarCollapseVariants } from './2026-05-24-progressbar-collapse-variants';
51
+ import { componentMigration_2026_05_24_collapsiblesectionDropActiveState } from './2026-05-24-collapsiblesection-drop-active-state';
52
+ import { componentMigration_2026_05_25_cornerbadgeFlattenVariants } from './2026-05-25-cornerbadge-flatten-variants';
53
+ import {
54
+ themeMigration_2026_05_26_dropOverlayExtraStops,
55
+ componentMigration_2026_05_26_dropOverlayExtraStops,
56
+ } from './2026-05-26-drop-overlay-extra-stops';
48
57
 
49
58
  /**
50
59
  * Registered migrations. Order in this array does not matter — the runner
@@ -64,6 +73,13 @@ export const MIGRATIONS: Migration[] = [
64
73
  componentMigration_2026_05_20_sectiondividerSlimVariants,
65
74
  componentMigration_2026_05_21_sectiondividerSpacingToPadding,
66
75
  componentMigration_2026_05_22_sectiondividerIntrinsicsToCss,
76
+ componentMigration_2026_05_24_segmentedcontrolDividerInset,
77
+ componentMigration_2026_05_24_promoteStateSharedTokens,
78
+ componentMigration_2026_05_24_progressbarCollapseVariants,
79
+ componentMigration_2026_05_24_collapsiblesectionDropActiveState,
80
+ componentMigration_2026_05_25_cornerbadgeFlattenVariants,
81
+ themeMigration_2026_05_26_dropOverlayExtraStops,
82
+ componentMigration_2026_05_26_dropOverlayExtraStops,
67
83
  ];
68
84
 
69
85
  function countFor(kind: 'theme' | 'component-config'): number {
@@ -1,28 +1,28 @@
1
1
  /**
2
- * Overlays slice — overlay + hover RGBA tokens. Defaults are editor-defined
3
- * (mirror what `VariablesTab` historically initialised into local let state)
4
- * and diverge from tokens.css by design: the editor starts with a neutral
5
- * palette and tokens.css continues to win until first edit.
2
+ * Overlays slice — overlay + hover stops, each stored as `{alias, opacity}`.
3
+ * Emits as `color-mix(in srgb, var(<alias>) <pct>%, transparent)` so the
4
+ * overlay tints automatically follow the aliased source token (a brand-color
5
+ * shift propagates without re-editing every overlay).
6
+ *
7
+ * Defaults diverge from tokens.css by design: the editor starts from a
8
+ * neutral alias and tokens.css continues to win until first edit (see
9
+ * `overlaysEqualsDefault`).
6
10
  */
7
11
  import type { EditorState, OverlayToken } from '../../store/editorTypes';
8
12
 
9
13
  export function makeDefaultOverlayTokens(): OverlayToken[] {
10
14
  return [
11
- { variable: '--overlay-lowest', label: 'Lowest', r: 0, g: 0, b: 0, opacity: 0.05 },
12
- { variable: '--overlay-lower', label: 'Lower', r: 0, g: 0, b: 0, opacity: 0.1 },
13
- { variable: '--overlay-low', label: 'Low', r: 0, g: 0, b: 0, opacity: 0.2 },
14
- { variable: '--overlay', label: 'Base', r: 0, g: 0, b: 0, opacity: 0.3 },
15
- { variable: '--overlay-high', label: 'High', r: 0, g: 0, b: 0, opacity: 0.5 },
16
- { variable: '--overlay-higher', label: 'Higher', r: 0, g: 0, b: 0, opacity: 0.7 },
17
- { variable: '--overlay-highest',label: 'Highest',r: 0, g: 0, b: 0, opacity: 0.95 },
15
+ { variable: '--overlay-low', label: 'Low', alias: '--surface-neutral-lowest', opacity: 0.38 },
16
+ { variable: '--overlay', label: 'Base', alias: '--surface-neutral-lowest', opacity: 0.51 },
17
+ { variable: '--overlay-high', label: 'High', alias: '--surface-neutral-lowest', opacity: 0.64 },
18
18
  ];
19
19
  }
20
20
 
21
21
  export function makeDefaultHoverTokens(): OverlayToken[] {
22
22
  return [
23
- { variable: '--hover-low', label: 'Low', r: 255, g: 255, b: 255, opacity: 0.05 },
24
- { variable: '--hover', label: 'Base', r: 255, g: 255, b: 255, opacity: 0.1 },
25
- { variable: '--hover-high', label: 'High', r: 255, g: 255, b: 255, opacity: 0.15 },
23
+ { variable: '--hover-low', label: 'Low', alias: '--text-primary', opacity: 0.05 },
24
+ { variable: '--hover', label: 'Base', alias: '--text-primary', opacity: 0.1 },
25
+ { variable: '--hover-high', label: 'High', alias: '--text-primary', opacity: 0.15 },
26
26
  ];
27
27
  }
28
28
 
@@ -30,88 +30,52 @@ export function makeDefaultOverlaysState(): EditorState['overlays'] {
30
30
  return {
31
31
  tokens: makeDefaultOverlayTokens(),
32
32
  hoverTokens: makeDefaultHoverTokens(),
33
- globals: {
34
- overlay: { hue: 0, saturation: 0, lightness: 0, opacityMin: 0.05, opacityMax: 0.95 },
35
- hover: { hue: 0, saturation: 0, lightness: 100, opacityMin: 0.05, opacityMax: 0.15 },
36
- },
37
33
  };
38
34
  }
39
35
 
40
36
  export const OVERLAY_VAR_NAMES = [
41
- '--overlay-lowest', '--overlay-lower', '--overlay-low', '--overlay',
42
- '--overlay-high', '--overlay-higher', '--overlay-highest',
37
+ '--overlay-low', '--overlay', '--overlay-high',
43
38
  '--hover-low', '--hover', '--hover-high',
44
39
  ] as const;
45
40
 
46
- // Accepts rgb(), rgba(), and #rrggbb[aa] — themes saved by the editor
47
- // always use rgba(), but loading hand-written files shouldn't break.
48
- export const RGBA_RE = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i;
49
- export const HEX_RE = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i;
41
+ export function overlayTokenToCss(t: OverlayToken): string {
42
+ const pct = Math.round(t.opacity * 100);
43
+ if (pct >= 100) return `var(${t.alias})`;
44
+ return `color-mix(in srgb, var(${t.alias}) ${pct}%, transparent)`;
45
+ }
46
+
47
+ const COLOR_MIX_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+)%,\s*transparent\)$/i;
48
+ const PLAIN_VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
50
49
 
51
- export function parseRgba(raw: string): { r: number; g: number; b: number; opacity: number } | null {
50
+ export function parseOverlayCss(raw: string): { alias: string; opacity: number } | null {
52
51
  const s = raw.trim();
53
- const m = s.match(RGBA_RE);
54
- if (m) {
55
- const r = parseInt(m[1], 10);
56
- const g = parseInt(m[2], 10);
57
- const b = parseInt(m[3], 10);
58
- const a = m[4] !== undefined ? parseFloat(m[4]) : 1;
59
- if (![r, g, b].every((n) => Number.isFinite(n) && n >= 0 && n <= 255)) return null;
60
- return { r, g, b, opacity: Number.isFinite(a) ? a : 1 };
61
- }
62
- const h = s.match(HEX_RE);
63
- if (h) {
64
- const hex = h[1];
65
- const alpha = h[2] !== undefined ? parseInt(h[2], 16) / 255 : 1;
66
- return {
67
- r: parseInt(hex.slice(0, 2), 16),
68
- g: parseInt(hex.slice(2, 4), 16),
69
- b: parseInt(hex.slice(4, 6), 16),
70
- opacity: Math.round(alpha * 100) / 100,
71
- };
52
+ const mix = s.match(COLOR_MIX_RE);
53
+ if (mix) {
54
+ const pct = parseInt(mix[2], 10);
55
+ if (!Number.isFinite(pct)) return null;
56
+ return { alias: mix[1], opacity: Math.max(0, Math.min(100, pct)) / 100 };
72
57
  }
58
+ const plain = s.match(PLAIN_VAR_RE);
59
+ if (plain) return { alias: plain[1], opacity: 1 };
73
60
  return null;
74
61
  }
75
62
 
76
- export function overlayTokenToRgba(t: OverlayToken): string {
77
- return `rgba(${t.r}, ${t.g}, ${t.b}, ${t.opacity})`;
78
- }
79
-
80
63
  export function overlaysToVars(o: EditorState['overlays']): Record<string, string> {
81
64
  const out: Record<string, string> = {};
82
- for (const t of o.tokens) out[t.variable] = overlayTokenToRgba(t);
83
- for (const t of o.hoverTokens) out[t.variable] = overlayTokenToRgba(t);
65
+ for (const t of o.tokens) out[t.variable] = overlayTokenToCss(t);
66
+ for (const t of o.hoverTokens) out[t.variable] = overlayTokenToCss(t);
84
67
  return out;
85
68
  }
86
69
 
87
- function tokensEqualDefault(tokens: OverlayToken[], defaults: OverlayToken[]): boolean {
88
- if (tokens.length !== defaults.length) return false;
89
- for (let i = 0; i < tokens.length; i++) {
90
- const a = tokens[i]; const b = defaults[i];
91
- if (a.variable !== b.variable || a.r !== b.r || a.g !== b.g || a.b !== b.b || a.opacity !== b.opacity) return false;
92
- }
93
- return true;
94
- }
95
-
96
- /**
97
- * Same pattern as columns: only emit overlay CSS vars once state diverges
98
- * from the editor defaults. tokens.css owns the rgba values until the
99
- * user touches any overlay control (or loads a theme that already
100
- * contains overrides).
101
- */
102
- export function overlaysEqualsDefault(o: EditorState['overlays']): boolean {
103
- return tokensEqualDefault(o.tokens, makeDefaultOverlayTokens())
104
- && tokensEqualDefault(o.hoverTokens, makeDefaultHoverTokens());
105
- }
106
-
107
70
  export function applyOverlayVarsToState(overlays: EditorState['overlays'], vars: Record<string, string>): void {
108
71
  const applyTo = (list: OverlayToken[]) => {
109
72
  for (const t of list) {
110
73
  const raw = vars[t.variable];
111
74
  if (!raw) continue;
112
- const parsed = parseRgba(raw);
75
+ const parsed = parseOverlayCss(raw);
113
76
  if (!parsed) continue;
114
- t.r = parsed.r; t.g = parsed.g; t.b = parsed.b; t.opacity = parsed.opacity;
77
+ t.alias = parsed.alias;
78
+ t.opacity = parsed.opacity;
115
79
  }
116
80
  };
117
81
  applyTo(overlays.tokens);
@@ -120,13 +84,18 @@ export function applyOverlayVarsToState(overlays: EditorState['overlays'], vars:
120
84
 
121
85
  /**
122
86
  * Loader: route overlay/hover entries from a freshly-loaded theme's vars
123
- * bag into `next.overlays` and remove them from the bag. Mutates `next`
124
- * and `rawVars` in place.
87
+ * bag into `next.overlays` and remove them from the bag but only when the
88
+ * value parses as the new format (color-mix / plain var). Legacy rgba values
89
+ * pass through to the cssVars bag so the DOM still paints them; the user's
90
+ * next edit in the picker promotes them to the typed slice.
125
91
  */
126
92
  export function loadOverlaysFromVars(
127
93
  next: EditorState,
128
94
  rawVars: Record<string, string>,
129
95
  ): void {
130
96
  applyOverlayVarsToState(next.overlays, rawVars);
131
- for (const name of OVERLAY_VAR_NAMES) delete rawVars[name];
97
+ for (const name of OVERLAY_VAR_NAMES) {
98
+ const raw = rawVars[name];
99
+ if (raw && parseOverlayCss(raw) !== null) delete rawVars[name];
100
+ }
132
101
  }
@@ -5,6 +5,7 @@ import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
5
5
  import { loadFromFile, seedComponentsFromApi } from '../store/editorStore';
6
6
  import { getActiveComponentConfig } from '../components/componentConfigService';
7
7
  import { safeFetch } from '../storage/storage';
8
+ import { API_BASE } from '../storage/apiBase';
8
9
 
9
10
  interface ComponentSummaryDto {
10
11
  name: string;
@@ -34,7 +35,7 @@ interface ListComponentsDto {
34
35
  * `safeFetch` (instead of empty try/catch) to make the silence intentional.
35
36
  */
36
37
  export async function initializeTheme(): Promise<void> {
37
- const theme = await safeFetch<Theme>('/api/themes/active');
38
+ const theme = await safeFetch<Theme>(`${API_BASE}/themes/active`);
38
39
  if (theme) {
39
40
  migrateThemeFonts(theme);
40
41
  loadFromFile(theme);
@@ -48,7 +49,7 @@ export async function initializeTheme(): Promise<void> {
48
49
  activeFileName.set(fileName);
49
50
  }
50
51
 
51
- const list = await safeFetch<ListComponentsDto>('/api/component-configs');
52
+ const list = await safeFetch<ListComponentsDto>(`${API_BASE}/component-configs`);
52
53
  if (list && Array.isArray(list.components)) {
53
54
  const configs: Record<
54
55
  string,
@@ -5,6 +5,7 @@ import {
5
5
  versionedFileResource,
6
6
  sanitizeFileName as sanitizeFileNameImpl,
7
7
  } from '../storage/files/versionedFileResourceClient';
8
+ import { API_BASE } from '../storage/apiBase';
8
9
  import { loadFromFile as loadEditorState, toTheme, markSaved } from '../store/editorStore';
9
10
  import { activeFileName } from '../store/editorConfigStore';
10
11
  import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
@@ -12,7 +13,7 @@ import { migrateThemeFonts } from '../fonts/fontMigration';
12
13
 
13
14
  // ── API helpers ──────────────────────────────────────────────
14
15
  //
15
- // All theme CRUD goes through `versionedFileResource('/api/themes')` —
16
+ // All theme CRUD goes through `versionedFileResource(`${API_BASE}/themes`)` —
16
17
  // shared with `componentConfigService`'s per-component clients. Theme-specific
17
18
  // response shapes (ThemeMeta list payload, ProductionInfo) are layered on top
18
19
  // via the generic type parameters.
@@ -25,7 +26,7 @@ export interface ProductionInfo {
25
26
  }
26
27
 
27
28
  const themeResource = versionedFileResource<Theme, ThemeMeta, ProductionInfo>({
28
- baseUrl: '/api/themes',
29
+ baseUrl: `${API_BASE}/themes`,
29
30
  });
30
31
 
31
32
  export async function listThemes(): Promise<ThemeMeta[]> {
@@ -31,13 +31,9 @@
31
31
  {
32
32
  title: 'Overlays',
33
33
  swatches: [
34
- { name: 'Lowest', variable: '--overlay-lowest', description: 'Barely visible tint' },
35
- { name: 'Lower', variable: '--overlay-lower', description: 'Very subtle overlay' },
36
- { name: 'Low', variable: '--overlay-low', description: 'Light overlay' },
37
- { name: 'Overlay', variable: '--overlay', description: 'Standard overlay' },
38
- { name: 'High', variable: '--overlay-high', description: 'Heavy overlay' },
39
- { name: 'Higher', variable: '--overlay-higher', description: 'Modal backdrop' },
40
- { name: 'Highest', variable: '--overlay-highest', description: 'Nearly opaque' }
34
+ { name: 'Low', variable: '--overlay-low', description: 'Light backdrop / pressed wash' },
35
+ { name: 'Overlay', variable: '--overlay', description: 'Standard backdrop' },
36
+ { name: 'High', variable: '--overlay-high', description: 'Strong backdrop' }
41
37
  ]
42
38
  },
43
39
  {