@motion-proto/live-tokens 0.95.0 → 0.97.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/CHANGELOG.md +55 -0
- package/bin/set-colors.mjs +1 -0
- package/bin/setup-claude.mjs +2 -2
- package/dist-plugin/setColors/index.cjs +7 -1
- package/dist-plugin/setColors/index.d.cts +9 -4
- package/dist-plugin/setColors/index.d.ts +9 -4
- package/dist-plugin/setColors/index.js +7 -1
- package/dist-plugin/setGeometry/index.d.cts +1 -1
- package/dist-plugin/setGeometry/index.d.ts +1 -1
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.cts → themeTypes-BaTQUyac.d.cts} +10 -1
- package/dist-plugin/{themeTypes-B8_Idrp4.d.ts → themeTypes-BaTQUyac.d.ts} +10 -1
- package/package.json +4 -2
- package/src/demo/TestingLoops.svelte +39 -16
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +0 -7
- package/src/editor/core/store/editorPersistence.ts +9 -1
- package/src/editor/core/store/editorRenderer.ts +2 -0
- package/src/editor/core/store/editorStore.ts +25 -0
- package/src/editor/core/store/editorTypes.ts +9 -0
- package/src/editor/core/themes/buildColors.ts +16 -5
- package/src/editor/core/themes/slices/customColors.ts +226 -0
- package/src/editor/core/themes/themeTypes.ts +10 -0
- package/src/editor/docs/content/editing-tokens.md +28 -0
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/pages/EditorShell.svelte +1 -0
- package/src/editor/ui/CustomColorReplaceStep.svelte +118 -0
- package/src/editor/ui/UIPaletteSelector.svelte +338 -15
- package/src/editor/ui/UITokenSelector.svelte +7 -12
- package/src/editor/ui/VariablesTab.svelte +3 -0
- package/src/editor/ui/anchorToTrigger.ts +63 -0
- package/src/editor/ui/sections/CustomColorsSection.svelte +254 -0
- package/src/system/components/ImageLightbox.svelte +17 -3
- package/src/testing-js/page-compliance.contract.js +31 -5
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/editor/ui/keepInViewport.ts +0 -84
|
@@ -26,7 +26,7 @@ import { SHADOW_VAR_NAMES, parseShadowCss, shadowTokenCss } from './parsers/shad
|
|
|
26
26
|
import { AA_BODY, AA_LARGE, contrastRatio, findLForContrast } from '../palettes/contrast';
|
|
27
27
|
import { type HarmonyAxis, type HarmonyMode } from '../palettes/colorHarmony';
|
|
28
28
|
import { defaultPaletteConfig } from '../../ui/palette/paletteMath';
|
|
29
|
-
import type { GradientDiskToken, PaletteConfig } from './themeTypes';
|
|
29
|
+
import type { CustomColorDiskToken, GradientDiskToken, PaletteConfig } from './themeTypes';
|
|
30
30
|
|
|
31
31
|
export interface ColorsInput {
|
|
32
32
|
scheme: SchemeDirection;
|
|
@@ -41,12 +41,14 @@ export interface ColorsInput {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** The live colors and type this pass reads before it replaces the color
|
|
44
|
-
* state: the override bag, so every name no palette owns rides through,
|
|
45
|
-
*
|
|
46
|
-
* derived, not carried — see
|
|
44
|
+
* state: the override bag, so every name no palette owns rides through, the
|
|
45
|
+
* swatch gradients, so user-tuned ones survive, and the custom colours, which
|
|
46
|
+
* ride through untouched. Shadow opacity is derived, not carried — see
|
|
47
|
+
* `shadowVars`. */
|
|
47
48
|
export interface CarryForward {
|
|
48
49
|
cssVariables?: Record<string, string>;
|
|
49
50
|
gradients?: GradientDiskToken[];
|
|
51
|
+
customColors?: CustomColorDiskToken[];
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface ContrastCheck {
|
|
@@ -80,6 +82,9 @@ export interface ColorsState {
|
|
|
80
82
|
editorConfigs: Record<string, PaletteConfig>;
|
|
81
83
|
harmonyAxes: HarmonyAxis[];
|
|
82
84
|
gradients: GradientDiskToken[];
|
|
85
|
+
/** Present only when the carried colours and type held the field, so a
|
|
86
|
+
* recolor adds no key to a theme that never had one. */
|
|
87
|
+
customColors?: CustomColorDiskToken[];
|
|
83
88
|
cssVariables: Record<string, string>;
|
|
84
89
|
}
|
|
85
90
|
|
|
@@ -467,7 +472,13 @@ export function buildColors(input: ColorsInput, carry: CarryForward = {}): Build
|
|
|
467
472
|
Object.assign(cssVariables, shadowVars(carry.cssVariables ?? {}, baseColors.Canvas));
|
|
468
473
|
|
|
469
474
|
return {
|
|
470
|
-
colors: {
|
|
475
|
+
colors: {
|
|
476
|
+
editorConfigs: configs,
|
|
477
|
+
harmonyAxes,
|
|
478
|
+
gradients,
|
|
479
|
+
...(carry.customColors ? { customColors: carry.customColors } : {}),
|
|
480
|
+
cssVariables,
|
|
481
|
+
},
|
|
471
482
|
report: {
|
|
472
483
|
...report,
|
|
473
484
|
gradients: gradients === carry.gradients ? 'carried' : 'recipes',
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { CssVarRef, CustomColorToken, EditorState } from '../../store/editorTypes';
|
|
2
|
+
import type { CustomColorDiskToken } from '../themeTypes';
|
|
3
|
+
import type { GradientStop } from '../parsers/gradient';
|
|
4
|
+
import { get } from 'svelte/store';
|
|
5
|
+
import { mutate, store } from '../../store/editorCore';
|
|
6
|
+
import { oklchToCss, type Oklch } from '../../palettes/oklch';
|
|
7
|
+
import { PALETTE_SPECS, PALETTE_STEPS, palettesToValues } from '../../palettes/paletteDerivation';
|
|
8
|
+
import { WASH_FAMILIES, type WashFamily } from './washes';
|
|
9
|
+
|
|
10
|
+
const CUSTOM_COLOR_SLOT_RE = /^--color-custom-\d+$/;
|
|
11
|
+
|
|
12
|
+
export function isCustomColorSlot(variable: string): boolean {
|
|
13
|
+
return CUSTOM_COLOR_SLOT_RE.test(variable);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isCustomColorToken(t: unknown): t is CustomColorToken {
|
|
17
|
+
if (!t || typeof t !== 'object') return false;
|
|
18
|
+
const { variable, color } = t as Partial<CustomColorToken>;
|
|
19
|
+
return typeof variable === 'string' && isCustomColorSlot(variable)
|
|
20
|
+
&& !!color && typeof color === 'object'
|
|
21
|
+
&& [color.l, color.c, color.h].every((n) => typeof n === 'number' && Number.isFinite(n));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isCustomColorList(tokens: unknown): tokens is CustomColorToken[] {
|
|
25
|
+
return Array.isArray(tokens) && tokens.every(isCustomColorToken);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The lowest free slot, so a removed number is reused before the list grows. */
|
|
29
|
+
export function nextCustomColorSlot(tokens: readonly CustomColorToken[]): string {
|
|
30
|
+
const used = new Set(tokens.map((t) => t.variable));
|
|
31
|
+
for (let n = 1; ; n++) {
|
|
32
|
+
const name = `--color-custom-${n}`;
|
|
33
|
+
if (!used.has(name)) return name;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function customColorsToVars(c: EditorState['customColors']): Record<string, string> {
|
|
38
|
+
const out: Record<string, string> = {};
|
|
39
|
+
for (const t of c.tokens) out[t.variable] = oklchToCss(t.color.l, t.color.c, t.color.h);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The rendered `--color-custom-N` strings in `cssVariables` are a projection
|
|
44
|
+
* for production CSS; the structured field is the basis, so the loader sheds
|
|
45
|
+
* them from the bag. A file without the field keeps its bag as it is. */
|
|
46
|
+
export function loadCustomColorsFromFile(
|
|
47
|
+
next: EditorState,
|
|
48
|
+
customColors: CustomColorDiskToken[] | undefined,
|
|
49
|
+
rawVars: Record<string, string>,
|
|
50
|
+
): void {
|
|
51
|
+
for (const t of customColors ?? []) delete rawVars[t.variable];
|
|
52
|
+
if (isCustomColorList(customColors)) {
|
|
53
|
+
next.customColors.tokens = structuredClone(customColors);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findCustomColor(s: EditorState, variable: string): CustomColorToken | undefined {
|
|
58
|
+
return s.customColors.tokens.find((t) => t.variable === variable);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function addCustomColor(color: Oklch): string {
|
|
62
|
+
let variable = '';
|
|
63
|
+
mutate('add custom colour', (s) => {
|
|
64
|
+
variable = nextCustomColorSlot(s.customColors.tokens);
|
|
65
|
+
s.customColors.tokens.push({ variable, color: { ...color } });
|
|
66
|
+
});
|
|
67
|
+
return variable;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function setCustomColor(variable: string, color: Oklch): void {
|
|
71
|
+
mutate(`set custom colour ${variable}`, (s) => {
|
|
72
|
+
const t = findCustomColor(s, variable);
|
|
73
|
+
if (t) t.color = { ...color };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One slot that names a custom colour. An alias counts once however many of
|
|
78
|
+
* its inline gradient stops name the colour; so does a library gradient. */
|
|
79
|
+
export type CustomColorUsage =
|
|
80
|
+
| { kind: 'alias'; component: string; property: string }
|
|
81
|
+
| { kind: 'gradient'; gradient: string }
|
|
82
|
+
| { kind: 'wash'; family: WashFamily };
|
|
83
|
+
|
|
84
|
+
function escapeRegExp(s: string): string {
|
|
85
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `var(--color-custom-1)` inside a literal, never `var(--color-custom-10)`. */
|
|
89
|
+
function varRefPattern(variable: string): RegExp {
|
|
90
|
+
return new RegExp(`var\\(${escapeRegExp(variable)}\\)`, 'g');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stopsName(stops: readonly GradientStop[], variable: string): boolean {
|
|
94
|
+
return stops.some((st) => st.color === variable);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A literal names a custom colour when the picker materialized a library
|
|
98
|
+
* gradient with a per-slot angle: the stop list is inlined as `var()` refs. */
|
|
99
|
+
function refNames(ref: CssVarRef, variable: string): boolean {
|
|
100
|
+
switch (ref.kind) {
|
|
101
|
+
case 'token':
|
|
102
|
+
return ref.name === variable;
|
|
103
|
+
case 'literal':
|
|
104
|
+
return varRefPattern(variable).test(ref.value);
|
|
105
|
+
case 'gradient':
|
|
106
|
+
return stopsName(ref.value.stops, variable);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function customColorUsages(state: EditorState, variable: string): CustomColorUsage[] {
|
|
111
|
+
const out: CustomColorUsage[] = [];
|
|
112
|
+
for (const [component, slice] of Object.entries(state.components)) {
|
|
113
|
+
for (const [property, ref] of Object.entries(slice.aliases)) {
|
|
114
|
+
if (refNames(ref, variable)) out.push({ kind: 'alias', component, property });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const g of state.gradients.tokens) {
|
|
118
|
+
if (stopsName(g.stops, variable)) out.push({ kind: 'gradient', gradient: g.variable });
|
|
119
|
+
}
|
|
120
|
+
for (const family of WASH_FAMILIES) {
|
|
121
|
+
if (state.washes[family].color === variable) out.push({ kind: 'wash', family });
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function joinAnd(items: readonly string[]): string {
|
|
127
|
+
if (items.length <= 1) return items[0] ?? '';
|
|
128
|
+
return `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The replace step's usage line: "Used by 4 properties in Button and Card." */
|
|
132
|
+
export function describeCustomColorUsages(
|
|
133
|
+
usages: readonly CustomColorUsage[],
|
|
134
|
+
componentLabel: (component: string) => string,
|
|
135
|
+
): string {
|
|
136
|
+
const aliases = usages.filter((u) => u.kind === 'alias');
|
|
137
|
+
const parts: string[] = [];
|
|
138
|
+
if (aliases.length > 0) {
|
|
139
|
+
const components = [...new Set(aliases.map((u) => u.component))].map(componentLabel);
|
|
140
|
+
const noun = aliases.length === 1 ? 'property' : 'properties';
|
|
141
|
+
parts.push(`${aliases.length} ${noun} in ${joinAnd(components)}`);
|
|
142
|
+
}
|
|
143
|
+
for (const u of usages) {
|
|
144
|
+
if (u.kind === 'gradient') parts.push(`Gradient ${u.gradient.replace(/^--gradient-/, '')}`);
|
|
145
|
+
else if (u.kind === 'wash') parts.push(`the ${u.family} colour`);
|
|
146
|
+
}
|
|
147
|
+
return `Used by ${joinAnd(parts)}.`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const WHITE: Oklch = { l: 1, c: 0, h: 0 };
|
|
151
|
+
const BLACK: Oklch = { l: 0, c: 0, h: 0 };
|
|
152
|
+
|
|
153
|
+
function oklabDistance(a: Oklch, b: Oklch): number {
|
|
154
|
+
const ar = (a.h * Math.PI) / 180;
|
|
155
|
+
const br = (b.h * Math.PI) / 180;
|
|
156
|
+
const dl = a.l - b.l;
|
|
157
|
+
const da = a.c * Math.cos(ar) - b.c * Math.cos(br);
|
|
158
|
+
const db = a.c * Math.sin(ar) - b.c * Math.sin(br);
|
|
159
|
+
return Math.hypot(dl, da, db);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The token a replace step preselects: nearest by OKLab distance among the
|
|
163
|
+
* palette steps, White, Black and the custom colours other than `exclude`. */
|
|
164
|
+
export function nearestColorToken(color: Oklch, state: EditorState, exclude?: string): string {
|
|
165
|
+
const candidates: [string, Oklch][] = [];
|
|
166
|
+
const values = palettesToValues(state.palettes);
|
|
167
|
+
for (const spec of PALETTE_SPECS) {
|
|
168
|
+
for (const step of PALETTE_STEPS) {
|
|
169
|
+
const name = `--color-${spec.cssNamespace}-${step.label}`;
|
|
170
|
+
const v = values[name];
|
|
171
|
+
if (v?.kind === 'color') candidates.push([name, v]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
candidates.push(['--color-white', WHITE], ['--color-black', BLACK]);
|
|
175
|
+
for (const t of state.customColors.tokens) {
|
|
176
|
+
if (t.variable !== exclude) candidates.push([t.variable, t.color]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let best = candidates[0][0];
|
|
180
|
+
let bestDistance = Infinity;
|
|
181
|
+
for (const [name, value] of candidates) {
|
|
182
|
+
const d = oklabDistance(color, value);
|
|
183
|
+
if (d < bestDistance) {
|
|
184
|
+
best = name;
|
|
185
|
+
bestDistance = d;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return best;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Removes a custom colour that no slot names. Returns false, changing
|
|
192
|
+
* nothing, while any slot still names it; `replaceAndRemoveCustomColor`
|
|
193
|
+
* serves that case. */
|
|
194
|
+
export function removeCustomColor(variable: string): boolean {
|
|
195
|
+
const current = get(store);
|
|
196
|
+
if (!findCustomColor(current, variable) || customColorUsages(current, variable).length > 0) return false;
|
|
197
|
+
mutate(`remove custom colour ${variable}`, (s) => {
|
|
198
|
+
s.customColors.tokens = s.customColors.tokens.filter((t) => t.variable !== variable);
|
|
199
|
+
});
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function replaceInStops(stops: GradientStop[], variable: string, replacement: string): void {
|
|
204
|
+
for (const st of stops) if (st.color === variable) st.color = replacement;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Repoints every slot that names `variable` at `replacement`, keeping each
|
|
208
|
+
* slot's opacity, then removes the custom colour. One undo step. */
|
|
209
|
+
export function replaceAndRemoveCustomColor(variable: string, replacement: string): void {
|
|
210
|
+
if (replacement === variable || !findCustomColor(get(store), variable)) return;
|
|
211
|
+
const pattern = varRefPattern(variable);
|
|
212
|
+
mutate(`replace and remove custom colour ${variable}`, (s) => {
|
|
213
|
+
for (const slice of Object.values(s.components)) {
|
|
214
|
+
for (const ref of Object.values(slice.aliases)) {
|
|
215
|
+
if (ref.kind === 'token' && ref.name === variable) ref.name = replacement;
|
|
216
|
+
else if (ref.kind === 'literal') ref.value = ref.value.replace(pattern, `var(${replacement})`);
|
|
217
|
+
else if (ref.kind === 'gradient') replaceInStops(ref.value.stops, variable, replacement);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const g of s.gradients.tokens) replaceInStops(g.stops, variable, replacement);
|
|
221
|
+
for (const family of WASH_FAMILIES) {
|
|
222
|
+
if (s.washes[family].color === variable) s.washes[family].color = replacement;
|
|
223
|
+
}
|
|
224
|
+
s.customColors.tokens = s.customColors.tokens.filter((t) => t.variable !== variable);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -148,6 +148,13 @@ export interface GradientDiskToken {
|
|
|
148
148
|
stops: { position: number; color: string; opacity?: number }[];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/** On-disk shape of one `--color-custom-N` token: a theme-owned colour that
|
|
152
|
+
* holds its own OKLCH value, one value for light and dark alike. */
|
|
153
|
+
export interface CustomColorDiskToken {
|
|
154
|
+
variable: string;
|
|
155
|
+
color: Oklch;
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
/**
|
|
152
159
|
* Where a live component config resolved from: the unsaved `_working`
|
|
153
160
|
* buffer, or the open theme's embedded copy. Every theme carries every
|
|
@@ -177,6 +184,9 @@ export interface ColorsAndType {
|
|
|
177
184
|
/** Absent on files saved before gradients round-tripped; the loader keeps
|
|
178
185
|
* the stock defaults then, which match what those files rendered. */
|
|
179
186
|
gradients?: GradientDiskToken[];
|
|
187
|
+
/** Absent when the theme holds no custom colour; the loader reads that as
|
|
188
|
+
* an empty list. */
|
|
189
|
+
customColors?: CustomColorDiskToken[];
|
|
180
190
|
/** Four fixed harmony axes, each owning a hue with an optional bound family. */
|
|
181
191
|
harmonyAxes?: HarmonyAxis[];
|
|
182
192
|
/**
|
|
@@ -37,6 +37,34 @@ Editing a palette base ripples through every colour that depends on it, in real
|
|
|
37
37
|
time. Colours use OKLCH, so the ramp stays perceptually even across hues
|
|
38
38
|
without muddy mid-tones.
|
|
39
39
|
|
|
40
|
+
## Custom colours
|
|
41
|
+
|
|
42
|
+
A custom colour is one colour of your own, for a slot no palette step fits.
|
|
43
|
+
The open theme holds it as a token named by number: `--color-custom-1`,
|
|
44
|
+
`--color-custom-2`, and so on.
|
|
45
|
+
|
|
46
|
+
- **Adding.** Every colour picker has a **Custom** row after Danger. Open it
|
|
47
|
+
and click the plus swatch. The new colour starts from the slot's current colour and
|
|
48
|
+
opens in the OKLCH editor, and the slot previews it as you drag. The check
|
|
49
|
+
assigns it to the slot; the cross discards it and restores the slot.
|
|
50
|
+
- **Reusing.** Pick a custom colour from the same row as you pick a palette
|
|
51
|
+
step, opacity included. Every slot that names it follows its value.
|
|
52
|
+
- **Editing.** Click the pencil on a custom swatch in the picker, or
|
|
53
|
+
click **Edit** in the **Custom Colours** section of the Tokens view. The edit
|
|
54
|
+
repaints every slot that names the colour.
|
|
55
|
+
- **Removing.** The bin on a custom swatch in the picker, or **Remove** in
|
|
56
|
+
the Tokens view, deletes a colour nothing names. When a component property,
|
|
57
|
+
gradient stop, or wash names it, the editor first asks for a replacement and
|
|
58
|
+
preselects the nearest colour. **Replace and remove** points each of those
|
|
59
|
+
at the replacement, keeps each one's opacity, and deletes the custom colour.
|
|
60
|
+
|
|
61
|
+
Each add, edit, and remove is one undo step. A custom colour holds one value in
|
|
62
|
+
light and dark mode. Saving writes the theme's custom colours into its file,
|
|
63
|
+
and loading another theme brings in that theme's set. Reach a custom colour
|
|
64
|
+
through a component property: `live-tokens check-page` reports `unknown-token`
|
|
65
|
+
for page CSS that writes `var(--color-custom-1)`, since the name exists only in
|
|
66
|
+
the theme.
|
|
67
|
+
|
|
40
68
|
## Type
|
|
41
69
|
|
|
42
70
|
- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export const docContent: Record<string, string> = {
|
|
5
5
|
"01-overview": "# Overview\n\nLiveTokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button,\n IconButton, Card, Dialog, Table, and more). You style components by changing\n the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Sketch mode](sketch-mode.md)**: redraw the page by hand.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
|
|
6
6
|
"creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Browse the skill atlas\n\nThe package also ships a Skill Atlas component: a diagram of the reasoning\nbehind each bundled skill, with its `SKILL.md` and reference files alongside\nso you can see exactly which lines drive which step. Import it from\n`@motion-proto/live-tokens/skill-atlas` and mount it on a route of your own:\n\n```js\n'/skills': {\n lazy: () => import('@motion-proto/live-tokens/skill-atlas'),\n},\n```\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- A catalogue entry in the runtime file that says what the component is for and\n what to use instead. `npx live-tokens components` prints it, and the skills\n read it when they choose a component for a page.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
|
|
7
|
-
"editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has four views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours\n tell across a page.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n- **Sketchstyle**: an effect layer that redraws the page by hand. See\n [Sketch mode](sketch-mode.md).\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Washes and gradients\n\n- **Scrims** are translucent layers that dim what sits behind them, like the\n one a dialog draws over the page. Set a colour and opacity per stop.\n- **Tints** are the opposite operation: they shade the surface they sit on\n rather than dimming what is behind it, which is what a hover needs.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nPages lay out on the column grid. The column count and the max width hold at\nevery width. Desktop, Tablet, and Phone each set a gutter and an outer margin,\nand the grid takes the tablet values at 767px and narrower and the phone values\nat 480px and narrower. The columns button in the editor bar draws the grid over\nthe page, and a page on the grid reflows as you edit.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
|
|
7
|
+
"editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has four views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours\n tell across a page.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n- **Sketchstyle**: an effect layer that redraws the page by hand. See\n [Sketch mode](sketch-mode.md).\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Custom colours\n\nA custom colour is one colour of your own, for a slot no palette step fits.\nThe open theme holds it as a token named by number: `--color-custom-1`,\n`--color-custom-2`, and so on.\n\n- **Adding.** Every colour picker has a **Custom** row after Danger. Open it\n and click the plus swatch. The new colour starts from the slot's current colour and\n opens in the OKLCH editor, and the slot previews it as you drag. The check\n assigns it to the slot; the cross discards it and restores the slot.\n- **Reusing.** Pick a custom colour from the same row as you pick a palette\n step, opacity included. Every slot that names it follows its value.\n- **Editing.** Click the pencil on a custom swatch in the picker, or\n click **Edit** in the **Custom Colours** section of the Tokens view. The edit\n repaints every slot that names the colour.\n- **Removing.** The bin on a custom swatch in the picker, or **Remove** in\n the Tokens view, deletes a colour nothing names. When a component property,\n gradient stop, or wash names it, the editor first asks for a replacement and\n preselects the nearest colour. **Replace and remove** points each of those\n at the replacement, keeps each one's opacity, and deletes the custom colour.\n\nEach add, edit, and remove is one undo step. A custom colour holds one value in\nlight and dark mode. Saving writes the theme's custom colours into its file,\nand loading another theme brings in that theme's set. Reach a custom colour\nthrough a component property: `live-tokens check-page` reports `unknown-token`\nfor page CSS that writes `var(--color-custom-1)`, since the name exists only in\nthe theme.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Washes and gradients\n\n- **Scrims** are translucent layers that dim what sits behind them, like the\n one a dialog draws over the page. Set a colour and opacity per stop.\n- **Tints** are the opposite operation: they shade the surface they sit on\n rather than dimming what is behind it, which is what a hover needs.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nPages lay out on the column grid. The column count and the max width hold at\nevery width. Desktop, Tablet, and Phone each set a gutter and an outer margin,\nand the grid takes the tablet values at 767px and narrower and the phone values\nat 480px and narrower. The columns button in the editor bar draws the grid over\nthe page, and a page on the grid reflows as you edit.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
|
|
8
8
|
"getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. In the **Theme** panel at the foot of the sidebar, choose **Save As**. Your\n theme appears as JSON under `src/live-tokens/data/themes/`.\n5. Reload. The editor reopens on your theme, so the page returns as you left\n it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, click **Adopt** in the Theme panel. That saves the open theme and bakes\nit into `src/live-tokens/data/tokens.generated.css`, which your build bundles\nalongside `tokens.css`. Adopt is the only action that changes what your site\nships, so try any theme you like first. The editor itself never reaches\nproduction.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
|
|
9
9
|
"light-and-dark": "# Light and dark\n\nSome things on a page cannot be written as a token. A wordmark drawn in white\ndisappears on a pale theme. Ink that multiplies onto paper vanishes on a dark\none. A photograph behind a headline is dark no matter what the palette says.\n\nEach of those needs the same fact first: which way does the surface behind this\nthing lean? One attribute carries it.\n\n## The attribute\n\n`data-backdrop` is either `light` or `dark`, and it does two things at once: it\nselects, so a rule can key on it, and it sets `color-scheme`, so every\n`light-dark()` under it resolves the half that reads.\n\n```css\n.title {\n color: light-dark(var(--color-black), var(--color-white));\n}\n```\n\nThat line is right on both sides of the theme, and it is right inside a dark\nsection on a pale page, because the nearest `color-scheme` wins.\n\n## Stating it\n\nPut it in the markup when the surface knows its own tone — a hero over a\nphotograph, a plate that stays pale in every theme:\n\n```svelte\n<div class=\"hero-panel\" data-backdrop=\"dark\">\n```\n\nA stated tone beats any measurement, and it inherits, so everything inside the\npanel resolves against it.\n\n## Measuring it\n\nWhere the tone is a property of the theme rather than of the markup, let it be\nmeasured:\n\n```svelte\n<script>\n import { backdrop } from '@motion-proto/live-tokens/backdrop';\n</script>\n\n<section use:backdrop>\n```\n\nThe action reads whatever actually paints behind the element — the nearest\nancestor with an opaque fill, averaged across its gradient stops, falling back\nto the theme's `--page-bg` — and stamps the answer. It re-reads when the theme\nchanges, which the editor does by rewriting custom properties with no reload,\nso the stamp follows a live edit.\n\nThe page itself is stamped for you: the build bakes the production theme's\npolarity into `tokens.generated.css`, so the first paint is already right, and\n`syncDocumentBackdrop()` keeps `<html>` current as themes switch.\n\n```ts\nimport { syncDocumentBackdrop } from '@motion-proto/live-tokens/backdrop';\n\nsyncDocumentBackdrop();\n```\n\n## Reading it from JavaScript\n\nAnything that paints outside CSS — a canvas, a WebGL uniform, an `<img>` that\ncomes in two versions — asks the same question through the same module:\n\n```ts\nimport { isLightBackdrop, watchBackdrop, cssColorToHex } from '@motion-proto/live-tokens/backdrop';\n\nconst stop = watchBackdrop(logoEl, {\n stamp: false,\n onChange: (polarity) => (src = polarity === 'light' ? darkMark : lightMark),\n});\n```\n\n`isLightBackdrop(el)` answers once. `watchBackdrop` keeps answering and returns\na stop function. `cssColorToHex` resolves any CSS colour — including the\n`oklch()` a token holds — to a hex a non-CSS consumer can take.\n\n## What it does not do\n\nPolarity is a property of a surface, not of a component, so nothing is stamped\nfor you below `<html>`: a section that needs an answer either states one or asks\nfor one. And a measurement reads the paint at the moment it runs — an element\nthat scrolls from a pale section onto a dark one keeps the answer it was given.\nState the tone on each section instead.\n",
|
|
10
10
|
"production-css": "# Production CSS\n\nIn dev, every design token sits on `:root` as a CSS variable, because the\neditor can rewrite any of them while the page runs. A production build runs no\neditor, so most of those values never change. `vite build` rewrites the CSS it\nemits to match: a token stays a variable only where its value can change at\nruntime, and every other `var()` gives way to the value it reads.\n\nThe dev server keeps every variable. Your source files keep every token too:\nthe build rewrites only the files it emits, so `tokens.css`,\n`tokens.generated.css` and your components still name each token, and the\ntoken names stay the API you write against.\n\n## The build step\n\n`themeFileApi` runs the step at the end of every `vite build`, after Vite has\nbundled and minified the CSS. It reads every stylesheet and script in the\nbundle, including lazy chunks, and then:\n\n- **removes** each custom property that no rule reads, directly or through\n another property;\n- **substitutes** each `var()` that reads a static property with the text of\n that property's value, so `calc()` and `color-mix()` still resolve on the\n element that uses them;\n- **declares** each remaining property once on `:root`, with the value that\n wins the cascade, so DevTools lists it once.\n\nThe build prints one line with the result:\n\n```\nlive-tokens: the production CSS declares 76 of 848 custom properties and is 61.3 kB smaller.\n```\n\nA file's hashed name follows its content. Each stylesheet the build rewrites\ntakes a new hash, and so does each script that loads it, so a cache that keeps\nhashed files forever serves each release its own files. A file name pattern\nwithout `[hash]` keeps every name.\n\n## The dynamic rule\n\nA property stays a variable when its value can change while the page runs, or\nwhen the build cannot prove it never does. A property that changes and loses\nits variable breaks a page, and an extra variable costs a few bytes, so a case\nthat could fall either way stays a variable. A property is **dynamic** when any\nof these hold:\n\n- A rule declares it under a selector besides a root selector such as `:root`\n or `:root:root`, or inside `@media`, `@supports` or `@container`. The size\n tokens and the column gutter and margin in the three responsive `@media`\n blocks of `tokens.css` stay variables this way.\n- Its name appears in a script or HTML page the build emits. That covers\n `style:--name` directives, `setProperty`, `getPropertyValue` and inline\n `style` attributes. Card sets `--card-hover-border-enabled` through a\n `style:` directive, so the token stays a variable.\n- Its name appears in an at-rule's prelude, such as a `style()` container\n query.\n- An `@property` rule registers it.\n- The `keep` option lists it.\n- Its value reads a dynamic property.\n- Its winning value is a CSS-wide keyword such as `initial`, or it reads\n itself through a loop of `var()` references.\n- Two stylesheets give it different `:root` values.\n- Its value reads a name that no stylesheet declares and no script or `keep`\n entry names.\n- Only a lazily loaded stylesheet declares it, and another stylesheet reads\n it.\n\nEvery other property is **static**: one value on `:root` for the life of the\npage.\n\n## Computed names\n\nThe build finds a name in a script only when the script spells it out. A\nscript that assembles a name while the page runs, such as\n`` `--color-${family}-500` ``, hides it. The build then substitutes or removes\nthat token, so a read returns an empty string and a write changes nothing. List\neach such name under `keep`:\n\n```ts\nthemeFileApi({\n tokensCssPath: 'src/system/styles/tokens.css',\n productionCss: { keep: ['--color-brand-500', '--color-accent-500'] },\n}),\n```\n\n`keep` takes exact names. The same goes for a stylesheet outside the bundle,\nsuch as one in `public/`: the build never reads it, so list each token it\nreads under `keep`.\n\n## Options\n\n`productionCss` on `themeFileApi` sets the step:\n\n- **Default.** The build removes, substitutes and declares, as above.\n- **`productionCss: { keep: [...] }`.** Each listed name stays a variable.\n- **`productionCss: { substitute: false }`.** The build removes properties no\n rule reads and keeps every other `var()`, so DevTools shows token names while\n you debug. It still declares each property once on `:root`.\n- **`productionCss: false`.** The build keeps every variable, as dev does.\n\n`keep` and `substitute` combine in one object. When a page renders differently\nin production, build it with `productionCss: false`. A difference that goes\naway points at a token the rule missed, and `keep` fixes it.\n\n## Limits\n\n- **Values a property rejects.** In dev, `width: var(--color-brand-500)` passes\n the parser, and when the color reaches `width` the browser treats the\n declaration as `unset`. After substitution, `width` holds a color, the parser\n drops the declaration, and an earlier `width` rule applies. Keep each token's\n value valid for every property that reads it.\n- **Fixed stylesheet names.** The build renames each stylesheet it rewrites\n through the hash in the build's `assetFileNames` pattern. A plugin that emits\n a stylesheet under a fixed `fileName` breaks that pattern, and the build\n stops with `live-tokens: <file> does not follow the output pattern\n \"<pattern>\"`. Set `productionCss: false` for that build.\n",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
{ id: 'icon-sizes', label: 'Icon Sizes', icon: 'fas fa-expand' },
|
|
25
25
|
{ id: 'shadows', label: 'Shadows', icon: 'fas fa-cloud' },
|
|
26
26
|
{ id: 'washes', label: 'Washes', icon: 'fas fa-layer-group' },
|
|
27
|
+
{ id: 'custom-colors', label: 'Custom Colours', icon: 'fas fa-eye-dropper' },
|
|
27
28
|
{ id: 'gradients', label: 'Gradients', icon: 'fas fa-droplet' },
|
|
28
29
|
{ id: 'utility-tokens', label: 'Utility Tokens', icon: 'fas fa-sliders' }
|
|
29
30
|
];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
let replaceVarSeq = 0;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { onDestroy, untrack } from 'svelte';
|
|
7
|
+
import { get } from 'svelte/store';
|
|
8
|
+
import {
|
|
9
|
+
editorState,
|
|
10
|
+
customColorUsages,
|
|
11
|
+
describeCustomColorUsages,
|
|
12
|
+
nearestColorToken,
|
|
13
|
+
} from '../core/store/editorStore';
|
|
14
|
+
import { setCssVar, removeCssVar } from '../core/cssVarSync';
|
|
15
|
+
import type { Oklch } from '../core/palettes/oklch';
|
|
16
|
+
import UIPaletteSelector from './UIPaletteSelector.svelte';
|
|
17
|
+
import UIPillButton from './UIPillButton.svelte';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** The custom colour that remove would delete. */
|
|
21
|
+
variable: string;
|
|
22
|
+
/** The colour the stand-in is chosen nearest to. */
|
|
23
|
+
color: Oklch;
|
|
24
|
+
onconfirm: (replacement: string) => void;
|
|
25
|
+
oncancel: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let { variable, color, onconfirm, oncancel }: Props = $props();
|
|
29
|
+
|
|
30
|
+
// The step is a snapshot of the theme at the moment remove was asked for;
|
|
31
|
+
// the parent mounts a fresh step for each remove.
|
|
32
|
+
const usages = untrack(() => customColorUsages(get(editorState), variable));
|
|
33
|
+
const nearest = untrack(() => nearestColorToken(color, get(editorState), variable));
|
|
34
|
+
let replacement = $state(nearest);
|
|
35
|
+
let usageLine = $state(describeCustomColorUsages(usages, (id) => id));
|
|
36
|
+
|
|
37
|
+
const replaceVar = `--__custom-replace-${++replaceVarSeq}`;
|
|
38
|
+
// Written before the nested picker mounts, so it opens on the preselection.
|
|
39
|
+
setCssVar(replaceVar, `var(${nearest})`);
|
|
40
|
+
onDestroy(() => removeCssVar(replaceVar));
|
|
41
|
+
|
|
42
|
+
// Loaded on demand: the registry imports every component editor, and those
|
|
43
|
+
// editors import the picker that imports this step.
|
|
44
|
+
import('../component-editor/registry').then(({ getComponentRegistry }) => {
|
|
45
|
+
const registry = getComponentRegistry();
|
|
46
|
+
usageLine = describeCustomColorUsages(usages, (id) => registry[id]?.label ?? id);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function write(value: string | null) {
|
|
50
|
+
if (!value?.startsWith('--')) return;
|
|
51
|
+
replacement = value;
|
|
52
|
+
setCssVar(replaceVar, `var(${value})`);
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="replace-step">
|
|
57
|
+
<p class="replace-usage">{usageLine}</p>
|
|
58
|
+
<div class="replace-pick">
|
|
59
|
+
<span class="replace-label">Replace with</span>
|
|
60
|
+
<div class="replace-picker">
|
|
61
|
+
<UIPaletteSelector
|
|
62
|
+
variable={replaceVar}
|
|
63
|
+
replacing={variable}
|
|
64
|
+
showNone={false}
|
|
65
|
+
showOpacity={false}
|
|
66
|
+
onwrite={write}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="replace-actions">
|
|
71
|
+
<UIPillButton size="compact" variant="primary" onclick={() => onconfirm(replacement)}>Replace and remove</UIPillButton>
|
|
72
|
+
<UIPillButton size="compact" variant="outline" onclick={oncancel}>Cancel</UIPillButton>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<style>
|
|
77
|
+
.replace-step {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: var(--ui-space-8);
|
|
81
|
+
padding: var(--ui-space-12);
|
|
82
|
+
background: var(--ui-surface-lowest);
|
|
83
|
+
border: 1px solid var(--ui-border-low);
|
|
84
|
+
border-radius: var(--ui-radius-md);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.replace-usage {
|
|
88
|
+
margin: 0;
|
|
89
|
+
font-size: var(--ui-font-size-sm);
|
|
90
|
+
color: var(--ui-text-primary);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.replace-pick {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: var(--ui-space-8);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.replace-label {
|
|
100
|
+
font-size: var(--ui-font-size-xs);
|
|
101
|
+
color: var(--ui-text-secondary);
|
|
102
|
+
flex-shrink: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* The nested picker lays itself out on its parent row's subgrid; this grid
|
|
106
|
+
stands in for that row. */
|
|
107
|
+
.replace-picker {
|
|
108
|
+
flex: 1;
|
|
109
|
+
min-width: 0;
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-columns: minmax(3rem, 1fr) auto;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.replace-actions {
|
|
115
|
+
display: flex;
|
|
116
|
+
gap: var(--ui-space-6);
|
|
117
|
+
}
|
|
118
|
+
</style>
|