@motion-proto/live-tokens 0.84.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +0 -1
- package/CHANGELOG.md +85 -0
- package/README.md +1 -1
- package/dist-plugin/{chunk-V2OVSC5Z.js → chunk-GWMEYBZ2.js} +22 -1
- package/dist-plugin/{chunk-FD4FVDOC.js → chunk-ONYAIXCZ.js} +74 -1
- package/dist-plugin/{chunk-M6BWSS6C.js → chunk-W5VQV32K.js} +1 -1
- package/dist-plugin/index.cjs +101 -7
- package/dist-plugin/index.js +3 -3
- package/dist-plugin/migrateData/index.cjs +78 -5
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +74 -1
- package/dist-plugin/setColors/index.js +1 -1
- package/dist-plugin/setGeometry/index.cjs +74 -4
- package/dist-plugin/setGeometry/index.d.cts +1 -1
- package/dist-plugin/setGeometry/index.d.ts +1 -1
- package/dist-plugin/setGeometry/index.js +1 -4
- package/dist-plugin/tokensCssMigrations/index.cjs +22 -1
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +1 -1
- package/src/editor/component-editor/DialogEditor.svelte +1 -2
- package/src/editor/component-editor/ImageLightboxEditor.svelte +1 -2
- package/src/editor/component-editor/SlidePagerEditor.svelte +118 -0
- package/src/editor/component-editor/VideoFrameEditor.svelte +73 -0
- package/src/editor/component-editor/VideoLightboxEditor.svelte +95 -0
- package/src/editor/component-editor/registry.ts +40 -1
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +1 -4
- package/src/editor/core/components/aliasKinds.ts +0 -4
- package/src/editor/core/sketch/sketchLayer.ts +8 -1
- package/src/editor/core/store/editorPersistence.ts +46 -23
- package/src/editor/core/store/editorStore.ts +1 -5
- package/src/editor/core/store/editorTypes.ts +11 -7
- package/src/editor/core/themes/migrations/2026-09-21-scrim-surface.ts +56 -0
- package/src/editor/core/themes/migrations/2026-09-21-wash-color-and-opacity.ts +51 -0
- package/src/editor/core/themes/migrations/index.ts +4 -0
- package/src/editor/core/themes/slices/washes.ts +59 -77
- package/src/editor/overlay/LiveEditorOverlay.svelte +45 -52
- package/src/editor/pages/ComponentEditorPage.svelte +1 -1
- package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
- package/src/editor/ui/ThemePanel.svelte +9 -53
- package/src/editor/ui/UIPaletteSelector.svelte +5 -1
- package/src/editor/ui/UIVariantSelector.svelte +1 -1
- package/src/editor/ui/sections/WashesSection.svelte +100 -99
- package/src/editor/ui/variantScales.ts +0 -11
- package/src/live-tokens/data/colors-and-type/autumn.json +9 -7
- package/src/live-tokens/data/colors-and-type/default.json +9 -7
- package/src/live-tokens/data/colors-and-type/halloween.json +9 -7
- package/src/live-tokens/data/colors-and-type/midnight-study.json +9 -7
- package/src/live-tokens/data/colors-and-type/ocean.json +9 -7
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +9 -7
- package/src/live-tokens/data/colors-and-type/sketchy.json +9 -7
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +9 -7
- package/src/live-tokens/data/colors-and-type/sunset.json +9 -7
- package/src/live-tokens/data/themes/autumn.json +98 -12
- package/src/live-tokens/data/themes/halloween.json +98 -12
- package/src/live-tokens/data/themes/midnight-study.json +98 -12
- package/src/live-tokens/data/themes/ocean.json +98 -12
- package/src/live-tokens/data/themes/royal-velvet.json +98 -12
- package/src/live-tokens/data/themes/sketchy.json +98 -12
- package/src/live-tokens/data/themes/spring-meadow.json +98 -12
- package/src/live-tokens/data/themes/sunset.json +98 -12
- package/src/live-tokens/data/tokens.generated.css +8 -6
- package/src/system/assets/slide-04.webp +0 -0
- package/src/system/assets/slide-05.webp +0 -0
- package/src/system/assets/slide-06.webp +0 -0
- package/src/system/assets/watch-navigation-poster.webp +0 -0
- package/src/system/assets/watch-navigation.mp4 +0 -0
- package/src/system/components/Dialog.svelte +2 -7
- package/src/system/components/ImageLightbox.svelte +27 -39
- package/src/system/components/SlidePager.svelte +516 -0
- package/src/system/components/VideoFrame.svelte +284 -0
- package/src/system/components/VideoLightbox.svelte +381 -0
- package/src/system/styles/tokens.css +13 -8
- package/src/testing-js/{chunk-WQJ6QB6Y.js → chunk-OHU5BHGZ.js} +347 -9
- package/src/testing-js/chunk-OHU5BHGZ.js.map +1 -0
- package/src/testing-js/{chunk-XXABJNUU.js → chunk-Y6R7Z7VK.js} +10 -6
- package/src/testing-js/chunk-Y6R7Z7VK.js.map +1 -0
- package/src/testing-js/component-behavior.contract.js +1 -1
- package/src/testing-js/component-editor.contract.js +1 -1
- package/src/testing-js/component-render.contract.js +1 -1
- package/src/testing-js/index.d.ts +2 -2
- package/src/testing-js/index.js +2 -2
- package/src/testing-js/page-compliance.contract.js +1 -1
- package/src/testing-js/{vitest-BMLIbDs2.d.ts → vitest-5vWtqwWe.d.ts} +2 -2
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +2 -2
- package/src/testing-js/chunk-WQJ6QB6Y.js.map +0 -1
- package/src/testing-js/chunk-XXABJNUU.js.map +0 -1
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { get } from 'svelte/store';
|
|
17
|
-
import type { EditorState } from './editorTypes';
|
|
17
|
+
import type { EditorState, WashScale } from './editorTypes';
|
|
18
18
|
import { storageKey } from './editorConfig';
|
|
19
19
|
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 {
|
|
23
|
+
import { makeDefaultWashScale, type WashFamily } from '../themes/slices/washes';
|
|
24
24
|
import { sanitizeHarmonyAxes } from '../palettes/colorHarmony';
|
|
25
25
|
import {
|
|
26
26
|
renameBackgroundPaletteKey,
|
|
@@ -78,34 +78,57 @@ function migrateGradients(state: EditorState): EditorState {
|
|
|
78
78
|
// calls `Object.entries(slice.config)` unconditionally, so backfill the
|
|
79
79
|
// required fields and drop any non-object slice before the state reaches the
|
|
80
80
|
// renderer. Spread preserves optional fields like `unlinked`.
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// `
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'--overlay-
|
|
90
|
-
'--
|
|
91
|
-
'--
|
|
92
|
-
'--
|
|
81
|
+
// A persisted washes slice can predate two reshapes. Before the scrim and tint
|
|
82
|
+
// renames it was `overlays: { tokens, hoverTokens }`; until 0.85.0 each family
|
|
83
|
+
// was a list of `{variable, alias, opacity}` stops, each carrying its own
|
|
84
|
+
// colour. `hydrate` shallow-merges, so an older build's `washes` arrives whole
|
|
85
|
+
// in that shape, and `washesToVars` reads `color` and `stops` unconditionally.
|
|
86
|
+
// Reshape here: the family takes the base stop's colour, and each stop its
|
|
87
|
+
// opacity under the new variable.
|
|
88
|
+
const LEGACY_STOP_OPACITY_VARS: Record<string, string> = {
|
|
89
|
+
'--overlay-low': '--scrim-opacity-low',
|
|
90
|
+
'--overlay': '--scrim-opacity',
|
|
91
|
+
'--overlay-high': '--scrim-opacity-high',
|
|
92
|
+
'--scrim-low': '--scrim-opacity-low',
|
|
93
|
+
'--scrim': '--scrim-opacity',
|
|
94
|
+
'--scrim-high': '--scrim-opacity-high',
|
|
95
|
+
'--hover-low': '--tint-opacity-low',
|
|
96
|
+
'--hover': '--tint-opacity',
|
|
97
|
+
'--hover-high': '--tint-opacity-high',
|
|
98
|
+
'--tint-low': '--tint-opacity-low',
|
|
99
|
+
'--tint': '--tint-opacity',
|
|
100
|
+
'--tint-high': '--tint-opacity-high',
|
|
93
101
|
};
|
|
94
102
|
|
|
95
103
|
type LegacyWashes = {
|
|
104
|
+
scrim?: unknown;
|
|
105
|
+
tint?: unknown;
|
|
96
106
|
scrims?: unknown;
|
|
97
107
|
tints?: unknown;
|
|
98
108
|
hoverTokens?: unknown;
|
|
99
109
|
tokens?: unknown;
|
|
100
110
|
};
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
108
|
-
|
|
112
|
+
type LegacyStop = { variable: string; alias?: unknown; opacity?: unknown };
|
|
113
|
+
|
|
114
|
+
function isScale(raw: unknown): raw is WashScale {
|
|
115
|
+
return !!raw && typeof raw === 'object'
|
|
116
|
+
&& typeof (raw as WashScale).color === 'string'
|
|
117
|
+
&& Array.isArray((raw as WashScale).stops);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function washScale(current: unknown, legacyList: unknown, family: WashFamily): WashScale {
|
|
121
|
+
if (isScale(current)) return current;
|
|
122
|
+
const scale = makeDefaultWashScale(family);
|
|
123
|
+
if (!Array.isArray(legacyList)) return scale;
|
|
124
|
+
const stops = legacyList.filter((t): t is LegacyStop => !!t && typeof t === 'object' && 'variable' in t);
|
|
125
|
+
const base = stops.find((t) => LEGACY_STOP_OPACITY_VARS[t.variable] === `--${family}-opacity`) ?? stops[0];
|
|
126
|
+
if (typeof base?.alias === 'string') scale.color = base.alias;
|
|
127
|
+
for (const t of stops) {
|
|
128
|
+
const stop = scale.stops.find((s) => s.variable === LEGACY_STOP_OPACITY_VARS[t.variable]);
|
|
129
|
+
if (stop && typeof t.opacity === 'number') stop.opacity = t.opacity;
|
|
130
|
+
}
|
|
131
|
+
return scale;
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
export function normalizeWashes(state: EditorState): EditorState {
|
|
@@ -113,8 +136,8 @@ export function normalizeWashes(state: EditorState): EditorState {
|
|
|
113
136
|
const legacy = ((state as unknown as { overlays?: LegacyWashes }).overlays ?? {}) as LegacyWashes;
|
|
114
137
|
const next = { ...state } as EditorState & { overlays?: unknown };
|
|
115
138
|
next.washes = {
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
scrim: washScale(washes.scrim, washes.scrims ?? legacy.tokens, 'scrim'),
|
|
140
|
+
tint: washScale(washes.tint, washes.tints ?? washes.hoverTokens ?? legacy.hoverTokens, 'tint'),
|
|
118
141
|
};
|
|
119
142
|
delete next.overlays;
|
|
120
143
|
return next;
|
|
@@ -152,12 +152,8 @@ export {
|
|
|
152
152
|
export {
|
|
153
153
|
washesToVars,
|
|
154
154
|
WASH_VAR_NAMES,
|
|
155
|
-
applyWashVarsToState,
|
|
156
155
|
makeDefaultWashesState,
|
|
157
|
-
|
|
158
|
-
makeDefaultTintTokens,
|
|
159
|
-
washTokenToCss,
|
|
160
|
-
parseWashCss,
|
|
156
|
+
makeDefaultWashScale,
|
|
161
157
|
} from '../themes/slices/washes';
|
|
162
158
|
|
|
163
159
|
export {
|
|
@@ -23,16 +23,20 @@ export interface ShadowOverrideFlags {
|
|
|
23
23
|
distance: boolean; blur: boolean; size: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
* is behind them; tints shade the surface they sit on. */
|
|
29
|
-
export interface WashToken {
|
|
26
|
+
/** One opacity stop of a wash family, emitted as a bare number. */
|
|
27
|
+
export interface WashStop {
|
|
30
28
|
variable: string;
|
|
31
29
|
label: string;
|
|
32
|
-
alias: string;
|
|
33
30
|
opacity: number;
|
|
34
31
|
}
|
|
35
32
|
|
|
33
|
+
/** A wash family: one aliased colour and its opacity stops. Scrims dim what is
|
|
34
|
+
* behind them; tints shade the surface they sit on. */
|
|
35
|
+
export interface WashScale {
|
|
36
|
+
color: string;
|
|
37
|
+
stops: WashStop[];
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
export interface ColumnsState {
|
|
37
41
|
count: number;
|
|
38
42
|
maxWidth: number;
|
|
@@ -113,8 +117,8 @@ export interface EditorState {
|
|
|
113
117
|
overrides: Record<string, ShadowOverrideFlags>;
|
|
114
118
|
};
|
|
115
119
|
washes: {
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
scrim: WashScale;
|
|
121
|
+
tint: WashScale;
|
|
118
122
|
};
|
|
119
123
|
columns: ColumnsState;
|
|
120
124
|
components: Record<string, ComponentSlice>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A screen's scrim is one fill again (2026-09-21).
|
|
5
|
+
*
|
|
6
|
+
* From 0.84.0 Dialog and ImageLightbox read `-scrim-color` and
|
|
7
|
+
* `-scrim-opacity` and mixed them where they painted. The theme now composes
|
|
8
|
+
* `--scrim-low`, `--scrim`, and `--scrim-high` from its own colour and
|
|
9
|
+
* strengths, so a screen picks one of those stops as `-scrim-surface`, the way
|
|
10
|
+
* a hover picks a `--tint-*` stop.
|
|
11
|
+
*
|
|
12
|
+
* The scale's colour at one of its strengths becomes that stop. Any other pair
|
|
13
|
+
* becomes the `color-mix()` the screen used to paint, so the page looks the
|
|
14
|
+
* same.
|
|
15
|
+
*/
|
|
16
|
+
const STOPS: Record<string, string> = {
|
|
17
|
+
'--scrim-opacity-low': '--scrim-low',
|
|
18
|
+
'--scrim-opacity': '--scrim',
|
|
19
|
+
'--scrim-opacity-high': '--scrim-high',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const PAIR = /^--(imagelightbox|dialog)-scrim-(color|opacity)$/;
|
|
23
|
+
|
|
24
|
+
const ref = (value: string) => (value.startsWith('--') ? `var(${value})` : value);
|
|
25
|
+
|
|
26
|
+
function fill(color: string, opacity: string): string {
|
|
27
|
+
if (color === '--scrim-color' && STOPS[opacity]) return STOPS[opacity];
|
|
28
|
+
if (opacity === '1') return color;
|
|
29
|
+
const pct = Number.isFinite(Number(opacity))
|
|
30
|
+
? `${Number(opacity) * 100}%`
|
|
31
|
+
: `calc(${ref(opacity)} * 100%)`;
|
|
32
|
+
return `color-mix(in srgb, ${ref(color)} ${pct}, transparent)`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const componentMigration_2026_09_21_scrimSurface: Migration = {
|
|
36
|
+
id: '2026-09-21-scrim-surface',
|
|
37
|
+
fromVersion: 37,
|
|
38
|
+
toVersion: 38,
|
|
39
|
+
appliesTo: 'component-config',
|
|
40
|
+
apply(rawVars) {
|
|
41
|
+
const out: Record<string, string> = {};
|
|
42
|
+
const pairs: Record<string, { color?: string; opacity?: string }> = {};
|
|
43
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
44
|
+
const match = PAIR.exec(key);
|
|
45
|
+
if (!match) {
|
|
46
|
+
out[key] = value;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
(pairs[match[1]] ??= {})[match[2] as 'color' | 'opacity'] = value;
|
|
50
|
+
}
|
|
51
|
+
for (const [id, { color = '--scrim-color', opacity = '--scrim-opacity-high' }] of Object.entries(pairs)) {
|
|
52
|
+
out[`--${id}-scrim-surface`] = fill(color, opacity);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Migration } from './index';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A wash family's colour and its strengths, apart (2026-09-21).
|
|
5
|
+
*
|
|
6
|
+
* A theme stored each `--scrim-*` and `--tint-*` stop as a composed fill,
|
|
7
|
+
* `color-mix(in srgb, var(<colour>) N%, transparent)`, so the colour was
|
|
8
|
+
* restated three times and the fill overrode the one tokens.css composes from
|
|
9
|
+
* `--scrim-color` and `--scrim-opacity-*`. That left Dialog and ImageLightbox,
|
|
10
|
+
* which read the pair, deaf to every theme's scrim.
|
|
11
|
+
*
|
|
12
|
+
* Each family now stores `--<family>-color` and `--<family>-opacity-*`. The
|
|
13
|
+
* base stop gives the colour, falling back to the first stop that parses. A
|
|
14
|
+
* stop that does not parse is a hand-written fill: it stays, so the page keeps
|
|
15
|
+
* painting what the theme's author chose.
|
|
16
|
+
*/
|
|
17
|
+
const FAMILIES = ['scrim', 'tint'] as const;
|
|
18
|
+
|
|
19
|
+
const COLOR_MIX_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+(?:\.\d+)?)%,\s*transparent\)$/i;
|
|
20
|
+
const PLAIN_VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
|
|
21
|
+
|
|
22
|
+
function parseFill(raw: string): { color: string; opacity: number } | null {
|
|
23
|
+
const s = raw.trim();
|
|
24
|
+
const mix = s.match(COLOR_MIX_RE);
|
|
25
|
+
if (mix) return { color: mix[1], opacity: Math.min(100, Number(mix[2])) / 100 };
|
|
26
|
+
const plain = s.match(PLAIN_VAR_RE);
|
|
27
|
+
return plain ? { color: plain[1], opacity: 1 } : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const colorsAndTypeMigration_2026_09_21_washColorAndOpacity: Migration = {
|
|
31
|
+
id: '2026-09-21-wash-color-and-opacity',
|
|
32
|
+
fromVersion: 8,
|
|
33
|
+
toVersion: 9,
|
|
34
|
+
appliesTo: 'colors-and-type',
|
|
35
|
+
apply(rawVars) {
|
|
36
|
+
const out = { ...rawVars };
|
|
37
|
+
for (const family of FAMILIES) {
|
|
38
|
+
let color: string | null = null;
|
|
39
|
+
for (const suffix of ['', '-low', '-high']) {
|
|
40
|
+
const key = `--${family}${suffix}`;
|
|
41
|
+
const parsed = out[key] === undefined ? null : parseFill(out[key]);
|
|
42
|
+
if (!parsed) continue;
|
|
43
|
+
color ??= parsed.color;
|
|
44
|
+
out[`--${family}-opacity${suffix}`] = String(parsed.opacity);
|
|
45
|
+
delete out[key];
|
|
46
|
+
}
|
|
47
|
+
if (color) out[`--${family}-color`] = `var(${color})`;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -81,6 +81,8 @@ import { componentMigration_2026_09_13_indicator } from './2026-09-13-indicator'
|
|
|
81
81
|
import { componentMigration_2026_09_13_sectiondividerSurface } from './2026-09-13-sectiondivider-surface';
|
|
82
82
|
import { componentMigration_2026_09_20_imagelightboxScrim } from './2026-09-20-imagelightbox-scrim';
|
|
83
83
|
import { componentMigration_2026_09_20_scrimColorAndOpacity } from './2026-09-20-scrim-color-and-opacity';
|
|
84
|
+
import { colorsAndTypeMigration_2026_09_21_washColorAndOpacity } from './2026-09-21-wash-color-and-opacity';
|
|
85
|
+
import { componentMigration_2026_09_21_scrimSurface } from './2026-09-21-scrim-surface';
|
|
84
86
|
import { componentMigration_2026_09_13_cornerbadgePrefix } from './2026-09-13-cornerbadge-prefix';
|
|
85
87
|
import { componentMigration_2026_09_13_toggleLabel } from './2026-09-13-toggle-label';
|
|
86
88
|
import { componentMigration_2026_09_13_collapsiblesectionOpen } from './2026-09-13-collapsiblesection-open';
|
|
@@ -136,6 +138,8 @@ export const MIGRATIONS: Migration[] = [
|
|
|
136
138
|
componentMigration_2026_09_13_badgeBrand,
|
|
137
139
|
componentMigration_2026_09_20_imagelightboxScrim,
|
|
138
140
|
componentMigration_2026_09_20_scrimColorAndOpacity,
|
|
141
|
+
colorsAndTypeMigration_2026_09_21_washColorAndOpacity,
|
|
142
|
+
componentMigration_2026_09_21_scrimSurface,
|
|
139
143
|
];
|
|
140
144
|
|
|
141
145
|
function countFor(kind: 'colors-and-type' | 'component-config'): number {
|
|
@@ -1,107 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Washes slice — a wash is a translucent layer of color
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* every stop).
|
|
2
|
+
* Washes slice — a wash is a translucent layer of color. Each family holds one
|
|
3
|
+
* colour, aliased to a theme token, and three opacity stops. The theme emits
|
|
4
|
+
* the colour and the stops; tokens.css composes the `--scrim-*` and `--tint-*`
|
|
5
|
+
* fills from them, so the strength moves without restating the colour.
|
|
7
6
|
*
|
|
8
7
|
* Two families, opposite jobs. A **scrim** dims what sits behind it, which is
|
|
9
|
-
* what a modal needs. A **tint** shades the surface it
|
|
10
|
-
* a hover needs
|
|
11
|
-
* surface
|
|
12
|
-
* darkens a light one without picking a direction.
|
|
13
|
-
*
|
|
14
|
-
* Defaults diverge from tokens.css by design: the editor starts from a
|
|
15
|
-
* neutral alias and tokens.css continues to win until first edit.
|
|
8
|
+
* what a modal needs, so its stops run dark. A **tint** shades the surface it
|
|
9
|
+
* sits on, which is what a hover needs, so its stops run faint. Scrims alias a
|
|
10
|
+
* near-black surface and tints the text color, so a tint lightens a dark theme
|
|
11
|
+
* and darkens a light one without picking a direction.
|
|
16
12
|
*/
|
|
17
|
-
import type { EditorState,
|
|
13
|
+
import type { EditorState, WashScale } from '../../store/editorTypes';
|
|
18
14
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{ variable: '--scrim', label: 'Base', alias: '--surface-neutral-lowest', opacity: 0.51 },
|
|
23
|
-
{ variable: '--scrim-high', label: 'High', alias: '--surface-neutral-lowest', opacity: 0.64 },
|
|
24
|
-
];
|
|
25
|
-
}
|
|
15
|
+
export type WashFamily = 'scrim' | 'tint';
|
|
16
|
+
|
|
17
|
+
export const WASH_FAMILIES: readonly WashFamily[] = ['scrim', 'tint'];
|
|
26
18
|
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
export function makeDefaultWashScale(family: WashFamily): WashScale {
|
|
20
|
+
if (family === 'scrim') {
|
|
21
|
+
return {
|
|
22
|
+
color: '--surface-neutral-lowest',
|
|
23
|
+
stops: [
|
|
24
|
+
{ variable: '--scrim-opacity-low', label: 'Low', opacity: 0.7 },
|
|
25
|
+
{ variable: '--scrim-opacity', label: 'Base', opacity: 0.8 },
|
|
26
|
+
{ variable: '--scrim-opacity-high', label: 'High', opacity: 0.9 },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
color: '--text-primary',
|
|
32
|
+
stops: [
|
|
33
|
+
{ variable: '--tint-opacity-low', label: 'Low', opacity: 0.05 },
|
|
34
|
+
{ variable: '--tint-opacity', label: 'Base', opacity: 0.1 },
|
|
35
|
+
{ variable: '--tint-opacity-high', label: 'High', opacity: 0.15 },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export function makeDefaultWashesState(): EditorState['washes'] {
|
|
36
41
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
scrim: makeDefaultWashScale('scrim'),
|
|
43
|
+
tint: makeDefaultWashScale('tint'),
|
|
39
44
|
};
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
export const
|
|
43
|
-
'--scrim-low', '--scrim', '--scrim-high',
|
|
44
|
-
'--tint-low', '--tint', '--tint-high',
|
|
45
|
-
] as const;
|
|
47
|
+
export const washColorVar = (family: WashFamily) => `--${family}-color`;
|
|
46
48
|
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
49
|
+
export const WASH_VAR_NAMES: readonly string[] = WASH_FAMILIES.flatMap((family) => [
|
|
50
|
+
washColorVar(family),
|
|
51
|
+
...makeDefaultWashScale(family).stops.map((s) => s.variable),
|
|
52
|
+
]);
|
|
52
53
|
|
|
53
|
-
const COLOR_MIX_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+)%,\s*transparent\)$/i;
|
|
54
54
|
const PLAIN_VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
|
|
55
55
|
|
|
56
|
-
export function parseWashCss(raw: string): { alias: string; opacity: number } | null {
|
|
57
|
-
const s = raw.trim();
|
|
58
|
-
const mix = s.match(COLOR_MIX_RE);
|
|
59
|
-
if (mix) {
|
|
60
|
-
const pct = parseInt(mix[2], 10);
|
|
61
|
-
if (!Number.isFinite(pct)) return null;
|
|
62
|
-
return { alias: mix[1], opacity: Math.max(0, Math.min(100, pct)) / 100 };
|
|
63
|
-
}
|
|
64
|
-
const plain = s.match(PLAIN_VAR_RE);
|
|
65
|
-
if (plain) return { alias: plain[1], opacity: 1 };
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
56
|
export function washesToVars(w: EditorState['washes']): Record<string, string> {
|
|
70
57
|
const out: Record<string, string> = {};
|
|
71
|
-
for (const
|
|
72
|
-
|
|
58
|
+
for (const family of WASH_FAMILIES) {
|
|
59
|
+
out[washColorVar(family)] = `var(${w[family].color})`;
|
|
60
|
+
for (const s of w[family].stops) out[s.variable] = String(s.opacity);
|
|
61
|
+
}
|
|
73
62
|
return out;
|
|
74
63
|
}
|
|
75
64
|
|
|
76
|
-
export function applyWashVarsToState(washes: EditorState['washes'], vars: Record<string, string>): void {
|
|
77
|
-
const applyTo = (list: WashToken[]) => {
|
|
78
|
-
for (const t of list) {
|
|
79
|
-
const raw = vars[t.variable];
|
|
80
|
-
if (!raw) continue;
|
|
81
|
-
const parsed = parseWashCss(raw);
|
|
82
|
-
if (!parsed) continue;
|
|
83
|
-
t.alias = parsed.alias;
|
|
84
|
-
t.opacity = parsed.opacity;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
applyTo(washes.scrims);
|
|
88
|
-
applyTo(washes.tints);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
65
|
/**
|
|
92
|
-
* Loader: route
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* the cssVars bag so the DOM still paints them; the user's next edit in the
|
|
96
|
-
* picker promotes them to the typed slice.
|
|
66
|
+
* Loader: route a freshly-loaded theme's wash entries into `next.washes` and
|
|
67
|
+
* remove them from the bag. A colour that is not a plain `var()` stays in the
|
|
68
|
+
* bag so the DOM still paints it.
|
|
97
69
|
*/
|
|
98
70
|
export function loadWashesFromVars(
|
|
99
71
|
next: EditorState,
|
|
100
72
|
rawVars: Record<string, string>,
|
|
101
73
|
): void {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
74
|
+
for (const family of WASH_FAMILIES) {
|
|
75
|
+
const scale = next.washes[family];
|
|
76
|
+
const colorVar = washColorVar(family);
|
|
77
|
+
const color = rawVars[colorVar]?.trim().match(PLAIN_VAR_RE);
|
|
78
|
+
if (color) {
|
|
79
|
+
scale.color = color[1];
|
|
80
|
+
delete rawVars[colorVar];
|
|
81
|
+
}
|
|
82
|
+
for (const s of scale.stops) {
|
|
83
|
+
const n = Number(rawVars[s.variable]);
|
|
84
|
+
if (rawVars[s.variable] === undefined || !Number.isFinite(n)) continue;
|
|
85
|
+
s.opacity = Math.max(0, Math.min(1, n));
|
|
86
|
+
delete rawVars[s.variable];
|
|
87
|
+
}
|
|
106
88
|
}
|
|
107
89
|
}
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
projectRoot = INJECTED_PROJECT_ROOT
|
|
48
48
|
}: Props = $props();
|
|
49
49
|
|
|
50
|
+
let activeLink = $derived(navLinks.find((l) => l.path === $route));
|
|
51
|
+
|
|
50
52
|
// Dev-only; skip inside iframe (editor route embeds this app).
|
|
51
53
|
const isDev = import.meta.env.DEV;
|
|
52
54
|
const isInIframe = typeof window !== 'undefined' && window.parent !== window;
|
|
@@ -677,22 +679,19 @@
|
|
|
677
679
|
|
|
678
680
|
{#if open && navLinks.length > 0}
|
|
679
681
|
<div class="seg-group" transition:fade={BTN_FADE}>
|
|
680
|
-
<
|
|
681
|
-
<div class="
|
|
682
|
-
{#
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
disabled={link.disabled}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
<span>{link.label}</span>
|
|
694
|
-
</button>
|
|
695
|
-
{/each}
|
|
682
|
+
<label class="seg-label" for="lt-active-page">Active Page:</label>
|
|
683
|
+
<div class="page-select">
|
|
684
|
+
{#if activeLink?.icon}<i class="fas {activeLink.icon} page-icon"></i>{/if}
|
|
685
|
+
<select
|
|
686
|
+
id="lt-active-page"
|
|
687
|
+
value={$route}
|
|
688
|
+
onchange={(e) => navigate(e.currentTarget.value)}
|
|
689
|
+
>
|
|
690
|
+
{#each navLinks as link (link.path)}
|
|
691
|
+
<option value={link.path} disabled={link.disabled}>{link.label}</option>
|
|
692
|
+
{/each}
|
|
693
|
+
</select>
|
|
694
|
+
<i class="fas fa-chevron-down page-caret"></i>
|
|
696
695
|
</div>
|
|
697
696
|
</div>
|
|
698
697
|
{/if}
|
|
@@ -1116,61 +1115,55 @@
|
|
|
1116
1115
|
color: var(--ui-text-primary, #fff);
|
|
1117
1116
|
}
|
|
1118
1117
|
|
|
1119
|
-
.
|
|
1118
|
+
.page-select {
|
|
1119
|
+
position: relative;
|
|
1120
1120
|
display: inline-flex;
|
|
1121
1121
|
align-items: center;
|
|
1122
|
-
|
|
1123
|
-
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.page-select select {
|
|
1125
|
+
appearance: none;
|
|
1126
|
+
color-scheme: dark;
|
|
1127
|
+
padding: var(--ui-space-4, 4px) calc(var(--ui-space-8, 8px) + 20px) var(--ui-space-4, 4px) var(--ui-space-8, 8px);
|
|
1124
1128
|
background: rgba(0, 0, 0, 0.55);
|
|
1125
1129
|
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
1126
1130
|
border-radius: var(--ui-radius-lg, 6px);
|
|
1127
|
-
|
|
1128
|
-
inset 0 1px 0 rgba(0, 0, 0, 0.5),
|
|
1129
|
-
0 0 0 1px rgba(0, 0, 0, 0.4);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
.seg-pill {
|
|
1133
|
-
display: inline-flex;
|
|
1134
|
-
align-items: center;
|
|
1135
|
-
gap: var(--ui-space-4, 4px);
|
|
1136
|
-
padding: var(--ui-space-4, 4px) var(--ui-space-8, 8px);
|
|
1137
|
-
background: transparent;
|
|
1138
|
-
border: 1px solid transparent;
|
|
1139
|
-
border-radius: 3px;
|
|
1140
|
-
color: rgba(255, 255, 255, 0.6);
|
|
1131
|
+
color: var(--ui-text-primary, #fff);
|
|
1141
1132
|
font-family: inherit;
|
|
1142
1133
|
font-size: var(--ui-font-size-md, 16px);
|
|
1143
1134
|
font-weight: var(--ui-font-weight-medium, 500);
|
|
1144
1135
|
cursor: pointer;
|
|
1145
|
-
transition:
|
|
1146
|
-
background var(--ui-transition-fast, 120ms ease),
|
|
1147
|
-
color var(--ui-transition-fast, 120ms ease),
|
|
1148
|
-
border-color var(--ui-transition-fast, 120ms ease);
|
|
1136
|
+
transition: border-color var(--ui-transition-fast, 120ms ease);
|
|
1149
1137
|
}
|
|
1150
1138
|
|
|
1151
|
-
.
|
|
1152
|
-
|
|
1153
|
-
opacity: 0.85;
|
|
1139
|
+
.page-icon ~ select {
|
|
1140
|
+
padding-left: calc(var(--ui-space-8, 8px) + 24px);
|
|
1154
1141
|
}
|
|
1155
1142
|
|
|
1156
|
-
.
|
|
1157
|
-
color: rgba(255, 255, 255, 0.
|
|
1143
|
+
.page-select select:hover {
|
|
1144
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
1158
1145
|
}
|
|
1159
1146
|
|
|
1160
|
-
.
|
|
1161
|
-
|
|
1162
|
-
|
|
1147
|
+
.page-select select:focus-visible {
|
|
1148
|
+
outline: 2px solid rgba(255, 255, 255, 0.5);
|
|
1149
|
+
outline-offset: 2px;
|
|
1163
1150
|
}
|
|
1164
1151
|
|
|
1165
|
-
.
|
|
1166
|
-
|
|
1152
|
+
.page-icon,
|
|
1153
|
+
.page-caret {
|
|
1154
|
+
position: absolute;
|
|
1155
|
+
pointer-events: none;
|
|
1156
|
+
color: rgba(255, 255, 255, 0.6);
|
|
1167
1157
|
}
|
|
1168
1158
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1159
|
+
.page-icon {
|
|
1160
|
+
left: var(--ui-space-8, 8px);
|
|
1161
|
+
font-size: var(--ui-font-size-md, 16px);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.page-caret {
|
|
1165
|
+
right: var(--ui-space-8, 8px);
|
|
1166
|
+
font-size: var(--ui-font-size-xs, 12px);
|
|
1174
1167
|
}
|
|
1175
1168
|
|
|
1176
1169
|
.frame-wrap {
|