@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.
- package/README.md +50 -29
- package/dist-plugin/index.cjs +177 -125
- package/dist-plugin/index.d.cts +3 -2
- package/dist-plugin/index.d.ts +3 -2
- package/dist-plugin/index.js +177 -125
- package/package.json +4 -1
- package/src/editor/component-editor/BadgeEditor.svelte +44 -42
- package/src/editor/component-editor/ButtonEditor.svelte +224 -0
- package/src/editor/component-editor/CardEditor.svelte +2 -0
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
- package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
- package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
- package/src/editor/component-editor/InputEditor.svelte +272 -0
- package/src/editor/component-editor/NotificationEditor.svelte +44 -65
- package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
- package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
- package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
- package/src/editor/component-editor/registry.ts +35 -2
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +10 -1
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
- package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
- package/src/editor/component-editor/scaffolding/types.ts +2 -1
- package/src/editor/core/components/componentConfigService.ts +7 -6
- package/src/editor/core/manifests/manifestService.ts +5 -4
- package/src/editor/core/storage/apiBase.ts +15 -0
- package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
- package/src/editor/core/store/editorRenderer.ts +1 -4
- package/src/editor/core/store/editorStore.ts +5 -9
- package/src/editor/core/store/editorTypes.ts +6 -13
- package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -29
- package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
- package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
- package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
- package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +46 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/slices/overlays.ts +44 -75
- package/src/editor/core/themes/themeInit.ts +3 -2
- package/src/editor/core/themes/themeService.ts +3 -2
- package/src/editor/ui/SurfacesTab.svelte +3 -7
- package/src/editor/ui/UIEasingSelector.svelte +240 -0
- package/src/editor/ui/UIPaddingSelector.svelte +2 -2
- package/src/editor/ui/UIPaletteSelector.svelte +151 -36
- package/src/editor/ui/{UIRelinkConfirmPopover.svelte → UIRelinkConfirmDialog.svelte} +107 -75
- package/src/editor/ui/UITokenSelector.svelte +15 -2
- package/src/editor/ui/sections/OverlaysSection.svelte +107 -540
- package/src/editor/ui/variantScales.ts +34 -0
- package/src/system/components/Button.svelte +34 -85
- package/src/system/components/Card.svelte +2 -1
- package/src/system/components/CollapsibleSection.svelte +1 -48
- package/src/system/components/CornerBadge.svelte +72 -138
- package/src/system/components/ImageLightbox.svelte +578 -0
- package/src/system/components/Input.svelte +387 -0
- package/src/system/components/ProgressBar.svelte +62 -258
- package/src/system/components/SegmentedControl.svelte +81 -15
- package/src/system/components/SideNavigation.svelte +777 -0
- package/src/system/components/TabBar.svelte +1 -1
- package/src/system/styles/tokens.css +48 -5
- package/src/system/styles/tokens.generated.css +33 -185
- 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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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-
|
|
12
|
-
{ variable: '--overlay
|
|
13
|
-
{ variable: '--overlay-
|
|
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',
|
|
24
|
-
{ variable: '--hover', label: 'Base',
|
|
25
|
-
{ variable: '--hover-high', label: 'High',
|
|
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-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
50
|
+
export function parseOverlayCss(raw: string): { alias: string; opacity: number } | null {
|
|
52
51
|
const s = raw.trim();
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
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] =
|
|
83
|
-
for (const t of o.hoverTokens) out[t.variable] =
|
|
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 =
|
|
75
|
+
const parsed = parseOverlayCss(raw);
|
|
113
76
|
if (!parsed) continue;
|
|
114
|
-
t.
|
|
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
|
|
124
|
-
*
|
|
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)
|
|
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>(
|
|
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>(
|
|
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(
|
|
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:
|
|
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: '
|
|
35
|
-
{ name: '
|
|
36
|
-
{ name: '
|
|
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
|
{
|