@motion-proto/live-tokens 0.1.0 → 0.3.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/README.md +160 -21
- package/dist-plugin/index.cjs +823 -336
- package/dist-plugin/index.d.cts +9 -7
- package/dist-plugin/index.d.ts +9 -7
- package/dist-plugin/index.js +822 -335
- package/package.json +51 -23
- package/src/assets/newspaper.webp +0 -0
- package/src/assets/offering.webp +0 -0
- package/src/component-editor/BadgeEditor.svelte +170 -0
- package/src/component-editor/CalloutEditor.svelte +103 -0
- package/src/component-editor/CardEditor.svelte +184 -0
- package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
- package/src/component-editor/CornerBadgeEditor.svelte +207 -0
- package/src/component-editor/DialogEditor.svelte +172 -0
- package/src/component-editor/ImageEditor.svelte +72 -0
- package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
- package/src/component-editor/NotificationEditor.svelte +160 -0
- package/src/component-editor/ProgressBarEditor.svelte +124 -0
- package/src/component-editor/RadioButtonEditor.svelte +140 -0
- package/src/component-editor/SectionDividerEditor.svelte +263 -0
- package/src/component-editor/SegmentedControlEditor.svelte +154 -0
- package/src/component-editor/StandardButtonsEditor.svelte +178 -0
- package/src/component-editor/TabBarEditor.svelte +137 -0
- package/src/component-editor/TableEditor.svelte +128 -0
- package/src/component-editor/TooltipEditor.svelte +122 -0
- package/src/component-editor/editorTokens.test.ts +93 -0
- package/src/component-editor/groupKeySlots.test.ts +67 -0
- package/src/component-editor/groupKeySnapshot.test.ts +52 -0
- package/src/component-editor/index.ts +5 -0
- package/src/component-editor/registry.ts +246 -0
- package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
- package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
- package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
- package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
- package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
- package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
- package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
- package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
- package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
- package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
- package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
- package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
- package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
- package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
- package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
- package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
- package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
- package/src/component-editor/scaffolding/componentSources.ts +9 -0
- package/src/component-editor/scaffolding/defaultSections.ts +16 -0
- package/src/component-editor/scaffolding/editorContext.ts +44 -0
- package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
- package/src/component-editor/scaffolding/siblings.ts +33 -0
- package/src/component-editor/scaffolding/types.ts +39 -0
- package/src/components/Badge.svelte +231 -42
- package/src/components/Button.svelte +324 -124
- package/src/components/Callout.svelte +145 -0
- package/src/components/Card.svelte +123 -25
- package/src/components/CollapsibleSection.svelte +213 -35
- package/src/components/CornerBadge.svelte +224 -0
- package/src/components/Dialog.svelte +137 -114
- package/src/components/Image.svelte +43 -0
- package/src/components/InlineEditActions.svelte +74 -14
- package/src/components/Notification.svelte +184 -163
- package/src/components/ProgressBar.svelte +216 -22
- package/src/components/RadioButton.svelte +110 -40
- package/src/components/SectionDivider.svelte +428 -74
- package/src/components/SegmentedControl.svelte +203 -0
- package/src/components/TabBar.svelte +146 -21
- package/src/components/Table.svelte +102 -0
- package/src/components/Tooltip.svelte +45 -19
- package/src/components/types.ts +51 -0
- package/src/data/google-fonts.json +75 -0
- package/src/lib/ColumnsOverlay.svelte +20 -7
- package/src/lib/LiveEditorOverlay.svelte +265 -82
- package/src/lib/columnsOverlay.ts +21 -17
- package/src/lib/componentConfig.test.ts +204 -0
- package/src/lib/componentConfigKeys.ts +19 -0
- package/src/lib/componentConfigService.ts +88 -0
- package/src/lib/copyPopover.ts +30 -0
- package/src/lib/cssVarSync.ts +59 -7
- package/src/lib/editorConfigStore.ts +0 -10
- package/src/lib/editorCore.ts +402 -0
- package/src/lib/editorKeybindings.ts +52 -0
- package/src/lib/editorPersistence.ts +106 -0
- package/src/lib/editorRenderer.ts +74 -0
- package/src/lib/editorStore.test.ts +328 -0
- package/src/lib/editorStore.ts +412 -0
- package/src/lib/editorTypes.ts +100 -0
- package/src/lib/editorViewStore.ts +55 -0
- package/src/lib/files/versionedFileResource.ts +140 -0
- package/src/lib/fontLoader.ts +130 -0
- package/src/lib/fontMigration.ts +140 -0
- package/src/lib/fontParse.ts +168 -0
- package/src/lib/index.ts +48 -31
- package/src/lib/lazyConfig.test.ts +54 -0
- package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
- package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
- package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
- package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
- package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
- package/src/lib/migrations/index.ts +93 -0
- package/src/lib/migrations/migrations.test.ts +341 -0
- package/src/lib/navLinkTypes.ts +1 -0
- package/src/lib/overlayState.ts +3 -0
- package/src/lib/paletteDerivation.ts +300 -0
- package/src/lib/parentRouteStore.ts +42 -0
- package/src/lib/parsers/globalRootBlock.ts +32 -0
- package/src/lib/presetService.ts +94 -0
- package/src/lib/router.ts +49 -0
- package/src/lib/scrollSection.ts +45 -0
- package/src/lib/slices/columns.ts +59 -0
- package/src/lib/slices/components.ts +362 -0
- package/src/lib/slices/domainVars.ts +15 -0
- package/src/lib/slices/fonts.ts +30 -0
- package/src/lib/slices/gradients.ts +153 -0
- package/src/lib/slices/overlays.ts +132 -0
- package/src/lib/slices/palettes.ts +26 -0
- package/src/lib/slices/shadows.ts +123 -0
- package/src/lib/storage.ts +88 -0
- package/src/lib/themeInit.ts +74 -0
- package/src/lib/themeService.ts +101 -0
- package/src/lib/themeTypes.ts +146 -0
- package/src/lib/tokenRegistry.ts +148 -0
- package/src/pages/ComponentEditorPage.svelte +384 -0
- package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
- package/src/pages/Editor.svelte +98 -0
- package/src/pages/Editor.svelte.d.ts +2 -0
- package/src/pages/EditorShell.svelte +348 -0
- package/src/styles/_padding.scss +34 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
- package/src/styles/fonts.css +22 -10
- package/src/styles/form-controls.css +14 -16
- package/src/styles/tokens.css +1322 -0
- package/src/styles/ui-editor.css +126 -0
- package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
- package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
- package/src/ui/EditorViewSwitcher.svelte +180 -0
- package/src/ui/FontStackEditor.svelte +360 -0
- package/src/ui/GradientEditor.svelte +461 -0
- package/src/ui/GradientStopPicker.svelte +74 -0
- package/src/ui/PaletteEditor.svelte +1590 -0
- package/src/ui/PaletteEditor.test.ts +108 -0
- package/src/ui/PresetFileManager.svelte +567 -0
- package/src/ui/ProjectFontsSection.svelte +645 -0
- package/src/{showcase → ui}/SurfacesTab.svelte +39 -41
- package/src/{showcase → ui}/TextTab.svelte +27 -29
- package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
- package/src/ui/Toggle.svelte +108 -0
- package/src/ui/UICopyPopover.svelte +78 -0
- package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
- package/src/ui/UIFontFamilySelector.svelte +309 -0
- package/src/ui/UIFontSizeSelector.svelte +165 -0
- package/src/ui/UIFontWeightSelector.svelte +52 -0
- package/src/ui/UILineHeightSelector.svelte +47 -0
- package/src/ui/UILinkToggle.svelte +60 -0
- package/src/ui/UIOptionItem.svelte +74 -0
- package/src/ui/UIOptionList.svelte +27 -0
- package/src/ui/UIPaddingSelector.svelte +661 -0
- package/src/ui/UIPaletteSelector.svelte +1084 -0
- package/src/ui/UIRadio.svelte +72 -0
- package/src/ui/UIRadioGroup.svelte +59 -0
- package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
- package/src/ui/UITokenSelector.svelte +509 -0
- package/src/ui/UIVariantSelector.svelte +145 -0
- package/src/ui/VariablesTab.svelte +252 -0
- package/src/ui/index.ts +31 -0
- package/src/ui/keepInViewport.ts +84 -0
- package/src/ui/palette/GradientStopEditor.svelte +482 -0
- package/src/ui/palette/OverridesPanel.svelte +526 -0
- package/src/ui/palette/PaletteBase.svelte +165 -0
- package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
- package/src/ui/palette/paletteEditorState.ts +89 -0
- package/src/ui/sections/ColumnsSection.svelte +273 -0
- package/src/ui/sections/GradientsSection.svelte +147 -0
- package/src/ui/sections/OverlaysSection.svelte +670 -0
- package/src/ui/sections/ShadowsSection.svelte +1250 -0
- package/src/ui/sections/TokenScaleTable.svelte +332 -0
- package/src/ui/sections/tokenScales.ts +81 -0
- package/src/ui/variantScales.ts +108 -0
- package/src/components/DetailNav.svelte +0 -78
- package/src/components/Toggle.svelte +0 -86
- package/src/lib/pageSource.ts +0 -6
- package/src/lib/tokenInit.ts +0 -29
- package/src/lib/tokenService.ts +0 -144
- package/src/lib/tokenTypes.ts +0 -45
- package/src/pages/Admin.svelte +0 -100
- package/src/pages/ShowcasePage.svelte +0 -146
- package/src/showcase/BackupBrowser.svelte +0 -617
- package/src/showcase/ComponentsTab.svelte +0 -107
- package/src/showcase/PaletteEditor.svelte +0 -2579
- package/src/showcase/PaletteSelector.svelte +0 -627
- package/src/showcase/TokenMap.svelte +0 -54
- package/src/showcase/VariablesTab.svelte +0 -2657
- package/src/showcase/VisualsTab.svelte +0 -233
- package/src/showcase/demos/BadgeDemo.svelte +0 -58
- package/src/showcase/demos/CardDemo.svelte +0 -52
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -194
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -56
- package/src/showcase/demos/DialogDemo.svelte +0 -42
- package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -27
- package/src/showcase/demos/NotificationDemo.svelte +0 -149
- package/src/showcase/demos/ProgressBarDemo.svelte +0 -56
- package/src/showcase/demos/RadioButtonDemo.svelte +0 -58
- package/src/showcase/demos/SectionDividerDemo.svelte +0 -79
- package/src/showcase/demos/StandardButtonsDemo.svelte +0 -457
- package/src/showcase/demos/TabBarDemo.svelte +0 -60
- package/src/showcase/demos/TooltipDemo.svelte +0 -54
- package/src/showcase/editor.css +0 -93
- package/src/showcase/index.ts +0 -17
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +0 -97
- package/src/styles/fonts/Domine/README.txt +0 -66
- /package/src/{showcase → ui}/curveEngine.ts +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregated CSS-var names owned by store domains. Consumers that scrape
|
|
3
|
+
* the DOM (e.g. the save flow still pulling palette ramps emitted by
|
|
4
|
+
* PaletteEditor) use this to drop domain-owned keys from their scraped bag
|
|
5
|
+
* so the store stays the single source of truth.
|
|
6
|
+
*/
|
|
7
|
+
import { COLUMN_VAR_NAMES } from './columns';
|
|
8
|
+
import { OVERLAY_VAR_NAMES } from './overlays';
|
|
9
|
+
import { SHADOW_VAR_NAMES } from './shadows';
|
|
10
|
+
|
|
11
|
+
export const DOMAIN_VAR_NAMES: readonly string[] = [
|
|
12
|
+
...COLUMN_VAR_NAMES,
|
|
13
|
+
...OVERLAY_VAR_NAMES,
|
|
14
|
+
...SHADOW_VAR_NAMES,
|
|
15
|
+
];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fonts slice — sources + stacks. No derived CSS vars owned by this store
|
|
3
|
+
* (the `--font-*` vars are written by `applyFontStacks` in fontLoader, and
|
|
4
|
+
* @font-face rules by `applyFontSources`). We own the *data* (sources +
|
|
5
|
+
* stacks); callers still invoke the DOM-side-effect helpers themselves
|
|
6
|
+
* after mutating.
|
|
7
|
+
*/
|
|
8
|
+
import type { FontSource, FontStack } from '../themeTypes';
|
|
9
|
+
import { store, mutate, persist } from '../editorCore';
|
|
10
|
+
|
|
11
|
+
export function setFontSources(sources: FontSource[]): void {
|
|
12
|
+
mutate('update font sources', (s) => { s.fonts.sources = sources; });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setFontStacks(stacks: FontStack[]): void {
|
|
16
|
+
mutate('update font stacks', (s) => { s.fonts.stacks = stacks; });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Populate fonts from the server's active theme at boot. Does not push
|
|
21
|
+
* a history entry — the boot load is a starting point, not an edit.
|
|
22
|
+
*/
|
|
23
|
+
export function seedFontsFromTheme(sources: FontSource[], stacks: FontStack[]): void {
|
|
24
|
+
store.update((s) => {
|
|
25
|
+
s.fonts.sources = structuredClone(sources);
|
|
26
|
+
s.fonts.stacks = structuredClone(stacks);
|
|
27
|
+
return s;
|
|
28
|
+
});
|
|
29
|
+
persist();
|
|
30
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradients slice — fixed four-slot scale (--gradient-1 … --gradient-4),
|
|
3
|
+
* each rendering to a single CSS var. Stops carry token-name references
|
|
4
|
+
* (`--color-brand-500`); the renderer wraps them in `var(...)` so palette
|
|
5
|
+
* edits flow through.
|
|
6
|
+
*/
|
|
7
|
+
import type { EditorState, GradientToken, GradientTokenStop, GradientType } from '../editorTypes';
|
|
8
|
+
import { mutate } from '../editorCore';
|
|
9
|
+
|
|
10
|
+
export function makeDefaultGradients(): GradientToken[] {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
variable: '--gradient-1',
|
|
14
|
+
type: 'linear',
|
|
15
|
+
angle: 90,
|
|
16
|
+
stops: [
|
|
17
|
+
{ position: 0, color: '--color-brand-500' },
|
|
18
|
+
{ position: 100, color: '--color-accent-500' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
variable: '--gradient-2',
|
|
23
|
+
type: 'linear',
|
|
24
|
+
angle: 135,
|
|
25
|
+
stops: [
|
|
26
|
+
{ position: 0, color: '--color-brand-500' },
|
|
27
|
+
{ position: 100, color: '--color-special-500' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
variable: '--gradient-3',
|
|
32
|
+
type: 'linear',
|
|
33
|
+
angle: 90,
|
|
34
|
+
stops: [
|
|
35
|
+
{ position: 0, color: '--color-success-500' },
|
|
36
|
+
{ position: 100, color: '--color-info-500' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
variable: '--gradient-4',
|
|
41
|
+
type: 'linear',
|
|
42
|
+
angle: 45,
|
|
43
|
+
stops: [
|
|
44
|
+
{ position: 0, color: '--color-danger-500' },
|
|
45
|
+
{ position: 100, color: '--color-warning-500' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatGradientStop(s: GradientTokenStop): string {
|
|
52
|
+
const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
|
|
53
|
+
const opacity = s.opacity ?? 100;
|
|
54
|
+
const color = opacity >= 100
|
|
55
|
+
? base
|
|
56
|
+
: `color-mix(in srgb, ${base} ${opacity}%, transparent)`;
|
|
57
|
+
return `${color} ${s.position}%`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Stops portion only — used by the palette selector to materialize a
|
|
61
|
+
* linear-gradient with a per-slot angle override while keeping the token's
|
|
62
|
+
* stop list (and its `var(--color-…)` refs, which propagate palette edits). */
|
|
63
|
+
export function formatGradientStops(t: GradientToken): string {
|
|
64
|
+
return t.stops.map(formatGradientStop).join(', ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatGradient(t: GradientToken): string {
|
|
68
|
+
const stops = formatGradientStops(t);
|
|
69
|
+
if (t.type === 'linear') return `linear-gradient(${t.angle}deg, ${stops})`;
|
|
70
|
+
return `radial-gradient(${stops})`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function gradientsToVars(g: EditorState['gradients']): Record<string, string> {
|
|
74
|
+
const out: Record<string, string> = {};
|
|
75
|
+
for (const t of g.tokens) out[t.variable] = formatGradient(t);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findGradient(s: EditorState, variable: string): GradientToken | undefined {
|
|
80
|
+
return s.gradients.tokens.find((t) => t.variable === variable);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Replace a gradient's type, angle, and stops in one shot. Used by the editor
|
|
84
|
+
* to restore a pre-edit snapshot on Cancel. */
|
|
85
|
+
export function setGradient(
|
|
86
|
+
variable: string,
|
|
87
|
+
next: { type: GradientType; angle: number; stops: GradientTokenStop[] },
|
|
88
|
+
): void {
|
|
89
|
+
mutate(`replace gradient ${variable}`, (s) => {
|
|
90
|
+
const t = findGradient(s, variable);
|
|
91
|
+
if (!t) return;
|
|
92
|
+
t.type = next.type;
|
|
93
|
+
t.angle = next.angle;
|
|
94
|
+
t.stops = next.stops.map((st) => ({ ...st }));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function setGradientType(variable: string, type: GradientType): void {
|
|
99
|
+
mutate(`set gradient type ${variable}`, (s) => {
|
|
100
|
+
const t = findGradient(s, variable);
|
|
101
|
+
if (t) t.type = type;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function setGradientAngle(variable: string, angle: number): void {
|
|
106
|
+
mutate(`set gradient angle ${variable}`, (s) => {
|
|
107
|
+
const t = findGradient(s, variable);
|
|
108
|
+
if (t) t.angle = angle;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function setGradientStop(variable: string, index: number, stop: Partial<GradientTokenStop>): void {
|
|
113
|
+
mutate(`set gradient stop ${variable}[${index}]`, (s) => {
|
|
114
|
+
const t = findGradient(s, variable);
|
|
115
|
+
if (!t || !t.stops[index]) return;
|
|
116
|
+
if (stop.position !== undefined) t.stops[index].position = stop.position;
|
|
117
|
+
if (stop.color !== undefined) t.stops[index].color = stop.color;
|
|
118
|
+
if (stop.opacity !== undefined) t.stops[index].opacity = stop.opacity;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function addGradientStop(variable: string, stop: GradientTokenStop): void {
|
|
123
|
+
mutate(`add gradient stop ${variable}`, (s) => {
|
|
124
|
+
const t = findGradient(s, variable);
|
|
125
|
+
if (!t) return;
|
|
126
|
+
t.stops.push(stop);
|
|
127
|
+
t.stops.sort((a, b) => a.position - b.position);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function removeGradientStop(variable: string, index: number): void {
|
|
132
|
+
mutate(`remove gradient stop ${variable}[${index}]`, (s) => {
|
|
133
|
+
const t = findGradient(s, variable);
|
|
134
|
+
if (!t || t.stops.length <= 2) return;
|
|
135
|
+
t.stops.splice(index, 1);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function addGradientToken(token: GradientToken): void {
|
|
140
|
+
mutate(`add gradient ${token.variable}`, (s) => {
|
|
141
|
+
if (findGradient(s, token.variable)) return;
|
|
142
|
+
s.gradients.tokens.push({
|
|
143
|
+
...token,
|
|
144
|
+
stops: token.stops.map((st) => ({ ...st })),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function removeGradientToken(variable: string): void {
|
|
150
|
+
mutate(`remove gradient ${variable}`, (s) => {
|
|
151
|
+
s.gradients.tokens = s.gradients.tokens.filter((t) => t.variable !== variable);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlays slice — overlay + hover RGBA tokens. Defaults are editor-defined
|
|
3
|
+
* (mirror what `VariablesTab` historically initialised into local let state)
|
|
4
|
+
* and diverge from tokens.css by design: the editor starts with a neutral
|
|
5
|
+
* palette and tokens.css continues to win until first edit.
|
|
6
|
+
*/
|
|
7
|
+
import type { EditorState, OverlayToken } from '../editorTypes';
|
|
8
|
+
|
|
9
|
+
export function makeDefaultOverlayTokens(): OverlayToken[] {
|
|
10
|
+
return [
|
|
11
|
+
{ variable: '--overlay-lowest', label: 'Lowest', r: 0, g: 0, b: 0, opacity: 0.05 },
|
|
12
|
+
{ variable: '--overlay-lower', label: 'Lower', r: 0, g: 0, b: 0, opacity: 0.1 },
|
|
13
|
+
{ variable: '--overlay-low', label: 'Low', r: 0, g: 0, b: 0, opacity: 0.2 },
|
|
14
|
+
{ variable: '--overlay', label: 'Base', r: 0, g: 0, b: 0, opacity: 0.3 },
|
|
15
|
+
{ variable: '--overlay-high', label: 'High', r: 0, g: 0, b: 0, opacity: 0.5 },
|
|
16
|
+
{ variable: '--overlay-higher', label: 'Higher', r: 0, g: 0, b: 0, opacity: 0.7 },
|
|
17
|
+
{ variable: '--overlay-highest',label: 'Highest',r: 0, g: 0, b: 0, opacity: 0.95 },
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function makeDefaultHoverTokens(): OverlayToken[] {
|
|
22
|
+
return [
|
|
23
|
+
{ variable: '--hover-low', label: 'Low', r: 255, g: 255, b: 255, opacity: 0.05 },
|
|
24
|
+
{ variable: '--hover', label: 'Base', r: 255, g: 255, b: 255, opacity: 0.1 },
|
|
25
|
+
{ variable: '--hover-high', label: 'High', r: 255, g: 255, b: 255, opacity: 0.15 },
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function makeDefaultOverlaysState(): EditorState['overlays'] {
|
|
30
|
+
return {
|
|
31
|
+
tokens: makeDefaultOverlayTokens(),
|
|
32
|
+
hoverTokens: makeDefaultHoverTokens(),
|
|
33
|
+
globals: {
|
|
34
|
+
overlay: { hue: 0, saturation: 0, lightness: 0, opacityMin: 0.05, opacityMax: 0.95 },
|
|
35
|
+
hover: { hue: 0, saturation: 0, lightness: 100, opacityMin: 0.05, opacityMax: 0.15 },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const OVERLAY_VAR_NAMES = [
|
|
41
|
+
'--overlay-lowest', '--overlay-lower', '--overlay-low', '--overlay',
|
|
42
|
+
'--overlay-high', '--overlay-higher', '--overlay-highest',
|
|
43
|
+
'--hover-low', '--hover', '--hover-high',
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
// Accepts rgb(), rgba(), and #rrggbb[aa] — themes saved by the editor
|
|
47
|
+
// always use rgba(), but loading hand-written files shouldn't break.
|
|
48
|
+
export const RGBA_RE = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i;
|
|
49
|
+
export const HEX_RE = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i;
|
|
50
|
+
|
|
51
|
+
export function parseRgba(raw: string): { r: number; g: number; b: number; opacity: number } | null {
|
|
52
|
+
const s = raw.trim();
|
|
53
|
+
const m = s.match(RGBA_RE);
|
|
54
|
+
if (m) {
|
|
55
|
+
const r = parseInt(m[1], 10);
|
|
56
|
+
const g = parseInt(m[2], 10);
|
|
57
|
+
const b = parseInt(m[3], 10);
|
|
58
|
+
const a = m[4] !== undefined ? parseFloat(m[4]) : 1;
|
|
59
|
+
if (![r, g, b].every((n) => Number.isFinite(n) && n >= 0 && n <= 255)) return null;
|
|
60
|
+
return { r, g, b, opacity: Number.isFinite(a) ? a : 1 };
|
|
61
|
+
}
|
|
62
|
+
const h = s.match(HEX_RE);
|
|
63
|
+
if (h) {
|
|
64
|
+
const hex = h[1];
|
|
65
|
+
const alpha = h[2] !== undefined ? parseInt(h[2], 16) / 255 : 1;
|
|
66
|
+
return {
|
|
67
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
68
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
69
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
70
|
+
opacity: Math.round(alpha * 100) / 100,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function overlayTokenToRgba(t: OverlayToken): string {
|
|
77
|
+
return `rgba(${t.r}, ${t.g}, ${t.b}, ${t.opacity})`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function overlaysToVars(o: EditorState['overlays']): Record<string, string> {
|
|
81
|
+
const out: Record<string, string> = {};
|
|
82
|
+
for (const t of o.tokens) out[t.variable] = overlayTokenToRgba(t);
|
|
83
|
+
for (const t of o.hoverTokens) out[t.variable] = overlayTokenToRgba(t);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tokensEqualDefault(tokens: OverlayToken[], defaults: OverlayToken[]): boolean {
|
|
88
|
+
if (tokens.length !== defaults.length) return false;
|
|
89
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
90
|
+
const a = tokens[i]; const b = defaults[i];
|
|
91
|
+
if (a.variable !== b.variable || a.r !== b.r || a.g !== b.g || a.b !== b.b || a.opacity !== b.opacity) return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Same pattern as columns: only emit overlay CSS vars once state diverges
|
|
98
|
+
* from the editor defaults. tokens.css owns the rgba values until the
|
|
99
|
+
* user touches any overlay control (or loads a theme that already
|
|
100
|
+
* contains overrides).
|
|
101
|
+
*/
|
|
102
|
+
export function overlaysEqualsDefault(o: EditorState['overlays']): boolean {
|
|
103
|
+
return tokensEqualDefault(o.tokens, makeDefaultOverlayTokens())
|
|
104
|
+
&& tokensEqualDefault(o.hoverTokens, makeDefaultHoverTokens());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function applyOverlayVarsToState(overlays: EditorState['overlays'], vars: Record<string, string>): void {
|
|
108
|
+
const applyTo = (list: OverlayToken[]) => {
|
|
109
|
+
for (const t of list) {
|
|
110
|
+
const raw = vars[t.variable];
|
|
111
|
+
if (!raw) continue;
|
|
112
|
+
const parsed = parseRgba(raw);
|
|
113
|
+
if (!parsed) continue;
|
|
114
|
+
t.r = parsed.r; t.g = parsed.g; t.b = parsed.b; t.opacity = parsed.opacity;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
applyTo(overlays.tokens);
|
|
118
|
+
applyTo(overlays.hoverTokens);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Loader: route overlay/hover entries from a freshly-loaded theme's vars
|
|
123
|
+
* bag into `next.overlays` and remove them from the bag. Mutates `next`
|
|
124
|
+
* and `rawVars` in place.
|
|
125
|
+
*/
|
|
126
|
+
export function loadOverlaysFromVars(
|
|
127
|
+
next: EditorState,
|
|
128
|
+
rawVars: Record<string, string>,
|
|
129
|
+
): void {
|
|
130
|
+
applyOverlayVarsToState(next.overlays, rawVars);
|
|
131
|
+
for (const name of OVERLAY_VAR_NAMES) delete rawVars[name];
|
|
132
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Palettes slice — each PaletteEditor instance is keyed by `label`
|
|
3
|
+
* (e.g. "Neutral", "Primary"). The store owns the full `PaletteConfig` for
|
|
4
|
+
* each label; CSS-var emission is still done by `paletteDerivation`
|
|
5
|
+
* (palette derivation involves OKLCH + bezier curves) at render time.
|
|
6
|
+
*/
|
|
7
|
+
import type { PaletteConfig } from '../themeTypes';
|
|
8
|
+
import { store, mutate, persist } from '../editorCore';
|
|
9
|
+
|
|
10
|
+
export function setPaletteConfig(label: string, config: PaletteConfig): void {
|
|
11
|
+
mutate(`update palette ${label}`, (s) => {
|
|
12
|
+
s.palettes[label] = config;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Server-boot path: populate all palettes at once without a history entry.
|
|
18
|
+
* Mirrors `seedFontsFromTheme`.
|
|
19
|
+
*/
|
|
20
|
+
export function seedPalettesFromTheme(palettes: Record<string, PaletteConfig>): void {
|
|
21
|
+
store.update((s) => {
|
|
22
|
+
s.palettes = structuredClone(palettes);
|
|
23
|
+
return s;
|
|
24
|
+
});
|
|
25
|
+
persist();
|
|
26
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadows slice — five-token scale (sm/md/lg/xl/2xl) plus globals/overrides
|
|
3
|
+
* for the editor UI's shared sliders. Defaults come from tokens.css (not the
|
|
4
|
+
* editor), so state.shadows starts with `tokens: []` and we do not emit any
|
|
5
|
+
* shadow CSS vars until the editor has populated tokens (via
|
|
6
|
+
* `seedShadowsFromDom` on hydrate, or via `loadFromFile`). Once tokens exist,
|
|
7
|
+
* the renderer writes one CSS var per token derived from its
|
|
8
|
+
* x/y/blur/spread/hsla fields.
|
|
9
|
+
*/
|
|
10
|
+
import { get } from 'svelte/store';
|
|
11
|
+
import type { EditorState, ShadowToken } from '../editorTypes';
|
|
12
|
+
import { store, persist } from '../editorCore';
|
|
13
|
+
|
|
14
|
+
export const SHADOW_VAR_NAMES = [
|
|
15
|
+
'--shadow-sm', '--shadow-md', '--shadow-lg', '--shadow-xl', '--shadow-2xl',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
// Identical literal set as SHADOW_VAR_NAMES — derived to avoid drift.
|
|
19
|
+
export const SCALE_SHADOW_VARIABLES: ReadonlySet<string> = new Set(SHADOW_VAR_NAMES);
|
|
20
|
+
|
|
21
|
+
export function computeShadowXY(angle: number, distance: number): { x: number; y: number } {
|
|
22
|
+
const rad = angle * (Math.PI / 180);
|
|
23
|
+
return {
|
|
24
|
+
x: Math.round(-distance * Math.cos(rad)),
|
|
25
|
+
y: Math.round(distance * Math.sin(rad)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function computeAngleDistance(x: number, y: number): { angle: number; distance: number } {
|
|
30
|
+
const distance = Math.round(Math.sqrt(x * x + y * y));
|
|
31
|
+
if (distance === 0) return { angle: 135, distance: 0 };
|
|
32
|
+
let angle = Math.atan2(y, -x) * (180 / Math.PI);
|
|
33
|
+
if (angle < 0) angle += 360;
|
|
34
|
+
return { angle: Math.round(angle), distance };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function shadowTokenCss(t: ShadowToken): string {
|
|
38
|
+
return `${t.x}px ${t.y}px ${t.blur}px ${t.spread}px hsla(${t.hue}, ${t.saturation}%, ${t.lightness}%, ${t.opacity})`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function defaultShadowOverride(): import('../editorTypes').ShadowOverrideFlags {
|
|
42
|
+
return { angle: false, opacity: false, color: false, distance: false, blur: false, size: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseShadowCss(variable: string, raw: string): ShadowToken | null {
|
|
46
|
+
const m = raw.trim().match(/^(-?\d+)px\s+(-?\d+)px\s+(\d+)px\s+(-?\d+)px\s+hsla\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)$/);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
const x = parseInt(m[1], 10);
|
|
49
|
+
const y = parseInt(m[2], 10);
|
|
50
|
+
const blur = parseInt(m[3], 10);
|
|
51
|
+
const spread = parseInt(m[4], 10);
|
|
52
|
+
const hue = Math.round(parseFloat(m[5]));
|
|
53
|
+
const saturation = Math.round(parseFloat(m[6]));
|
|
54
|
+
const lightness = Math.round(parseFloat(m[7]));
|
|
55
|
+
const opacity = parseFloat(m[8]);
|
|
56
|
+
const { angle, distance } = computeAngleDistance(x, y);
|
|
57
|
+
return { variable, x, y, blur, spread, opacity, hue, saturation, lightness, angle, distance };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shadowsToVars(shadows: EditorState['shadows']): Record<string, string> {
|
|
61
|
+
const out: Record<string, string> = {};
|
|
62
|
+
for (const t of shadows.tokens) out[t.variable] = shadowTokenCss(t);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function applyShadowVarsToState(shadows: EditorState['shadows'], vars: Record<string, string>): void {
|
|
67
|
+
const parsed: ShadowToken[] = [];
|
|
68
|
+
for (const name of SHADOW_VAR_NAMES) {
|
|
69
|
+
const raw = vars[name];
|
|
70
|
+
if (!raw) continue;
|
|
71
|
+
const tok = parseShadowCss(name, raw);
|
|
72
|
+
if (tok) parsed.push(tok);
|
|
73
|
+
}
|
|
74
|
+
if (parsed.length > 0) shadows.tokens = parsed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Loader: route shadow scale tokens from a freshly-loaded theme's vars bag
|
|
79
|
+
* into `next.shadows.tokens` and remove them from the bag. Globals/overrides
|
|
80
|
+
* are preserved across theme loads from the *current* state — themes don't
|
|
81
|
+
* carry editor-UI state — so the caller copies them in. Mutates `next` and
|
|
82
|
+
* `rawVars` in place.
|
|
83
|
+
*/
|
|
84
|
+
export function loadShadowsFromVars(
|
|
85
|
+
next: EditorState,
|
|
86
|
+
rawVars: Record<string, string>,
|
|
87
|
+
): void {
|
|
88
|
+
applyShadowVarsToState(next.shadows, rawVars);
|
|
89
|
+
for (const name of SHADOW_VAR_NAMES) delete rawVars[name];
|
|
90
|
+
// Preserve shadow globals/overrides across theme loads so the editor UI
|
|
91
|
+
// reopens with the same controls the user was working with.
|
|
92
|
+
const current = get(store).shadows;
|
|
93
|
+
next.shadows.globals = structuredClone(current.globals);
|
|
94
|
+
next.shadows.overrides = structuredClone(current.overrides);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Seed state.shadows.tokens from computed styles on the document element.
|
|
99
|
+
* Captures the tokens.css baseline so the editor can mutate it. Does NOT push
|
|
100
|
+
* a history entry; the seed is treated as an initial snapshot, not a user
|
|
101
|
+
* edit. Persists so a reload doesn't re-seed from the DOM on every fresh
|
|
102
|
+
* session.
|
|
103
|
+
*
|
|
104
|
+
* Called from the persistence layer's `hydrate()` so the seed lands once on
|
|
105
|
+
* boot regardless of whether the user opens the shadows tab — m13 cleanup.
|
|
106
|
+
*/
|
|
107
|
+
export function seedShadowsFromDom(): void {
|
|
108
|
+
if (typeof document === 'undefined') return;
|
|
109
|
+
const current = get(store);
|
|
110
|
+
if (current.shadows.tokens.length > 0) return;
|
|
111
|
+
const cs = getComputedStyle(document.documentElement);
|
|
112
|
+
const parsed: ShadowToken[] = [];
|
|
113
|
+
for (const name of SHADOW_VAR_NAMES) {
|
|
114
|
+
const raw = cs.getPropertyValue(name).trim();
|
|
115
|
+
if (!raw) continue;
|
|
116
|
+
const tok = parseShadowCss(name, raw);
|
|
117
|
+
if (tok) parsed.push(tok);
|
|
118
|
+
}
|
|
119
|
+
if (parsed.length === 0) return;
|
|
120
|
+
store.update((s) => { s.shadows.tokens = parsed; return s; });
|
|
121
|
+
// No bumpTick — seed is hydration-equivalent, not an edit.
|
|
122
|
+
persist();
|
|
123
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for operations that are *intentionally* allowed to fail silently:
|
|
3
|
+
* boot-time storage reads, debounced storage writes, and dev-server fetches.
|
|
4
|
+
*
|
|
5
|
+
* Naming the silence ("quietGet", "quietSet", "safeFetch") communicates intent
|
|
6
|
+
* better than scattered `try { ... } catch {}` blocks, where the empty catch
|
|
7
|
+
* leaves it ambiguous whether the author considered the failure mode or just
|
|
8
|
+
* copied a nearby pattern.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface QuietGetOptions {
|
|
12
|
+
/** When true, the stored string is parsed as JSON. Parse failures return null. */
|
|
13
|
+
parse?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read a value from `localStorage`. Returns `null` on:
|
|
18
|
+
* - Missing key
|
|
19
|
+
* - Storage unavailable (private/incognito mode, quota, browser disabled)
|
|
20
|
+
* - JSON parse failure (when `parse: true`)
|
|
21
|
+
*
|
|
22
|
+
* Boot-time hydrate paths can call this without a try/catch and trust that
|
|
23
|
+
* a fresh-boot fallback will be used when storage isn't reachable.
|
|
24
|
+
*/
|
|
25
|
+
export function quietGet<T = string>(
|
|
26
|
+
key: string,
|
|
27
|
+
opts?: QuietGetOptions,
|
|
28
|
+
): T | string | null {
|
|
29
|
+
try {
|
|
30
|
+
if (typeof localStorage === 'undefined') return null;
|
|
31
|
+
const raw = localStorage.getItem(key);
|
|
32
|
+
if (raw === null) return null;
|
|
33
|
+
if (opts?.parse) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(raw) as T;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return raw;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write a value to `localStorage`. Returns `true` on success, `false` if
|
|
48
|
+
* storage was unavailable or the write failed (quota, security errors).
|
|
49
|
+
*
|
|
50
|
+
* Callers that want to know whether the write landed can branch on the
|
|
51
|
+
* return value; callers that don't care can ignore it.
|
|
52
|
+
*/
|
|
53
|
+
export function quietSet(key: string, value: string): boolean {
|
|
54
|
+
try {
|
|
55
|
+
if (typeof localStorage === 'undefined') return false;
|
|
56
|
+
localStorage.setItem(key, value);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fetch a JSON resource. Returns `null` on:
|
|
65
|
+
* - Network failure (offline, dev-server not running)
|
|
66
|
+
* - Non-2xx response
|
|
67
|
+
* - JSON parse failure
|
|
68
|
+
*
|
|
69
|
+
* Used for boot-time API calls where the editor must continue working even
|
|
70
|
+
* if the dev-server's themeFileApi (or component-config endpoints) is not
|
|
71
|
+
* available — the caller falls through to defaults.
|
|
72
|
+
*/
|
|
73
|
+
export async function safeFetch<T = unknown>(
|
|
74
|
+
url: string,
|
|
75
|
+
opts?: RequestInit,
|
|
76
|
+
): Promise<T | null> {
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(url, opts);
|
|
79
|
+
if (!res.ok) return null;
|
|
80
|
+
try {
|
|
81
|
+
return (await res.json()) as T;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Theme } from './themeTypes';
|
|
2
|
+
import { activeFileName } from './editorConfigStore';
|
|
3
|
+
import { migrateThemeFonts } from './fontMigration';
|
|
4
|
+
import { applyFontSources, applyFontStacks } from './fontLoader';
|
|
5
|
+
import { loadFromFile, seedComponentsFromApi } from './editorStore';
|
|
6
|
+
import { getActiveComponentConfig } from './componentConfigService';
|
|
7
|
+
import { safeFetch } from './storage';
|
|
8
|
+
|
|
9
|
+
interface ComponentSummaryDto {
|
|
10
|
+
name: string;
|
|
11
|
+
activeFile: string;
|
|
12
|
+
productionFile: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ListComponentsDto {
|
|
16
|
+
components: ComponentSummaryDto[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetch the active theme from the server and apply its CSS variables
|
|
21
|
+
* to :root before the app mounts. Seeds the editor store so PaletteEditors
|
|
22
|
+
* initialize from the theme instead of stale localStorage.
|
|
23
|
+
*
|
|
24
|
+
* Routes the theme through `loadFromFile` so palette-derived vars in
|
|
25
|
+
* `deriveCssVars` correctly overwrite any stale hexes baked into
|
|
26
|
+
* `theme.cssVariables` by `handleSave`'s scrape. Writing
|
|
27
|
+
* `theme.cssVariables` directly to inline :root (the previous approach)
|
|
28
|
+
* bypassed the store and left the subscriber's `lastApplied` diff cache
|
|
29
|
+
* out of sync, so palette-derived values stayed stale until a
|
|
30
|
+
* `PaletteEditor` mounted and re-emitted them.
|
|
31
|
+
*
|
|
32
|
+
* Network/parse failures fall through silently — `tokens.css` provides
|
|
33
|
+
* defaults and the components slice stays empty until first edit. We use
|
|
34
|
+
* `safeFetch` (instead of empty try/catch) to make the silence intentional.
|
|
35
|
+
*/
|
|
36
|
+
export async function initializeTheme(): Promise<void> {
|
|
37
|
+
const theme = await safeFetch<Theme>('/api/themes/active');
|
|
38
|
+
if (theme) {
|
|
39
|
+
migrateThemeFonts(theme);
|
|
40
|
+
loadFromFile(theme);
|
|
41
|
+
if (theme.fontSources && theme.fontSources.length > 0) {
|
|
42
|
+
applyFontSources(theme.fontSources);
|
|
43
|
+
}
|
|
44
|
+
if (theme.fontStacks && theme.fontStacks.length > 0) {
|
|
45
|
+
applyFontStacks(theme.fontStacks, theme.fontSources ?? []);
|
|
46
|
+
}
|
|
47
|
+
const fileName = theme._fileName || 'default';
|
|
48
|
+
activeFileName.set(fileName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const list = await safeFetch<ListComponentsDto>('/api/component-configs');
|
|
52
|
+
if (list && Array.isArray(list.components)) {
|
|
53
|
+
const configs: Record<
|
|
54
|
+
string,
|
|
55
|
+
{ activeFile: string; aliases: Record<string, string>; config?: Record<string, unknown>; schemaVersion?: number }
|
|
56
|
+
> = {};
|
|
57
|
+
await Promise.all(
|
|
58
|
+
list.components.map(async (c) => {
|
|
59
|
+
const cfg = await getActiveComponentConfig(c.name);
|
|
60
|
+
if (cfg) {
|
|
61
|
+
configs[c.name] = {
|
|
62
|
+
activeFile: c.activeFile,
|
|
63
|
+
aliases: cfg.aliases,
|
|
64
|
+
config: cfg.config,
|
|
65
|
+
schemaVersion: cfg.schemaVersion,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
if (Object.keys(configs).length > 0) {
|
|
71
|
+
seedComponentsFromApi(configs);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|