@motion-proto/live-tokens 0.1.1 → 0.3.2
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 +168 -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 +46 -20
- 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 +257 -78
- 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 -30
- 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 +42 -10
- 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 -39
- package/src/{showcase → ui}/TextTab.svelte +27 -27
- 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/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 -144
- package/src/showcase/BackupBrowser.svelte +0 -617
- package/src/showcase/ComponentsTab.svelte +0 -105
- 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 -2655
- package/src/showcase/VisualsTab.svelte +0 -231
- package/src/showcase/demos/BadgeDemo.svelte +0 -56
- package/src/showcase/demos/CardDemo.svelte +0 -50
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
- package/src/showcase/demos/DialogDemo.svelte +0 -42
- package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
- package/src/showcase/demos/NotificationDemo.svelte +0 -147
- package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
- package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
- package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
- package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
- package/src/showcase/demos/TabBarDemo.svelte +0 -58
- package/src/showcase/demos/TooltipDemo.svelte +0 -52
- 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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central editor state store — the single mutation funnel.
|
|
3
|
+
*
|
|
4
|
+
* All editor state (palettes, fonts, shadows, overlays, columns, ad-hoc CSS
|
|
5
|
+
* vars) lives in one `EditorState` tree. Every change must go through
|
|
6
|
+
* `mutate` (or a transaction). History is captured automatically; the
|
|
7
|
+
* renderer subscribes and writes derived CSS vars to :root via `cssVarSync`
|
|
8
|
+
* (which fans out to the parent document for the overlay iframe).
|
|
9
|
+
*
|
|
10
|
+
* This module is now a barrel: it re-exports the public API from
|
|
11
|
+
* - `editorCore` (history machine + transaction/session primitives)
|
|
12
|
+
* - `slices/*` (per-domain factories + actions)
|
|
13
|
+
* - `editorRenderer` (DOM subscriber)
|
|
14
|
+
* - `editorPersistence` (debounced localStorage)
|
|
15
|
+
*
|
|
16
|
+
* Component-config + theme migrations live in `./migrations` (Wave 4).
|
|
17
|
+
* `loadComponentActive` / `seedComponentsFromApi` / `loadFromFile` / `toTheme`
|
|
18
|
+
* orchestrate across slices and stay here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { CssVarRef, EditorState } from './editorTypes';
|
|
22
|
+
import type { Theme } from './themeTypes';
|
|
23
|
+
import { KNOWN_COMPONENT_CONFIG_KEYS } from './componentConfigKeys';
|
|
24
|
+
import {
|
|
25
|
+
CURRENT_THEME_SCHEMA_VERSION,
|
|
26
|
+
CURRENT_COMPONENT_SCHEMA_VERSION,
|
|
27
|
+
runMigrations,
|
|
28
|
+
} from './migrations';
|
|
29
|
+
import { renamePrimaryPaletteKey } from './migrations/2026-05-13-primary-to-brand';
|
|
30
|
+
import { __resetRendererCacheForTests, installRenderer } from './editorRenderer';
|
|
31
|
+
import {
|
|
32
|
+
store,
|
|
33
|
+
mutate,
|
|
34
|
+
setEmptyStateFactory as setCoreEmptyStateFactory,
|
|
35
|
+
setPersistHook,
|
|
36
|
+
resetHistoryForLoad,
|
|
37
|
+
__resetCoreForTests,
|
|
38
|
+
} from './editorCore';
|
|
39
|
+
import {
|
|
40
|
+
schedulePersist,
|
|
41
|
+
setEmptyStateFactory as setPersistenceEmptyStateFactory,
|
|
42
|
+
ensureHydrated,
|
|
43
|
+
initializeEditorStore,
|
|
44
|
+
} from './editorPersistence';
|
|
45
|
+
import {
|
|
46
|
+
DEFAULT_COLUMNS,
|
|
47
|
+
columnsEqualsDefault,
|
|
48
|
+
columnsToVars,
|
|
49
|
+
loadColumnsFromVars,
|
|
50
|
+
} from './slices/columns';
|
|
51
|
+
import {
|
|
52
|
+
loadOverlaysFromVars,
|
|
53
|
+
makeDefaultOverlaysState,
|
|
54
|
+
overlaysEqualsDefault,
|
|
55
|
+
overlaysToVars,
|
|
56
|
+
} from './slices/overlays';
|
|
57
|
+
import {
|
|
58
|
+
loadShadowsFromVars,
|
|
59
|
+
shadowsToVars,
|
|
60
|
+
} from './slices/shadows';
|
|
61
|
+
import { makeDefaultGradients } from './slices/gradients';
|
|
62
|
+
import {
|
|
63
|
+
componentBaseline,
|
|
64
|
+
loadComponentsFromVars,
|
|
65
|
+
notifyComponentSavedChanged,
|
|
66
|
+
setSavedComponentBaseline,
|
|
67
|
+
__resetComponentsForTests,
|
|
68
|
+
} from './slices/components';
|
|
69
|
+
|
|
70
|
+
function emptyState(): EditorState {
|
|
71
|
+
return {
|
|
72
|
+
palettes: {},
|
|
73
|
+
fonts: { sources: [], stacks: [] },
|
|
74
|
+
shadows: {
|
|
75
|
+
globals: {
|
|
76
|
+
angle: 90, opacityMin: 0.15, opacityMax: 0.15, opacityLocked: true,
|
|
77
|
+
distanceMin: 1, distanceMax: 25,
|
|
78
|
+
blurMin: 2, blurMax: 50, blurLocked: false,
|
|
79
|
+
sizeMin: 0, sizeMax: 0, sizeLocked: true,
|
|
80
|
+
hue: 0, saturation: 0, lightness: 0,
|
|
81
|
+
},
|
|
82
|
+
tokens: [],
|
|
83
|
+
overrides: {},
|
|
84
|
+
},
|
|
85
|
+
overlays: makeDefaultOverlaysState(),
|
|
86
|
+
columns: { ...DEFAULT_COLUMNS },
|
|
87
|
+
components: {},
|
|
88
|
+
gradients: { tokens: makeDefaultGradients() },
|
|
89
|
+
cssVars: {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── editorCore re-exports ─────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
editorState,
|
|
97
|
+
mutate,
|
|
98
|
+
transaction,
|
|
99
|
+
beginScope,
|
|
100
|
+
commitScope,
|
|
101
|
+
cancelScope,
|
|
102
|
+
beginSliderGesture,
|
|
103
|
+
undo,
|
|
104
|
+
redo,
|
|
105
|
+
canUndo,
|
|
106
|
+
canRedo,
|
|
107
|
+
dirty,
|
|
108
|
+
markSaved,
|
|
109
|
+
__getHistoryLengths,
|
|
110
|
+
__getPastAt,
|
|
111
|
+
} from './editorCore';
|
|
112
|
+
export type { Scope } from './editorCore';
|
|
113
|
+
|
|
114
|
+
// Wire the factory in both editorCore and editorPersistence. Resetting the
|
|
115
|
+
// store lands a proper EditorState before any helper below runs `mutate`.
|
|
116
|
+
setCoreEmptyStateFactory(emptyState);
|
|
117
|
+
setPersistenceEmptyStateFactory(emptyState);
|
|
118
|
+
store.set(emptyState());
|
|
119
|
+
|
|
120
|
+
// ── Slice re-exports ──────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
DOMAIN_VAR_NAMES,
|
|
124
|
+
} from './slices/domainVars';
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
columnsToVars,
|
|
128
|
+
columnsEqualsDefault,
|
|
129
|
+
COLUMN_VAR_NAMES,
|
|
130
|
+
DEFAULT_COLUMNS,
|
|
131
|
+
parseColumnVars,
|
|
132
|
+
} from './slices/columns';
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
overlaysToVars,
|
|
136
|
+
overlaysEqualsDefault,
|
|
137
|
+
OVERLAY_VAR_NAMES,
|
|
138
|
+
applyOverlayVarsToState,
|
|
139
|
+
makeDefaultOverlaysState,
|
|
140
|
+
overlayTokenToRgba,
|
|
141
|
+
parseRgba,
|
|
142
|
+
RGBA_RE,
|
|
143
|
+
HEX_RE,
|
|
144
|
+
} from './slices/overlays';
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
shadowsToVars,
|
|
148
|
+
applyShadowVarsToState,
|
|
149
|
+
SHADOW_VAR_NAMES,
|
|
150
|
+
SCALE_SHADOW_VARIABLES,
|
|
151
|
+
computeShadowXY,
|
|
152
|
+
shadowTokenCss,
|
|
153
|
+
defaultShadowOverride,
|
|
154
|
+
parseShadowCss,
|
|
155
|
+
seedShadowsFromDom,
|
|
156
|
+
} from './slices/shadows';
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
gradientsToVars,
|
|
160
|
+
makeDefaultGradients,
|
|
161
|
+
setGradient,
|
|
162
|
+
setGradientType,
|
|
163
|
+
setGradientAngle,
|
|
164
|
+
setGradientStop,
|
|
165
|
+
addGradientStop,
|
|
166
|
+
removeGradientStop,
|
|
167
|
+
addGradientToken,
|
|
168
|
+
removeGradientToken,
|
|
169
|
+
} from './slices/gradients';
|
|
170
|
+
|
|
171
|
+
export {
|
|
172
|
+
componentsToVars,
|
|
173
|
+
getComponentOwnedVarNames,
|
|
174
|
+
componentDirty,
|
|
175
|
+
setComponentAlias,
|
|
176
|
+
clearComponentAlias,
|
|
177
|
+
setComponentConfig,
|
|
178
|
+
clearComponentConfig,
|
|
179
|
+
registerComponentSchema,
|
|
180
|
+
getComponentPropertySiblings,
|
|
181
|
+
isComponentPropertyLinked,
|
|
182
|
+
setComponentAliasLinked,
|
|
183
|
+
clearComponentAliasLinked,
|
|
184
|
+
unlinkComponentProperty,
|
|
185
|
+
relinkComponentProperty,
|
|
186
|
+
markComponentSaved,
|
|
187
|
+
} from './slices/components';
|
|
188
|
+
|
|
189
|
+
export {
|
|
190
|
+
setFontSources,
|
|
191
|
+
setFontStacks,
|
|
192
|
+
seedFontsFromTheme,
|
|
193
|
+
} from './slices/fonts';
|
|
194
|
+
|
|
195
|
+
export {
|
|
196
|
+
setPaletteConfig,
|
|
197
|
+
seedPalettesFromTheme,
|
|
198
|
+
} from './slices/palettes';
|
|
199
|
+
|
|
200
|
+
// ── Component-config load orchestration ───────────────────────────────────
|
|
201
|
+
//
|
|
202
|
+
// Component-config migrations now live in `./migrations` (Wave 4): the
|
|
203
|
+
// migration runner pipes `aliases` through any registered transforms whose
|
|
204
|
+
// `fromVersion >= file.schemaVersion`, then `splitAliasesAndConfig` routes
|
|
205
|
+
// literal-valued knobs (per `KNOWN_COMPONENT_CONFIG_KEYS`) into the config
|
|
206
|
+
// bucket and wraps the remainder as `CssVarRef` discriminated unions.
|
|
207
|
+
|
|
208
|
+
function migrateComponentAliases(
|
|
209
|
+
component: string,
|
|
210
|
+
aliases: Record<string, string>,
|
|
211
|
+
fileVersion: number,
|
|
212
|
+
): Record<string, string> {
|
|
213
|
+
return runMigrations('component-config', fileVersion, aliases, { component });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Disk-shape → in-memory split. Routes legacy single-bucket aliases that
|
|
218
|
+
* carry literal-valued knobs (per `KNOWN_COMPONENT_CONFIG_KEYS`) into the
|
|
219
|
+
* config bucket, and wraps the remainder as `CssVarRef` discriminated unions.
|
|
220
|
+
*/
|
|
221
|
+
function splitAliasesAndConfig(
|
|
222
|
+
rawAliases: Record<string, string>,
|
|
223
|
+
rawConfig: Record<string, unknown> | undefined,
|
|
224
|
+
): { aliases: Record<string, CssVarRef>; config: Record<string, unknown> } {
|
|
225
|
+
const aliases: Record<string, CssVarRef> = {};
|
|
226
|
+
const config: Record<string, unknown> = { ...(rawConfig ?? {}) };
|
|
227
|
+
for (const [key, value] of Object.entries(rawAliases)) {
|
|
228
|
+
if (KNOWN_COMPONENT_CONFIG_KEYS.has(key)) {
|
|
229
|
+
if (config[key] === undefined) config[key] = value;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
aliases[key] = value.startsWith('--')
|
|
233
|
+
? { kind: 'token', name: value }
|
|
234
|
+
: { kind: 'literal', value };
|
|
235
|
+
}
|
|
236
|
+
return { aliases, config };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Replace a component's slice with a loaded config file's contents. Uses
|
|
241
|
+
* `mutate()` so the load is one undoable entry; updates the dirty baseline
|
|
242
|
+
* so the post-load state reads clean for this component.
|
|
243
|
+
*
|
|
244
|
+
* `schemaVersion` is the stamp on the loaded file (0 for legacy files
|
|
245
|
+
* with no stamp). The runner applies any migrations between that and
|
|
246
|
+
* `CURRENT_COMPONENT_SCHEMA_VERSION` before the slice is stored — in-memory
|
|
247
|
+
* state is always at the current version.
|
|
248
|
+
*/
|
|
249
|
+
export function loadComponentActive(
|
|
250
|
+
component: string,
|
|
251
|
+
activeFile: string,
|
|
252
|
+
aliases: Record<string, string>,
|
|
253
|
+
config?: Record<string, unknown>,
|
|
254
|
+
schemaVersion: number = 0,
|
|
255
|
+
): void {
|
|
256
|
+
const migrated = migrateComponentAliases(component, aliases, schemaVersion);
|
|
257
|
+
const split = splitAliasesAndConfig(migrated, config);
|
|
258
|
+
mutate(`load ${component}/${activeFile}`, (s) => {
|
|
259
|
+
s.components[component] = { activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
|
|
260
|
+
});
|
|
261
|
+
setSavedComponentBaseline(component, componentBaseline(split));
|
|
262
|
+
notifyComponentSavedChanged();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface ComponentSeed {
|
|
266
|
+
activeFile: string;
|
|
267
|
+
aliases: Record<string, string>;
|
|
268
|
+
config?: Record<string, unknown>;
|
|
269
|
+
schemaVersion?: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Boot-path hydration from the server's /api/component-configs fetch. No
|
|
274
|
+
* history entry, no dirty flag — components are clean relative to disk.
|
|
275
|
+
*
|
|
276
|
+
* Each seed may carry a `schemaVersion`; absent entries are treated as 0
|
|
277
|
+
* and migrated up to current.
|
|
278
|
+
*/
|
|
279
|
+
export function seedComponentsFromApi(
|
|
280
|
+
configs: Record<string, ComponentSeed>,
|
|
281
|
+
): void {
|
|
282
|
+
store.update((s) => {
|
|
283
|
+
s.components = {};
|
|
284
|
+
for (const [comp, cfg] of Object.entries(configs)) {
|
|
285
|
+
const migrated = migrateComponentAliases(comp, cfg.aliases, cfg.schemaVersion ?? 0);
|
|
286
|
+
const split = splitAliasesAndConfig(migrated, cfg.config);
|
|
287
|
+
s.components[comp] = { activeFile: cfg.activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
|
|
288
|
+
setSavedComponentBaseline(comp, componentBaseline(split));
|
|
289
|
+
}
|
|
290
|
+
return s;
|
|
291
|
+
});
|
|
292
|
+
notifyComponentSavedChanged();
|
|
293
|
+
schedulePersist();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Theme load / save ──────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Per-domain loader: routes a freshly-loaded theme's `cssVariables` bag
|
|
300
|
+
* into typed state on `next`, then removes the routed entries so the
|
|
301
|
+
* remainder lands as the catch-all `cssVars` bag. Each loader owns its
|
|
302
|
+
* domain's parser + name list; this table is the only place editorStore
|
|
303
|
+
* needs to know about the per-slice loading contract.
|
|
304
|
+
*/
|
|
305
|
+
type DomainLoader = (next: EditorState, rawVars: Record<string, string>) => void;
|
|
306
|
+
|
|
307
|
+
const domainLoaders: Record<string, DomainLoader> = {
|
|
308
|
+
columns: loadColumnsFromVars,
|
|
309
|
+
overlays: loadOverlaysFromVars,
|
|
310
|
+
shadows: loadShadowsFromVars,
|
|
311
|
+
components: loadComponentsFromVars,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Replace state with a loaded theme. Clears history and marks saved —
|
|
316
|
+
* "open a different document" semantics. Undo cannot cross a theme load.
|
|
317
|
+
*
|
|
318
|
+
* Reads `theme.schemaVersion` (absent = 0) and runs theme migrations up to
|
|
319
|
+
* `CURRENT_THEME_SCHEMA_VERSION` before splitting the bag into domains.
|
|
320
|
+
*/
|
|
321
|
+
export function loadFromFile(theme: Theme): void {
|
|
322
|
+
const next = emptyState();
|
|
323
|
+
next.palettes = renamePrimaryPaletteKey(structuredClone(theme.editorConfigs ?? {}));
|
|
324
|
+
next.fonts.sources = structuredClone(theme.fontSources ?? []);
|
|
325
|
+
next.fonts.stacks = structuredClone(theme.fontStacks ?? []);
|
|
326
|
+
const rawVars = runMigrations(
|
|
327
|
+
'theme',
|
|
328
|
+
theme.schemaVersion ?? 0,
|
|
329
|
+
theme.cssVariables ?? {},
|
|
330
|
+
);
|
|
331
|
+
// Route domain-owned entries out of the catch-all bag into typed state.
|
|
332
|
+
// Order doesn't matter — each loader claims a disjoint set of var names
|
|
333
|
+
// (or in components' case, vars that wouldn't have been in the theme to
|
|
334
|
+
// begin with).
|
|
335
|
+
for (const load of Object.values(domainLoaders)) load(next, rawVars);
|
|
336
|
+
next.cssVars = rawVars;
|
|
337
|
+
resetHistoryForLoad();
|
|
338
|
+
store.set(next);
|
|
339
|
+
schedulePersist();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Serialize current state for saving. Domains with their own typed state
|
|
344
|
+
* (columns, overlays, shadows) fold derived vars into `cssVariables` only
|
|
345
|
+
* when they diverge from defaults; the catch-all `cssVars` bag carries
|
|
346
|
+
* everything not yet migrated to a typed domain.
|
|
347
|
+
*
|
|
348
|
+
* Stamps the file with `CURRENT_THEME_SCHEMA_VERSION` so future loads can
|
|
349
|
+
* skip migrations the file is already past.
|
|
350
|
+
*/
|
|
351
|
+
export function toTheme(state: EditorState, meta: { name: string }): Theme {
|
|
352
|
+
const now = new Date().toISOString();
|
|
353
|
+
const cssVariables: Record<string, string> = { ...state.cssVars };
|
|
354
|
+
if (!columnsEqualsDefault(state.columns)) {
|
|
355
|
+
Object.assign(cssVariables, columnsToVars(state.columns));
|
|
356
|
+
}
|
|
357
|
+
if (!overlaysEqualsDefault(state.overlays)) {
|
|
358
|
+
Object.assign(cssVariables, overlaysToVars(state.overlays));
|
|
359
|
+
}
|
|
360
|
+
if (state.shadows.tokens.length > 0) {
|
|
361
|
+
Object.assign(cssVariables, shadowsToVars(state.shadows));
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
name: meta.name,
|
|
365
|
+
createdAt: now,
|
|
366
|
+
updatedAt: now,
|
|
367
|
+
editorConfigs: state.palettes,
|
|
368
|
+
cssVariables,
|
|
369
|
+
fontSources: state.fonts.sources,
|
|
370
|
+
fontStacks: state.fonts.stacks,
|
|
371
|
+
schemaVersion: CURRENT_THEME_SCHEMA_VERSION,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Persistence ────────────────────────────────────────────────────────────
|
|
376
|
+
//
|
|
377
|
+
// `schedulePersist` + `hydrate` + the eager-hydrate gate live in
|
|
378
|
+
// `./editorPersistence`. The barrel re-exports `initializeEditorStore` for
|
|
379
|
+
// API parity.
|
|
380
|
+
|
|
381
|
+
export { initializeEditorStore };
|
|
382
|
+
/** Idempotent host hook — call once during boot. Alias for `initializeEditorStore`
|
|
383
|
+
* matching the `module.init()` convention used by `cssVarSync` / `router` /
|
|
384
|
+
* `columnsOverlay` so `main.ts` can call them uniformly. */
|
|
385
|
+
export const init = initializeEditorStore;
|
|
386
|
+
|
|
387
|
+
// ── Test-only reset ────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Test-only: clear all history + transient session/transaction state and
|
|
391
|
+
* reset the store to `emptyState()`. Not exported from the public barrel.
|
|
392
|
+
*/
|
|
393
|
+
export function __resetForTests(): void {
|
|
394
|
+
__resetCoreForTests();
|
|
395
|
+
__resetRendererCacheForTests();
|
|
396
|
+
__resetComponentsForTests();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Wiring ─────────────────────────────────────────────────────────────────
|
|
400
|
+
//
|
|
401
|
+
// `setPersistHook(schedulePersist)` lets editorCore's mutate/undo/redo
|
|
402
|
+
// debounce-persist through the same path. `ensureHydrated()` runs the
|
|
403
|
+
// eager localStorage load (pre-Svelte-mount so children see persisted
|
|
404
|
+
// state in onMount). `installRenderer()` runs the `store.subscribe(...)`
|
|
405
|
+
// wiring legacy `import '.../editorStore'` paths depended on; it must run
|
|
406
|
+
// *after* this module's exports are initialized — the renderer imports
|
|
407
|
+
// `editorState` back from editorCore, and calling it earlier would TDZ
|
|
408
|
+
// because of the circular import.
|
|
409
|
+
setPersistHook(schedulePersist);
|
|
410
|
+
ensureHydrated();
|
|
411
|
+
export { deriveCssVars } from './editorRenderer';
|
|
412
|
+
installRenderer();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { PaletteConfig, FontSource, FontStack } from './themeTypes';
|
|
2
|
+
|
|
3
|
+
export interface ShadowGlobals {
|
|
4
|
+
angle: number;
|
|
5
|
+
opacityMin: number; opacityMax: number; opacityLocked: boolean;
|
|
6
|
+
distanceMin: number; distanceMax: number;
|
|
7
|
+
blurMin: number; blurMax: number; blurLocked: boolean;
|
|
8
|
+
sizeMin: number; sizeMax: number; sizeLocked: boolean;
|
|
9
|
+
hue: number; saturation: number; lightness: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ShadowToken {
|
|
13
|
+
variable: string;
|
|
14
|
+
x: number; y: number; blur: number; spread: number;
|
|
15
|
+
opacity: number; hue: number; saturation: number; lightness: number;
|
|
16
|
+
angle: number; distance: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ShadowOverrideFlags {
|
|
20
|
+
angle: boolean; opacity: boolean; color: boolean;
|
|
21
|
+
distance: boolean; blur: boolean; size: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OverlayToken {
|
|
25
|
+
variable: string; label: string;
|
|
26
|
+
r: number; g: number; b: number; opacity: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OverlayChannelGlobals {
|
|
30
|
+
hue: number; saturation: number; lightness: number;
|
|
31
|
+
opacityMin: number; opacityMax: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface OverlayGlobals {
|
|
35
|
+
overlay: OverlayChannelGlobals;
|
|
36
|
+
hover: OverlayChannelGlobals;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ColumnsState {
|
|
40
|
+
count: number;
|
|
41
|
+
maxWidth: number;
|
|
42
|
+
gutter: number;
|
|
43
|
+
margin: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type CssVarRef =
|
|
47
|
+
| { kind: 'token'; name: string }
|
|
48
|
+
| { kind: 'literal'; value: string };
|
|
49
|
+
|
|
50
|
+
export interface ComponentSlice {
|
|
51
|
+
activeFile: string;
|
|
52
|
+
aliases: Record<string, CssVarRef>;
|
|
53
|
+
config: Record<string, unknown>;
|
|
54
|
+
unlinked?: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type GradientType = 'linear' | 'radial';
|
|
58
|
+
|
|
59
|
+
export interface GradientTokenStop {
|
|
60
|
+
/** 0–100 percentage along the gradient axis. */
|
|
61
|
+
position: number;
|
|
62
|
+
/** CSS variable name the stop resolves through (e.g. '--color-brand-500'). */
|
|
63
|
+
color: string;
|
|
64
|
+
/** 0–100 alpha applied to the stop's color. Defaults to 100 (fully opaque). */
|
|
65
|
+
opacity?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface GradientToken {
|
|
69
|
+
/** Output CSS variable, e.g. '--gradient-1'. */
|
|
70
|
+
variable: string;
|
|
71
|
+
type: GradientType;
|
|
72
|
+
/** Degrees, applies to linear only. */
|
|
73
|
+
angle: number;
|
|
74
|
+
stops: GradientTokenStop[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Single source of truth for everything a saved token file depends on, plus
|
|
79
|
+
* the domain state currently scattered across VariablesTab local `let` fields.
|
|
80
|
+
* View state (tab selection, dialog flags, drag payloads, editing drafts)
|
|
81
|
+
* stays out of this tree.
|
|
82
|
+
*/
|
|
83
|
+
export interface EditorState {
|
|
84
|
+
palettes: Record<string, PaletteConfig>;
|
|
85
|
+
fonts: { sources: FontSource[]; stacks: FontStack[] };
|
|
86
|
+
shadows: {
|
|
87
|
+
globals: ShadowGlobals;
|
|
88
|
+
tokens: ShadowToken[];
|
|
89
|
+
overrides: Record<string, ShadowOverrideFlags>;
|
|
90
|
+
};
|
|
91
|
+
overlays: {
|
|
92
|
+
tokens: OverlayToken[];
|
|
93
|
+
hoverTokens: OverlayToken[];
|
|
94
|
+
globals: OverlayGlobals;
|
|
95
|
+
};
|
|
96
|
+
columns: ColumnsState;
|
|
97
|
+
components: Record<string, ComponentSlice>;
|
|
98
|
+
gradients: { tokens: GradientToken[] };
|
|
99
|
+
cssVars: Record<string, string>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
export type EditorView = 'tokens' | 'components';
|
|
4
|
+
export type SidebarCondensed = boolean | 'auto';
|
|
5
|
+
|
|
6
|
+
const VIEW_KEY = 'lt.editorView';
|
|
7
|
+
const CONDENSED_KEY = 'lt.sidebarCondensed';
|
|
8
|
+
|
|
9
|
+
function readView(): EditorView {
|
|
10
|
+
try {
|
|
11
|
+
const v = localStorage.getItem(VIEW_KEY);
|
|
12
|
+
if (v === 'components' || v === 'tokens') return v;
|
|
13
|
+
} catch {}
|
|
14
|
+
return 'tokens';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readCondensed(): SidebarCondensed {
|
|
18
|
+
try {
|
|
19
|
+
const v = localStorage.getItem(CONDENSED_KEY);
|
|
20
|
+
if (v === 'true') return true;
|
|
21
|
+
if (v === 'false') return false;
|
|
22
|
+
} catch {}
|
|
23
|
+
return 'auto';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const editorView = writable<EditorView>(readView());
|
|
27
|
+
export const sidebarCondensed = writable<SidebarCondensed>(readCondensed());
|
|
28
|
+
export const selectedComponent = writable<string>('button');
|
|
29
|
+
|
|
30
|
+
editorView.subscribe((v) => {
|
|
31
|
+
try { localStorage.setItem(VIEW_KEY, v); } catch {}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
sidebarCondensed.subscribe((v) => {
|
|
35
|
+
try { localStorage.setItem(CONDENSED_KEY, v === 'auto' ? 'auto' : String(v)); } catch {}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Cross-window sync: parent and overlay iframe share localStorage but each have
|
|
39
|
+
// their own module state. `storage` events fire in the *other* window when one
|
|
40
|
+
// writes, so we mirror the new value into this window's store.
|
|
41
|
+
if (typeof window !== 'undefined') {
|
|
42
|
+
window.addEventListener('storage', (e) => {
|
|
43
|
+
if (e.key === VIEW_KEY) {
|
|
44
|
+
if (e.newValue === 'tokens' || e.newValue === 'components') editorView.set(e.newValue);
|
|
45
|
+
} else if (e.key === CONDENSED_KEY) {
|
|
46
|
+
if (e.newValue === 'true') sidebarCondensed.set(true);
|
|
47
|
+
else if (e.newValue === 'false') sidebarCondensed.set(false);
|
|
48
|
+
else if (e.newValue === 'auto') sidebarCondensed.set('auto');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function setEditorView(v: EditorView) {
|
|
54
|
+
editorView.set(v);
|
|
55
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side REST helpers for any "versioned file resource" — a class of
|
|
3
|
+
* editable artifact whose lifecycle mirrors the theme files: list / load /
|
|
4
|
+
* save / delete + an active pointer + a production pointer.
|
|
5
|
+
*
|
|
6
|
+
* `themeService.ts` and `componentConfigService.ts` previously reimplemented
|
|
7
|
+
* the same fetch shape. They now both consume `versionedFileResource(...)`
|
|
8
|
+
* with their resource-specific URL.
|
|
9
|
+
*
|
|
10
|
+
* Pure URL construction + fetch — no DOM, no filesystem, safe for any browser
|
|
11
|
+
* or test harness that has a `fetch` global.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface VersionedFileResourceClientOptions {
|
|
15
|
+
/** REST endpoint root, e.g. `/api/themes` or `/api/component-configs/button`. */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VersionedFileResourceClient<TItem, TMeta, TProductionInfo> {
|
|
20
|
+
list(): Promise<{ files: TMeta[]; activeFile?: string; productionFile?: string }>;
|
|
21
|
+
load(fileName: string): Promise<TItem>;
|
|
22
|
+
save(fileName: string, data: TItem): Promise<void>;
|
|
23
|
+
remove(fileName: string): Promise<void>;
|
|
24
|
+
getActive(): Promise<TItem | null>;
|
|
25
|
+
setActive(fileName: string): Promise<void>;
|
|
26
|
+
getProductionInfo(): Promise<TProductionInfo>;
|
|
27
|
+
setProduction(fileName: string): Promise<TProductionInfo & { ok: boolean }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a CRUD client bound to a single resource root. All routes follow the
|
|
32
|
+
* same shape:
|
|
33
|
+
* GET {base} → list
|
|
34
|
+
* GET {base}/:name → load
|
|
35
|
+
* PUT {base}/:name → save
|
|
36
|
+
* DELETE {base}/:name → delete
|
|
37
|
+
* GET {base}/active → load active
|
|
38
|
+
* PUT {base}/active {name} → set active
|
|
39
|
+
* GET {base}/production → production info
|
|
40
|
+
* PUT {base}/production {name} → set production
|
|
41
|
+
*
|
|
42
|
+
* Generic over the payload types so theme & component-config can share the
|
|
43
|
+
* same plumbing while keeping their own response/info shapes.
|
|
44
|
+
*/
|
|
45
|
+
export function versionedFileResource<TItem, TMeta, TProductionInfo>(
|
|
46
|
+
opts: VersionedFileResourceClientOptions,
|
|
47
|
+
): VersionedFileResourceClient<TItem, TMeta, TProductionInfo> {
|
|
48
|
+
const { baseUrl } = opts;
|
|
49
|
+
|
|
50
|
+
async function readJsonError(res: Response, fallback: string): Promise<string> {
|
|
51
|
+
const err = await res.json().catch(() => ({ error: fallback }));
|
|
52
|
+
return err.error || fallback;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function list(): Promise<{ files: TMeta[]; activeFile?: string; productionFile?: string }> {
|
|
56
|
+
const res = await fetch(baseUrl);
|
|
57
|
+
if (!res.ok) throw new Error(`Failed to list ${baseUrl}`);
|
|
58
|
+
return res.json();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function load(fileName: string): Promise<TItem> {
|
|
62
|
+
const res = await fetch(`${baseUrl}/${encodeURIComponent(fileName)}`);
|
|
63
|
+
if (!res.ok) throw new Error(`Failed to load: ${fileName}`);
|
|
64
|
+
return res.json();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function save(fileName: string, data: TItem): Promise<void> {
|
|
68
|
+
const res = await fetch(`${baseUrl}/${encodeURIComponent(fileName)}`, {
|
|
69
|
+
method: 'PUT',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify(data),
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
throw new Error(await readJsonError(res, 'Save failed'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function remove(fileName: string): Promise<void> {
|
|
79
|
+
const res = await fetch(`${baseUrl}/${encodeURIComponent(fileName)}`, {
|
|
80
|
+
method: 'DELETE',
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
throw new Error(await readJsonError(res, 'Delete failed'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getActive(): Promise<TItem | null> {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`${baseUrl}/active`);
|
|
90
|
+
if (!res.ok) return null;
|
|
91
|
+
return res.json();
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function setActive(fileName: string): Promise<void> {
|
|
98
|
+
const res = await fetch(`${baseUrl}/active`, {
|
|
99
|
+
method: 'PUT',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ name: fileName }),
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
throw new Error(await readJsonError(res, 'Set active failed'));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function getProductionInfo(): Promise<TProductionInfo> {
|
|
109
|
+
const res = await fetch(`${baseUrl}/production`);
|
|
110
|
+
if (!res.ok) throw new Error('Failed to get production info');
|
|
111
|
+
return res.json();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function setProduction(fileName: string): Promise<TProductionInfo & { ok: boolean }> {
|
|
115
|
+
const res = await fetch(`${baseUrl}/production`, {
|
|
116
|
+
method: 'PUT',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ name: fileName }),
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
throw new Error(await readJsonError(res, 'Set production failed'));
|
|
122
|
+
}
|
|
123
|
+
return res.json();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { list, load, save, remove, getActive, setActive, getProductionInfo, setProduction };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Sanitize a display name to a safe file name. Pure — no DOM, no fs. */
|
|
130
|
+
export function sanitizeFileName(name: string): string {
|
|
131
|
+
return (
|
|
132
|
+
name
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.trim()
|
|
135
|
+
.replace(/\s+/g, '-')
|
|
136
|
+
.replace(/[^a-z0-9\-_]/g, '')
|
|
137
|
+
.replace(/-+/g, '-')
|
|
138
|
+
.replace(/^-|-$/g, '') || 'unnamed'
|
|
139
|
+
);
|
|
140
|
+
}
|