@motion-proto/live-tokens 0.49.1 → 0.52.1
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 +72 -0
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/{chunk-76TFDTJO.js → chunk-JIGIIE5P.js} +60 -132
- package/dist-plugin/{chunk-44RSTAII.js → chunk-TKZBVIW5.js} +33 -5
- package/dist-plugin/chunk-YR6GPXW2.js +129 -0
- package/dist-plugin/generateColorsAndType/index.cjs +100 -33
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.js +35 -14
- package/dist-plugin/index.cjs +95 -55
- package/dist-plugin/index.js +5 -3
- package/dist-plugin/{themeTypes-DSwKq-bj.d.cts → themeTypes-Dky03HL9.d.cts} +18 -5
- package/dist-plugin/{themeTypes-DSwKq-bj.d.ts → themeTypes-Dky03HL9.d.ts} +18 -5
- package/dist-plugin/tokensCssMigrations/index.cjs +71 -5
- package/dist-plugin/tokensCssMigrations/index.d.cts +20 -9
- package/dist-plugin/tokensCssMigrations/index.d.ts +20 -9
- package/dist-plugin/tokensCssMigrations/index.js +4 -1
- package/package.json +8 -1
- package/src/editor/bootstrap.ts +2 -0
- package/src/editor/component-editor/ButtonEditor.svelte +1 -1
- package/src/editor/component-editor/CardEditor.svelte +5 -2
- package/src/editor/component-editor/IconButtonEditor.svelte +1 -1
- package/src/editor/component-editor/ImageEditor.svelte +1 -1
- package/src/editor/component-editor/NotificationEditor.svelte +6 -0
- package/src/editor/component-editor/SectionDividerEditor.svelte +4 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +6 -0
- package/src/editor/component-editor/TabBarEditor.svelte +1 -5
- package/src/editor/component-editor/scaffolding/ComponentEditorBase.svelte +8 -1
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -32
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +2 -0
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +3 -1
- package/src/editor/component-editor/scaffolding/types.ts +3 -0
- package/src/editor/core/components/componentConfigService.ts +25 -0
- package/src/editor/core/cssVarSync.ts +65 -7
- package/src/editor/core/fonts/fontLoader.ts +12 -3
- package/src/editor/core/fonts/fontWeightAvailability.ts +97 -0
- package/src/editor/core/palettes/oklch.ts +3 -137
- package/src/editor/core/palettes/paletteDerivation.ts +79 -37
- package/src/editor/core/preview/lookPreview.ts +23 -18
- package/src/editor/core/store/editorRenderer.ts +22 -7
- package/src/editor/core/store/editorStore.ts +31 -0
- package/src/editor/core/store/gradientSource.ts +5 -0
- package/src/editor/core/themes/colorsAndTypeService.ts +2 -12
- package/src/editor/core/themes/generateColorsAndType.ts +18 -8
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +4 -1
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +4 -1
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +8 -0
- package/src/editor/core/themes/slices/fonts.ts +4 -5
- package/src/editor/core/themes/themeDocumentSync.ts +66 -0
- package/src/editor/core/themes/themeInit.ts +7 -10
- package/src/editor/core/themes/themeService.ts +10 -3
- package/src/editor/core/themes/themeTypes.ts +18 -6
- package/src/editor/docs/content/editing-tokens.md +6 -2
- package/src/editor/docs/content/themes-workflow.md +14 -11
- package/src/editor/docs/content/where-themes-live.md +2 -2
- package/src/editor/docs/content.generated.ts +3 -3
- package/src/editor/ui/BezierCurveEditor.svelte +90 -67
- package/src/editor/ui/FileLoadList.svelte +1 -0
- package/src/editor/ui/FontStackEditor.svelte +1 -2
- package/src/editor/ui/GradientEditor.svelte +5 -1
- package/src/editor/ui/PaletteEditor.svelte +150 -44
- package/src/editor/ui/ProjectFontsSection.svelte +0 -5
- package/src/editor/ui/ThemePanel.svelte +206 -42
- package/src/editor/ui/Toggle.svelte +14 -2
- package/src/editor/ui/UIDialog.svelte +7 -1
- package/src/editor/ui/UIFontFamilySelector.svelte +13 -13
- package/src/editor/ui/UIFontSizeSelector.svelte +5 -5
- package/src/editor/ui/UIFontWeightSelector.svelte +52 -2
- package/src/editor/ui/UIOptionItem.svelte +14 -1
- package/src/editor/ui/UIPaddingSelector.svelte +4 -3
- package/src/editor/ui/UIPaletteSelector.svelte +20 -13
- package/src/editor/ui/UIRelinkConfirmDialog.svelte +18 -3
- package/src/editor/ui/UITextTransformSelector.svelte +5 -5
- package/src/editor/ui/UITokenSelector.svelte +29 -7
- package/src/editor/ui/colors/ColorStory.svelte +12 -8
- package/src/editor/ui/curveEngine.ts +101 -21
- package/src/editor/ui/palette/OverridesPanel.svelte +19 -5
- package/src/editor/ui/palette/PaletteBase.svelte +17 -78
- package/src/editor/ui/palette/ScaleCurveEditor.svelte +109 -20
- package/src/editor/ui/palette/curveSummary.ts +20 -0
- package/src/editor/ui/palette/dockMagnify.ts +5 -13
- package/src/editor/ui/palette/paletteMath.ts +7 -4
- package/src/live-tokens/data/colors-and-type/autumn.json +528 -528
- package/src/live-tokens/data/colors-and-type/default.json +247 -247
- package/src/live-tokens/data/colors-and-type/halloween.json +532 -532
- package/src/live-tokens/data/colors-and-type/midnight-study.json +538 -538
- package/src/live-tokens/data/colors-and-type/ocean.json +521 -521
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +532 -532
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +504 -504
- package/src/live-tokens/data/colors-and-type/sunset.json +531 -531
- package/src/live-tokens/data/themes/autumn.json +527 -527
- package/src/live-tokens/data/themes/halloween.json +531 -531
- package/src/live-tokens/data/themes/midnight-study.json +537 -537
- package/src/live-tokens/data/themes/ocean.json +520 -520
- package/src/live-tokens/data/themes/royal-velvet.json +531 -531
- package/src/live-tokens/data/themes/spring-meadow.json +516 -514
- package/src/live-tokens/data/themes/sunset.json +531 -531
- package/src/live-tokens/data/tokens.generated.css +282 -282
- package/src/system/components/Button.svelte +2 -1
- package/src/system/components/FloatingTokenTags.css +2 -2
- package/src/system/components/FloatingTokenTags.svelte +11 -0
- package/src/system/components/IconButton.svelte +2 -1
- package/src/system/components/Image.svelte +9 -3
- package/src/system/components/ImageLightbox.svelte +2 -0
- package/src/system/components/SectionDivider.svelte +20 -2
- package/src/system/components/SideNavigation.svelte +15 -1
- package/src/system/internal/backgroundContrast.ts +7 -0
- package/src/system/internal/oklch.ts +189 -0
- package/src/system/styles/fonts.css +6 -6
- package/src/system/styles/tokens.css +283 -283
|
@@ -22,8 +22,12 @@
|
|
|
22
22
|
exportTheme,
|
|
23
23
|
importTheme,
|
|
24
24
|
} from '../core/themes/themeService';
|
|
25
|
+
import {
|
|
26
|
+
THEME_APPLIED_EVENT,
|
|
27
|
+
type AppliedThemeDetail,
|
|
28
|
+
} from '../core/themes/themeDocumentSync';
|
|
25
29
|
import { countComponentsOffLook, lookProductionState } from '../core/themes/lookSummary';
|
|
26
|
-
import { previewTheme, previewColorsAndType, revertPreview } from '../core/preview/lookPreview';
|
|
30
|
+
import { commitPreview, previewTheme, previewColorsAndType, revertPreview } from '../core/preview/lookPreview';
|
|
27
31
|
import {
|
|
28
32
|
deleteColorsAndType,
|
|
29
33
|
hydrateColorsAndType,
|
|
@@ -41,10 +45,17 @@
|
|
|
41
45
|
} from '../core/themes/loadRows';
|
|
42
46
|
import {
|
|
43
47
|
listComponents,
|
|
48
|
+
componentConfigFromState,
|
|
49
|
+
writeWorkingComponentConfig,
|
|
44
50
|
type ComponentSummary,
|
|
45
51
|
} from '../core/components/componentConfigService';
|
|
46
52
|
import { fontPairingLabel } from '../core/fonts/fontPairing';
|
|
47
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
componentDirty,
|
|
55
|
+
editorState,
|
|
56
|
+
colorsAndTypeDirty,
|
|
57
|
+
markComponentSaved,
|
|
58
|
+
} from '../core/store/editorStore';
|
|
48
59
|
import { openThemeSlug } from '../core/store/editorConfigStore';
|
|
49
60
|
import { editorView } from '../core/store/editorViewStore';
|
|
50
61
|
import {
|
|
@@ -53,12 +64,14 @@
|
|
|
53
64
|
bumpProductionRevision,
|
|
54
65
|
liveMovedSinceBake,
|
|
55
66
|
productionTheme,
|
|
67
|
+
bumpComponentActiveRevision,
|
|
56
68
|
} from '../core/productionPulse';
|
|
57
69
|
import { flashStatus } from '../core/flashStatus';
|
|
58
70
|
import UIInfoPopover from './UIInfoPopover.svelte';
|
|
59
71
|
import UIPillButton from './UIPillButton.svelte';
|
|
60
72
|
import FileLoadList from './FileLoadList.svelte';
|
|
61
73
|
import FilePill from './FilePill.svelte';
|
|
74
|
+
import UIDialog from './UIDialog.svelte';
|
|
62
75
|
import SaveAsDialog from '../component-editor/scaffolding/SaveAsDialog.svelte';
|
|
63
76
|
|
|
64
77
|
interface Props {
|
|
@@ -75,6 +88,9 @@
|
|
|
75
88
|
let rows = $derived(buildLoadRows(files, colorsFiles));
|
|
76
89
|
let showFileList = $state(false);
|
|
77
90
|
let saveAsDialog = $state(false);
|
|
91
|
+
let saveComponentsDialog = $state(false);
|
|
92
|
+
let pendingComponentSave: (() => Promise<void>) | null = null;
|
|
93
|
+
let pendingComponentCount = $state(0);
|
|
78
94
|
let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
|
|
79
95
|
|
|
80
96
|
let currentDisplayName = $state('Motion Proto');
|
|
@@ -171,6 +187,19 @@
|
|
|
171
187
|
await refreshProduction();
|
|
172
188
|
});
|
|
173
189
|
|
|
190
|
+
// A consumer can load a theme from the host while this iframe stays open.
|
|
191
|
+
// The cross-document bridge hydrates this document's typed store; keep the
|
|
192
|
+
// panel's local identity/summary state on that same payload as well.
|
|
193
|
+
onMount(() => {
|
|
194
|
+
const handleThemeApplied = (event: Event) => {
|
|
195
|
+
const { result } = (event as CustomEvent<AppliedThemeDetail>).detail;
|
|
196
|
+
currentDisplayName = result.theme.name;
|
|
197
|
+
lookConfigs = result.theme.componentConfigs;
|
|
198
|
+
};
|
|
199
|
+
document.addEventListener(THEME_APPLIED_EVENT, handleThemeApplied);
|
|
200
|
+
return () => document.removeEventListener(THEME_APPLIED_EVENT, handleThemeApplied);
|
|
201
|
+
});
|
|
202
|
+
|
|
174
203
|
// Re-read whenever an Adopt fires, here or in a component editor: it saves
|
|
175
204
|
// the open theme and moves the production pointer, so the identity, the
|
|
176
205
|
// component summary and the production state shown here all need to track.
|
|
@@ -188,10 +217,10 @@
|
|
|
188
217
|
refreshComponents();
|
|
189
218
|
});
|
|
190
219
|
|
|
191
|
-
// A capture reads the server's live state, so
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
220
|
+
// A capture reads the server's live state, so colors and type go to their
|
|
221
|
+
// buffer first. Component editors retain their own save boundary; when any
|
|
222
|
+
// are dirty, the theme panel offers to flush all of them in one explicit
|
|
223
|
+
// step before continuing.
|
|
195
224
|
//
|
|
196
225
|
// The gate is `colorsAndTypeDirty`, not history: history counts every entry,
|
|
197
226
|
// components included, so it would flush the colors over component-only work.
|
|
@@ -200,20 +229,45 @@
|
|
|
200
229
|
await persistColorsAndType(get(editorState), currentDisplayName);
|
|
201
230
|
}
|
|
202
231
|
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
232
|
+
async function flushComponents(displayName = currentDisplayName): Promise<void> {
|
|
233
|
+
const dirtyComponents = Object.entries(get(componentDirty))
|
|
234
|
+
.filter(([, dirty]) => dirty)
|
|
235
|
+
.map(([component]) => component);
|
|
236
|
+
if (dirtyComponents.length === 0) return;
|
|
237
|
+
|
|
238
|
+
const state = get(editorState);
|
|
239
|
+
await Promise.all(dirtyComponents.map((component) =>
|
|
240
|
+
writeWorkingComponentConfig(
|
|
241
|
+
component,
|
|
242
|
+
componentConfigFromState(state, component, displayName),
|
|
243
|
+
),
|
|
244
|
+
));
|
|
245
|
+
for (const component of dirtyComponents) markComponentSaved(component);
|
|
246
|
+
bumpComponentActiveRevision();
|
|
210
247
|
}
|
|
211
248
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
249
|
+
function continueWithComponentChoice(action: () => Promise<void>): void {
|
|
250
|
+
const dirtyCount = Object.values(get(componentDirty)).filter(Boolean).length;
|
|
251
|
+
if (dirtyCount === 0) {
|
|
252
|
+
void action();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
pendingComponentCount = dirtyCount;
|
|
256
|
+
pendingComponentSave = action;
|
|
257
|
+
saveComponentsDialog = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function confirmSaveAllComponents(): Promise<void> {
|
|
261
|
+
const action = pendingComponentSave;
|
|
262
|
+
pendingComponentSave = null;
|
|
263
|
+
saveComponentsDialog = false;
|
|
264
|
+
if (action) await action();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function runSave(saveComponents: boolean) {
|
|
215
268
|
saveStatus = 'saving';
|
|
216
269
|
try {
|
|
270
|
+
if (saveComponents) await flushComponents();
|
|
217
271
|
await flushColors();
|
|
218
272
|
await saveActiveTheme(currentDisplayName);
|
|
219
273
|
await refreshActive();
|
|
@@ -223,10 +277,21 @@
|
|
|
223
277
|
}
|
|
224
278
|
}
|
|
225
279
|
|
|
280
|
+
function handleSave() {
|
|
281
|
+
if (activeIsProtected) return;
|
|
282
|
+
continueWithComponentChoice(() => runSave(true));
|
|
283
|
+
}
|
|
284
|
+
|
|
226
285
|
function openSaveAs() {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
286
|
+
continueWithComponentChoice(async () => {
|
|
287
|
+
try {
|
|
288
|
+
await flushComponents();
|
|
289
|
+
showFileList = false;
|
|
290
|
+
saveAsDialog = true;
|
|
291
|
+
} catch {
|
|
292
|
+
flashStatus(setSaveStatus, 'error');
|
|
293
|
+
}
|
|
294
|
+
});
|
|
230
295
|
}
|
|
231
296
|
|
|
232
297
|
async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
|
|
@@ -266,10 +331,9 @@
|
|
|
266
331
|
return 'Save this theme and ship it to production';
|
|
267
332
|
});
|
|
268
333
|
|
|
269
|
-
|
|
334
|
+
function handleAdopt() {
|
|
270
335
|
if (production.inProduction || adoptStatus === 'adopting') return;
|
|
271
|
-
|
|
272
|
-
await runAdopt();
|
|
336
|
+
continueWithComponentChoice(() => runAdopt(true));
|
|
273
337
|
}
|
|
274
338
|
|
|
275
339
|
/**
|
|
@@ -278,9 +342,10 @@
|
|
|
278
342
|
* Adopt, and which file holds the look is bookkeeping they should not have
|
|
279
343
|
* to think about, so it forks to a theme of their own first.
|
|
280
344
|
*/
|
|
281
|
-
async function runAdopt() {
|
|
345
|
+
async function runAdopt(saveComponents = false) {
|
|
282
346
|
adoptStatus = 'adopting';
|
|
283
347
|
try {
|
|
348
|
+
if (saveComponents) await flushComponents();
|
|
284
349
|
await flushColors();
|
|
285
350
|
if (activeIsProtected) {
|
|
286
351
|
const taken = new Set((await listThemes()).map((m) => m.fileName));
|
|
@@ -329,6 +394,13 @@
|
|
|
329
394
|
previewLayer = null;
|
|
330
395
|
}
|
|
331
396
|
|
|
397
|
+
function acceptPreview() {
|
|
398
|
+
commitPreview();
|
|
399
|
+
previewRow = null;
|
|
400
|
+
previewLook = null;
|
|
401
|
+
previewLayer = null;
|
|
402
|
+
}
|
|
403
|
+
|
|
332
404
|
/** Paint whatever is selected, through the engine the mode calls for. A
|
|
333
405
|
* theme's colors and type are the slice embedded in it, so one look
|
|
334
406
|
* previews either way. */
|
|
@@ -386,26 +458,54 @@
|
|
|
386
458
|
if (!row) return;
|
|
387
459
|
if (unsavedEdits) {
|
|
388
460
|
const ok = window.confirm(
|
|
389
|
-
'Loading a theme will
|
|
461
|
+
'Loading a theme will replace the editor’s current state and discard unsaved changes. Continue?',
|
|
390
462
|
);
|
|
391
463
|
if (!ok) return;
|
|
392
464
|
}
|
|
393
|
-
// The
|
|
394
|
-
//
|
|
465
|
+
// The exact theme being opened is already painted. Hand that preview to
|
|
466
|
+
// the store load without restoring the old look across the request.
|
|
467
|
+
acceptPreview();
|
|
468
|
+
let result: Awaited<ReturnType<typeof applyTheme>>;
|
|
395
469
|
try {
|
|
396
|
-
|
|
470
|
+
result = await applyTheme(row.slug);
|
|
397
471
|
if (result.skippedComponents.length > 0) {
|
|
398
472
|
window.alert(
|
|
399
473
|
`Loaded "${row.name}". These components are not installed here, so their `
|
|
400
474
|
+ `saved settings were skipped:\n\n${result.skippedComponents.join(', ')}`,
|
|
401
475
|
);
|
|
402
476
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
477
|
+
currentDisplayName = result.theme.name;
|
|
478
|
+
lookConfigs = result.theme.componentConfigs;
|
|
479
|
+
showFileList = false;
|
|
406
480
|
} catch (err) {
|
|
407
481
|
window.alert(`Failed to load theme: ${(err as Error).message}`);
|
|
482
|
+
// The selection remains visible after a failed request; put its preview
|
|
483
|
+
// back so the dialog and page continue to agree.
|
|
484
|
+
previewRow = row;
|
|
485
|
+
previewLook = row.kind === 'look' ? await loadTheme(row.slug).catch(() => null) : null;
|
|
486
|
+
if (previewLook) await repaint();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// The picker is a one-step choose-and-ship flow. Package/user presets are
|
|
491
|
+
// already saved documents, so adopting can publish them directly. The
|
|
492
|
+
// protected baseline cannot be production's editable document; mirror the
|
|
493
|
+
// normal Adopt behavior and fork it first when it is selected.
|
|
494
|
+
adoptStatus = 'adopting';
|
|
495
|
+
try {
|
|
496
|
+
if (row.slug === 'default') {
|
|
497
|
+
const taken = new Set((await listThemes()).map((m) => m.fileName));
|
|
498
|
+
await saveAsTheme(freshName('my-theme', taken), 'My Theme');
|
|
499
|
+
await refreshActive();
|
|
500
|
+
}
|
|
501
|
+
await adoptLook();
|
|
502
|
+
bumpProductionRevision();
|
|
503
|
+
flashStatus(setAdoptStatus, 'done');
|
|
504
|
+
} catch (err) {
|
|
505
|
+
window.alert(`Theme loaded, but could not be adopted: ${(err as Error).message}`);
|
|
506
|
+
flashStatus(setAdoptStatus, 'error', { durationMs: 3000 });
|
|
408
507
|
}
|
|
508
|
+
await Promise.all([refreshFiles(), refreshComponents(), refreshProduction()]);
|
|
409
509
|
}
|
|
410
510
|
|
|
411
511
|
/**
|
|
@@ -554,6 +654,11 @@
|
|
|
554
654
|
if (showFileList) refreshFiles();
|
|
555
655
|
}
|
|
556
656
|
|
|
657
|
+
function openThemePicker() {
|
|
658
|
+
showFileList = true;
|
|
659
|
+
refreshFiles();
|
|
660
|
+
}
|
|
661
|
+
|
|
557
662
|
function rowBadge(row: LoadRow) {
|
|
558
663
|
return row.kind === 'layer'
|
|
559
664
|
? { label: 'colors & type', title: 'Holds colors and type only, no shapes' }
|
|
@@ -575,7 +680,7 @@
|
|
|
575
680
|
<strong>Load</strong> opens the list. Picking a theme shows it on the page as a preview, so you can try each look with nothing written to disk. Pick another to compare, or <strong>Cancel</strong> to go back to where you were.
|
|
576
681
|
</p>
|
|
577
682
|
<p>
|
|
578
|
-
<strong>Save</strong> opens the previewed theme: the editor works on it from then on
|
|
683
|
+
<strong>Save</strong> opens and adopts the previewed theme: the editor works on it from then on and production ships it immediately. Components it does not carry go back to their defaults. Previewing and cancelling never change what your site ships.
|
|
579
684
|
</p>
|
|
580
685
|
<p>
|
|
581
686
|
<strong>Colors and type only</strong> in that window takes the palette and the fonts and leaves your shapes and component settings alone.
|
|
@@ -584,7 +689,7 @@
|
|
|
584
689
|
The <strong>active</strong> theme is the one the editor has open. <strong>Adopt</strong> saves it and ships it to production, colors and type plus every component you changed. The line under the name says whether production is running this theme.
|
|
585
690
|
</p>
|
|
586
691
|
<p>
|
|
587
|
-
|
|
692
|
+
If components have unsaved edits, Save and Adopt offer to save all of them before continuing. You can cancel to review or save components individually.
|
|
588
693
|
</p>
|
|
589
694
|
<p>
|
|
590
695
|
<strong>Motion Proto</strong> is the protected baseline. To start customizing, <strong>Save As</strong> a new theme first.
|
|
@@ -603,13 +708,21 @@
|
|
|
603
708
|
</span>
|
|
604
709
|
{/if}
|
|
605
710
|
</div>
|
|
606
|
-
<
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
title=
|
|
611
|
-
|
|
612
|
-
|
|
711
|
+
<button
|
|
712
|
+
type="button"
|
|
713
|
+
class="theme-name-trigger"
|
|
714
|
+
onclick={openThemePicker}
|
|
715
|
+
title="Open the Theme Picker"
|
|
716
|
+
aria-label={`Open Theme Picker. Current theme: ${currentDisplayName}`}
|
|
717
|
+
>
|
|
718
|
+
<FilePill
|
|
719
|
+
name={currentDisplayName}
|
|
720
|
+
isProtected={activeIsProtected}
|
|
721
|
+
protectedTitle="Protected default theme"
|
|
722
|
+
title={currentDisplayName}
|
|
723
|
+
style="display: flex;"
|
|
724
|
+
/>
|
|
725
|
+
</button>
|
|
613
726
|
<span class="mfm-prod-status" class:applied={production.inProduction}>
|
|
614
727
|
<i class="mfm-status-dot" aria-hidden="true"></i>
|
|
615
728
|
<span>
|
|
@@ -668,7 +781,7 @@
|
|
|
668
781
|
class="mfm-btn mfm-btn-row"
|
|
669
782
|
class:active={showFileList}
|
|
670
783
|
onclick={toggleFileList}
|
|
671
|
-
title="
|
|
784
|
+
title="Open the Theme Picker"
|
|
672
785
|
>
|
|
673
786
|
<i class="fas fa-folder-open"></i>
|
|
674
787
|
<span>Load…</span>
|
|
@@ -741,13 +854,29 @@
|
|
|
741
854
|
style="display: none;"
|
|
742
855
|
/>
|
|
743
856
|
|
|
857
|
+
<UIDialog
|
|
858
|
+
bind:show={saveComponentsDialog}
|
|
859
|
+
title="Unsaved component edits"
|
|
860
|
+
cancelLabel="Cancel"
|
|
861
|
+
confirmLabel={pendingComponentCount === 1 ? 'Save component' : 'Save all components'}
|
|
862
|
+
onconfirm={confirmSaveAllComponents}
|
|
863
|
+
width="400px"
|
|
864
|
+
>
|
|
865
|
+
<p class="save-components-message">
|
|
866
|
+
{pendingComponentCount === 1
|
|
867
|
+
? '1 component has unsaved edits.'
|
|
868
|
+
: `${pendingComponentCount} components have unsaved edits.`}
|
|
869
|
+
Save {pendingComponentCount === 1 ? 'it' : 'all of them'} and continue?
|
|
870
|
+
</p>
|
|
871
|
+
</UIDialog>
|
|
872
|
+
|
|
744
873
|
<FileLoadList
|
|
745
874
|
bind:show={showFileList}
|
|
746
|
-
title="
|
|
875
|
+
title="Theme Picker"
|
|
747
876
|
files={rows}
|
|
748
877
|
activeFileName={activeRowId}
|
|
749
878
|
selectedFileName={previewRow?.fileName ?? null}
|
|
750
|
-
selectedBadge={{ label: 'Preview', title: 'Shown on the page now. Save to
|
|
879
|
+
selectedBadge={{ label: 'Preview', title: 'Shown on the page now. Save to use and publish it.' }}
|
|
751
880
|
{rowBadge}
|
|
752
881
|
cancelLabel={previewRow ? 'Cancel' : 'Close'}
|
|
753
882
|
confirmLabel={previewRow ? 'Save' : ''}
|
|
@@ -788,6 +917,13 @@
|
|
|
788
917
|
/>
|
|
789
918
|
|
|
790
919
|
<style>
|
|
920
|
+
.save-components-message {
|
|
921
|
+
margin: 0;
|
|
922
|
+
color: var(--ui-text-secondary);
|
|
923
|
+
font-size: var(--ui-font-size-sm);
|
|
924
|
+
line-height: 1.5;
|
|
925
|
+
}
|
|
926
|
+
|
|
791
927
|
.look-panel {
|
|
792
928
|
--mfm-active: #5aa85e;
|
|
793
929
|
--mfm-rail-neutral: var(--ui-border);
|
|
@@ -850,6 +986,34 @@
|
|
|
850
986
|
color: var(--ui-text-tertiary);
|
|
851
987
|
}
|
|
852
988
|
|
|
989
|
+
.theme-name-trigger {
|
|
990
|
+
display: block;
|
|
991
|
+
width: 100%;
|
|
992
|
+
padding: 0;
|
|
993
|
+
border: 0;
|
|
994
|
+
background: transparent;
|
|
995
|
+
color: inherit;
|
|
996
|
+
font: inherit;
|
|
997
|
+
text-align: inherit;
|
|
998
|
+
cursor: pointer;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.theme-name-trigger :global(.file-pill) {
|
|
1002
|
+
width: 100%;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
.theme-name-trigger:hover :global(.file-pill),
|
|
1006
|
+
.theme-name-trigger:focus-visible :global(.file-pill) {
|
|
1007
|
+
border-color: var(--ui-border-higher);
|
|
1008
|
+
box-shadow: inset 0 0 0 1px var(--ui-border-high);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
.theme-name-trigger:focus-visible {
|
|
1012
|
+
outline: 2px solid var(--ui-text-accent);
|
|
1013
|
+
outline-offset: 2px;
|
|
1014
|
+
border-radius: var(--ui-radius-md);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
853
1017
|
.mfm-badge {
|
|
854
1018
|
display: inline-flex;
|
|
855
1019
|
align-items: center;
|
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
checked?: boolean;
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
label?: string;
|
|
6
|
+
/** Put the label before the switch, for rows that align to a right edge. */
|
|
7
|
+
labelFirst?: boolean;
|
|
6
8
|
onchange?: (checked: boolean) => void;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
let {
|
|
11
|
+
let {
|
|
12
|
+
checked = $bindable(false),
|
|
13
|
+
disabled = false,
|
|
14
|
+
label = '',
|
|
15
|
+
labelFirst = false,
|
|
16
|
+
onchange,
|
|
17
|
+
}: Props = $props();
|
|
10
18
|
|
|
11
19
|
function toggle() {
|
|
12
20
|
if (disabled) return;
|
|
@@ -15,7 +23,7 @@
|
|
|
15
23
|
}
|
|
16
24
|
</script>
|
|
17
25
|
|
|
18
|
-
<label class="toggle" class:disabled>
|
|
26
|
+
<label class="toggle" class:disabled class:label-first={labelFirst}>
|
|
19
27
|
<button
|
|
20
28
|
type="button"
|
|
21
29
|
role="switch"
|
|
@@ -41,6 +49,10 @@
|
|
|
41
49
|
cursor: pointer;
|
|
42
50
|
user-select: none;
|
|
43
51
|
|
|
52
|
+
&.label-first {
|
|
53
|
+
flex-direction: row-reverse;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
&.disabled {
|
|
45
57
|
cursor: not-allowed;
|
|
46
58
|
|
|
@@ -55,7 +55,13 @@
|
|
|
55
55
|
{#if show}
|
|
56
56
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions, a11y_no_static_element_interactions -->
|
|
57
57
|
<div class="ui-dialog-backdrop" onclick={self(handleClose)}>
|
|
58
|
-
<div
|
|
58
|
+
<div
|
|
59
|
+
class="ui-dialog"
|
|
60
|
+
role="dialog"
|
|
61
|
+
aria-modal="true"
|
|
62
|
+
aria-label={title || 'Dialog'}
|
|
63
|
+
style="width: {width}; max-width: {width};"
|
|
64
|
+
>
|
|
59
65
|
{#if title}
|
|
60
66
|
<div class="ui-dialog-header">
|
|
61
67
|
<h3 class="ui-dialog-title">{title}</h3>
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { onMount, onDestroy } from 'svelte';
|
|
2
|
+
import { onMount, onDestroy, untrack } from 'svelte';
|
|
5
3
|
import { resolveAliasChain } from '../core/palettes/tokenRegistry';
|
|
6
4
|
import { editorState } from '../core/store/editorStore';
|
|
7
|
-
import {
|
|
5
|
+
import { CSS_VARS_CHANGE_EVENT, type CssVarsChangeDetail } from '../core/cssVarSync';
|
|
8
6
|
import type { FontFamily, FontSource, FontSourceKind } from '../core/themes/themeTypes';
|
|
9
7
|
import UITokenSelector from './UITokenSelector.svelte';
|
|
10
8
|
import UIOptionList from './UIOptionList.svelte';
|
|
@@ -93,8 +91,8 @@
|
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
function handleVarChange(e: Event) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
94
|
+
const names = (e as CustomEvent<CssVarsChangeDetail>).detail?.names ?? [];
|
|
95
|
+
if (names.some((name) => name.startsWith('--font-'))) readResolved();
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
function initFromCurrent() {
|
|
@@ -160,19 +158,21 @@
|
|
|
160
158
|
|
|
161
159
|
// Re-derive `chosenKey` / `chosenFamilyId` when `variable` changes (the
|
|
162
160
|
// VariantGroup tabs view reuses the same selector instance across states).
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
$effect(() => {
|
|
162
|
+
// A tab can reuse this instance with a different variable. Track only the
|
|
163
|
+
// prop; initialization writes local selector state and is intentionally
|
|
164
|
+
// untracked to prevent a self-triggering effect.
|
|
165
|
+
variable;
|
|
166
|
+
untrack(() => {
|
|
167
167
|
initFromCurrent();
|
|
168
|
-
}
|
|
168
|
+
});
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
onMount(() => {
|
|
172
|
-
document.addEventListener(
|
|
172
|
+
document.addEventListener(CSS_VARS_CHANGE_EVENT, handleVarChange);
|
|
173
173
|
});
|
|
174
174
|
onDestroy(() => {
|
|
175
|
-
document.removeEventListener(
|
|
175
|
+
document.removeEventListener(CSS_VARS_CHANGE_EVENT, handleVarChange);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
let activeLabel = $derived(chosenFamilyId
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { run } from 'svelte/legacy';
|
|
3
3
|
|
|
4
4
|
import { onMount, onDestroy } from 'svelte';
|
|
5
|
-
import {
|
|
5
|
+
import { CSS_VARS_CHANGE_EVENT, type CssVarsChangeDetail } from '../core/cssVarSync';
|
|
6
6
|
import UIVariantSelector from './UIVariantSelector.svelte';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function handleVarChange(e: Event) {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
77
|
+
const names = (e as CustomEvent<CssVarsChangeDetail>).detail?.names ?? [];
|
|
78
|
+
if (names.some((name) => name.startsWith('--font-size-') || name.startsWith('--icon-size-'))) readPxValues();
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Re-read sizes when the target variable's scale changes
|
|
@@ -86,10 +86,10 @@
|
|
|
86
86
|
|
|
87
87
|
onMount(() => {
|
|
88
88
|
readPxValues();
|
|
89
|
-
document.addEventListener(
|
|
89
|
+
document.addEventListener(CSS_VARS_CHANGE_EVENT, handleVarChange);
|
|
90
90
|
});
|
|
91
91
|
onDestroy(() => {
|
|
92
|
-
document.removeEventListener(
|
|
92
|
+
document.removeEventListener(CSS_VARS_CHANGE_EVENT, handleVarChange);
|
|
93
93
|
});
|
|
94
94
|
</script>
|
|
95
95
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy, onMount } from 'svelte';
|
|
3
|
+
import { editorState } from '../core/store/editorStore';
|
|
4
|
+
import { CSS_VARS_CHANGE_EVENT, type CssVarsChangeDetail } from '../core/cssVarSync';
|
|
5
|
+
import {
|
|
6
|
+
fontWeightAvailability,
|
|
7
|
+
inferFontFamilyVariable,
|
|
8
|
+
} from '../core/fonts/fontWeightAvailability';
|
|
2
9
|
import UIVariantSelector from './UIVariantSelector.svelte';
|
|
3
10
|
import UIOptionItem from './UIOptionItem.svelte';
|
|
4
11
|
|
|
@@ -31,6 +38,44 @@
|
|
|
31
38
|
{ key: 'extrabold', label: 'Extra Bold', value: '800' },
|
|
32
39
|
{ key: 'black', label: 'Black', value: '900' },
|
|
33
40
|
] as const;
|
|
41
|
+
|
|
42
|
+
let availability = $state<ReturnType<typeof fontWeightAvailability>>(null);
|
|
43
|
+
let familyVariable = $derived(inferFontFamilyVariable(variable));
|
|
44
|
+
|
|
45
|
+
function refreshAvailability() {
|
|
46
|
+
if (!familyVariable) {
|
|
47
|
+
availability = null;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const familyValue = getComputedStyle(document.documentElement)
|
|
51
|
+
.getPropertyValue(familyVariable)
|
|
52
|
+
.trim();
|
|
53
|
+
availability = fontWeightAvailability(
|
|
54
|
+
familyValue,
|
|
55
|
+
$editorState.fonts.sources,
|
|
56
|
+
$editorState.fonts.stacks,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleVarChange(event: Event) {
|
|
61
|
+
const names = (event as CustomEvent<CssVarsChangeDetail>).detail?.names ?? [];
|
|
62
|
+
if (
|
|
63
|
+
(familyVariable && names.includes(familyVariable))
|
|
64
|
+
|| names.some((name) => /^--font-(?:display|sans|serif|mono)$/.test(name))
|
|
65
|
+
) {
|
|
66
|
+
refreshAvailability();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
$effect(() => {
|
|
71
|
+
variable;
|
|
72
|
+
$editorState.fonts.sources;
|
|
73
|
+
$editorState.fonts.stacks;
|
|
74
|
+
refreshAvailability();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
onMount(() => document.addEventListener(CSS_VARS_CHANGE_EVENT, handleVarChange));
|
|
78
|
+
onDestroy(() => document.removeEventListener(CSS_VARS_CHANGE_EVENT, handleVarChange));
|
|
34
79
|
</script>
|
|
35
80
|
|
|
36
81
|
<UIVariantSelector
|
|
@@ -44,8 +89,13 @@
|
|
|
44
89
|
{onchange}
|
|
45
90
|
>
|
|
46
91
|
{#snippet option({ opt, active, select })}
|
|
47
|
-
|
|
48
|
-
<UIOptionItem
|
|
92
|
+
{@const unsupported = availability !== null && !availability.weights.has(Number(opt.value))}
|
|
93
|
+
<UIOptionItem
|
|
94
|
+
{active}
|
|
95
|
+
disabled={unsupported}
|
|
96
|
+
title={unsupported ? `${opt.label} is not available for ${availability?.familyName}` : ''}
|
|
97
|
+
onclick={select}
|
|
98
|
+
>
|
|
49
99
|
{#snippet preview()}
|
|
50
100
|
<span class="weight-sample" style="font-weight: var(--font-weight-{opt.key});">A</span>
|
|
51
101
|
{/snippet}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
interface Props {
|
|
3
3
|
active?: boolean;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
title?: string;
|
|
4
6
|
preview?: import('svelte').Snippet;
|
|
5
7
|
label?: import('svelte').Snippet;
|
|
6
8
|
meta?: import('svelte').Snippet;
|
|
@@ -9,6 +11,8 @@
|
|
|
9
11
|
|
|
10
12
|
let {
|
|
11
13
|
active = false,
|
|
14
|
+
disabled = false,
|
|
15
|
+
title = '',
|
|
12
16
|
preview,
|
|
13
17
|
label,
|
|
14
18
|
meta,
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
}: Props = $props();
|
|
17
21
|
</script>
|
|
18
22
|
|
|
19
|
-
<button class="ui-option-item" class:active {onclick}>
|
|
23
|
+
<button class="ui-option-item" class:active {onclick} {disabled} {title}>
|
|
20
24
|
{#if preview}
|
|
21
25
|
<span class="ui-option-preview">
|
|
22
26
|
{@render preview?.()}
|
|
@@ -64,6 +68,15 @@
|
|
|
64
68
|
color: var(--ui-text-primary);
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
.ui-option-item:disabled {
|
|
72
|
+
opacity: 0.4;
|
|
73
|
+
cursor: not-allowed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ui-option-item:disabled:hover {
|
|
77
|
+
background: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
.ui-option-preview {
|
|
68
81
|
flex-shrink: 0;
|
|
69
82
|
display: inline-flex;
|