@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,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State-core for the editor store.
|
|
3
|
+
*
|
|
4
|
+
* Owns the writable, history stacks (undo/redo), and the unified `Scope`
|
|
5
|
+
* primitive that brackets a series of mutations. Exposes `editorState`
|
|
6
|
+
* (Readable), `mutate` for unscoped one-shots, `transaction(label, fn)` for
|
|
7
|
+
* sync closure form, `beginSliderGesture(label)` for window-pointerup wiring,
|
|
8
|
+
* and the `beginScope/commitScope/cancelScope` trio for explicit handle-based
|
|
9
|
+
* scopes (the only form palette panels need). Domain slices import `mutate`
|
|
10
|
+
* and friends from here.
|
|
11
|
+
*
|
|
12
|
+
* The Scope primitive (M6 fold) — two orthogonal axes:
|
|
13
|
+
* - `collapseToOne` — on commit, all intra-scope mutations collapse to a
|
|
14
|
+
* single history entry (the pre-scope snapshot).
|
|
15
|
+
* - `clipUndoFloor` — while the scope is active, undo() is clipped to the
|
|
16
|
+
* scope's start; on commit, intra-scope past entries are dropped from
|
|
17
|
+
* history so the snapshot becomes the single undo target.
|
|
18
|
+
*
|
|
19
|
+
* Mapping to use cases:
|
|
20
|
+
* - `mutate` — unscoped one-shot (no scope at all).
|
|
21
|
+
* - drag gesture / atomic — scope { collapseToOne: true, clipUndoFloor: false }.
|
|
22
|
+
* - palette panel session — scope { collapseToOne: true, clipUndoFloor: true }.
|
|
23
|
+
*
|
|
24
|
+
* Two side-effect hooks are wired by `editorStore.ts` at boot:
|
|
25
|
+
* - `setEmptyStateFactory` — produces a fresh state tree.
|
|
26
|
+
* - `setPersistHook` — debounced localStorage write.
|
|
27
|
+
*
|
|
28
|
+
* Hooks are nullable on first import so the module evaluates cleanly even
|
|
29
|
+
* when consumed from contexts that don't need persistence (SSR, isolated
|
|
30
|
+
* unit tests).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { writable, derived, get, type Readable } from 'svelte/store';
|
|
34
|
+
import type { EditorState } from './editorTypes';
|
|
35
|
+
|
|
36
|
+
const HISTORY_MAX = 100;
|
|
37
|
+
|
|
38
|
+
// ── Hooks (wired by editorStore at boot) ──────────────────────────────────
|
|
39
|
+
|
|
40
|
+
let emptyStateFactory: () => EditorState = () => ({} as EditorState);
|
|
41
|
+
let persistHook: () => void = () => {};
|
|
42
|
+
|
|
43
|
+
export function setEmptyStateFactory(fn: () => EditorState): void {
|
|
44
|
+
emptyStateFactory = fn;
|
|
45
|
+
}
|
|
46
|
+
export function setPersistHook(fn: () => void): void {
|
|
47
|
+
persistHook = fn;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Trigger the persistence hook. Used by seed paths that bypass `mutate`. */
|
|
51
|
+
export function persist(): void {
|
|
52
|
+
persistHook();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Store + history machine ───────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export const store = writable<EditorState>(emptyStateFactory());
|
|
58
|
+
|
|
59
|
+
// History stacks hold snapshots that are independent of the current state
|
|
60
|
+
// reference (always created via structuredClone at push time). This lets
|
|
61
|
+
// mutate / undo / redo use in-place mutation on the live state without
|
|
62
|
+
// corrupting history entries.
|
|
63
|
+
const past: EditorState[] = [];
|
|
64
|
+
const future: EditorState[] = [];
|
|
65
|
+
let savedAtIndex = 0;
|
|
66
|
+
|
|
67
|
+
// A counter that bumps on every history-affecting change, so `derived`
|
|
68
|
+
// stores (canUndo, canRedo, dirty) re-evaluate. The history arrays
|
|
69
|
+
// themselves live outside Svelte reactivity.
|
|
70
|
+
const historyTick = writable(0);
|
|
71
|
+
function bumpTick() { historyTick.update((n) => n + 1); }
|
|
72
|
+
|
|
73
|
+
/** Push `entry` onto past[], handling overflow + clearing future. */
|
|
74
|
+
function pushPast(entry: EditorState): void {
|
|
75
|
+
past.push(entry);
|
|
76
|
+
if (past.length > HISTORY_MAX) {
|
|
77
|
+
past.shift();
|
|
78
|
+
if (savedAtIndex > 0) savedAtIndex--;
|
|
79
|
+
}
|
|
80
|
+
future.length = 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Scope primitive ───────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A handle for a bracketed series of mutations. Returned by `beginScope` and
|
|
87
|
+
* passed back to `commitScope`/`cancelScope`. The shape is public but its
|
|
88
|
+
* fields are read-only from outside this module — callers treat it as an
|
|
89
|
+
* opaque ticket.
|
|
90
|
+
*
|
|
91
|
+
* Two orthogonal axes:
|
|
92
|
+
* - collapseToOne: on commit, everything in the scope collapses to one
|
|
93
|
+
* history entry (the pre-scope snapshot).
|
|
94
|
+
* - clipUndoFloor: while the scope is active, undo() cannot pop past
|
|
95
|
+
* entries pushed before the scope began; on commit, intra-scope past
|
|
96
|
+
* entries are dropped (past.length = historyIdx).
|
|
97
|
+
*/
|
|
98
|
+
export interface Scope {
|
|
99
|
+
readonly snapshot: EditorState;
|
|
100
|
+
readonly label: string;
|
|
101
|
+
readonly collapseToOne: boolean;
|
|
102
|
+
readonly clipUndoFloor: boolean;
|
|
103
|
+
/** past.length captured at scope start — only consulted when clipUndoFloor. */
|
|
104
|
+
readonly historyIdx: number;
|
|
105
|
+
/** Set by `mutate()` when at least one mutation is applied within the scope. */
|
|
106
|
+
changed: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Two scope slots: one for in-flight non-clipping scopes (drag gestures,
|
|
110
|
+
// atomic edits), one for clipping scopes (palette edit sessions). They can
|
|
111
|
+
// coexist — a slider drag inside an open palette session is the canonical
|
|
112
|
+
// case — so they're tracked independently. `mutate` consults `transactionScope`
|
|
113
|
+
// to suppress per-mutate pushes; `undo` consults `clippingScope` to enforce
|
|
114
|
+
// the floor.
|
|
115
|
+
let transactionScope: Scope | null = null;
|
|
116
|
+
let clippingScope: Scope | null = null;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Open a new scope. Returns a handle the caller passes back to
|
|
120
|
+
* `commitScope`/`cancelScope`. The handle's `clipUndoFloor` flag determines
|
|
121
|
+
* which internal slot the scope occupies; if a slot is already filled, the
|
|
122
|
+
* prior scope is auto-committed (transaction slot) or auto-committed
|
|
123
|
+
* (clipping slot) — matching the prior wrapper behavior.
|
|
124
|
+
*/
|
|
125
|
+
export function beginScope(opts: {
|
|
126
|
+
label: string;
|
|
127
|
+
collapseToOne: boolean;
|
|
128
|
+
clipUndoFloor: boolean;
|
|
129
|
+
}): Scope {
|
|
130
|
+
const scope: Scope = {
|
|
131
|
+
snapshot: structuredClone(get(store)),
|
|
132
|
+
label: opts.label,
|
|
133
|
+
collapseToOne: opts.collapseToOne,
|
|
134
|
+
clipUndoFloor: opts.clipUndoFloor,
|
|
135
|
+
historyIdx: past.length,
|
|
136
|
+
changed: false,
|
|
137
|
+
};
|
|
138
|
+
if (opts.clipUndoFloor) {
|
|
139
|
+
if (clippingScope) commitScope(clippingScope);
|
|
140
|
+
clippingScope = scope;
|
|
141
|
+
} else {
|
|
142
|
+
if (transactionScope) {
|
|
143
|
+
if (import.meta.env.DEV) {
|
|
144
|
+
console.warn(
|
|
145
|
+
`[editorStore] beginScope("${opts.label}") while "${transactionScope.label}" is still open; aborting previous`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
cancelScope(transactionScope, { silent: true });
|
|
149
|
+
}
|
|
150
|
+
transactionScope = scope;
|
|
151
|
+
}
|
|
152
|
+
return scope;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Commit the given scope. If collapseToOne, all intra-scope past entries
|
|
157
|
+
* (those pushed since `historyIdx`) are dropped and the pre-scope snapshot
|
|
158
|
+
* is pushed in their place — but only if anything actually changed.
|
|
159
|
+
*
|
|
160
|
+
* "Did anything change" is computed by state-equality (JSON) when
|
|
161
|
+
* `clipUndoFloor` is set, because the scope's intra-scope mutations were
|
|
162
|
+
* applied to history AND then dropped — the only signal left is whether
|
|
163
|
+
* the live state still matches the snapshot. For non-clipping scopes,
|
|
164
|
+
* the `changed` flag is the source of truth: empty scopes skip the commit
|
|
165
|
+
* entirely.
|
|
166
|
+
*/
|
|
167
|
+
export function commitScope(scope: Scope): void {
|
|
168
|
+
// Clear the slot first so internal aborts triggered during commit don't
|
|
169
|
+
// recurse against the same scope.
|
|
170
|
+
if (scope.clipUndoFloor) {
|
|
171
|
+
if (clippingScope !== scope) return;
|
|
172
|
+
if (transactionScope) cancelScope(transactionScope, { silent: true });
|
|
173
|
+
clippingScope = null;
|
|
174
|
+
} else {
|
|
175
|
+
if (transactionScope !== scope) return;
|
|
176
|
+
transactionScope = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (scope.collapseToOne) {
|
|
180
|
+
if (scope.clipUndoFloor) {
|
|
181
|
+
// Clipping path: intra-scope entries were each pushed by mutate()
|
|
182
|
+
// (they had to be, so undo within the scope could walk them back).
|
|
183
|
+
// On commit, drop them and push the pre-scope snapshot iff state is
|
|
184
|
+
// not back at the snapshot.
|
|
185
|
+
past.length = scope.historyIdx;
|
|
186
|
+
const snapshotJson = JSON.stringify(scope.snapshot);
|
|
187
|
+
const currentJson = JSON.stringify(get(store));
|
|
188
|
+
const changedByEquality = snapshotJson !== currentJson;
|
|
189
|
+
if (changedByEquality) {
|
|
190
|
+
pushPast(scope.snapshot);
|
|
191
|
+
persistHook();
|
|
192
|
+
}
|
|
193
|
+
bumpTick();
|
|
194
|
+
if (import.meta.env.DEV) {
|
|
195
|
+
console.debug('[editorStore] commitScope (clipping) →', {
|
|
196
|
+
label: scope.label,
|
|
197
|
+
pastLen: past.length,
|
|
198
|
+
snapshotPalettes: paletteSnapshot(scope.snapshot),
|
|
199
|
+
currentPalettes: paletteSnapshot(get(store)),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Non-clipping path: intra-scope mutations were applied to the store
|
|
205
|
+
// but NOT pushed individually. Push the pre-scope snapshot iff anything
|
|
206
|
+
// changed.
|
|
207
|
+
if (!scope.changed) return;
|
|
208
|
+
pushPast(scope.snapshot);
|
|
209
|
+
bumpTick();
|
|
210
|
+
persistHook();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Future: a non-collapsing scope would be a no-op on commit (entries
|
|
214
|
+
// already live in past[]). Not used today.
|
|
215
|
+
bumpTick();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Cancel the given scope. Always reverts state to the snapshot and drops
|
|
220
|
+
* intra-scope past entries. The `silent` flag controls whether the cancel
|
|
221
|
+
* surfaces as a tick + persist:
|
|
222
|
+
* - silent: false (default) — used when the user aborts a scope
|
|
223
|
+
* explicitly (e.g., palette panel X). Refreshes dirty + persist so the
|
|
224
|
+
* UI reflects the discard.
|
|
225
|
+
* - silent: true — used by internal auto-aborts (a stray transaction
|
|
226
|
+
* being killed by undo() / by a competing scope). UI observes the state
|
|
227
|
+
* revert, but `dirty` reflects the pre-scope position because no tick
|
|
228
|
+
* fires and no persist runs.
|
|
229
|
+
*/
|
|
230
|
+
export function cancelScope(scope: Scope, opts?: { silent?: boolean }): void {
|
|
231
|
+
// Clear the slot first so any code observing the state revert doesn't see
|
|
232
|
+
// a stale "scope still open" view.
|
|
233
|
+
if (scope.clipUndoFloor) {
|
|
234
|
+
if (clippingScope !== scope) return;
|
|
235
|
+
if (transactionScope) cancelScope(transactionScope, { silent: true });
|
|
236
|
+
clippingScope = null;
|
|
237
|
+
} else {
|
|
238
|
+
if (transactionScope !== scope) return;
|
|
239
|
+
transactionScope = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
past.length = scope.historyIdx;
|
|
243
|
+
if (scope.clipUndoFloor) future.length = 0;
|
|
244
|
+
store.set(scope.snapshot);
|
|
245
|
+
if (opts?.silent) return;
|
|
246
|
+
bumpTick();
|
|
247
|
+
persistHook();
|
|
248
|
+
if (import.meta.env.DEV && scope.clipUndoFloor) {
|
|
249
|
+
console.debug('[editorStore] cancelScope (clipping) →', {
|
|
250
|
+
label: scope.label,
|
|
251
|
+
pastLen: past.length,
|
|
252
|
+
restoredPalettes: paletteSnapshot(scope.snapshot),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const editorState: Readable<EditorState> = { subscribe: store.subscribe };
|
|
258
|
+
|
|
259
|
+
export function mutate(label: string, fn: (draft: EditorState) => void): void {
|
|
260
|
+
if (import.meta.env.DEV && !label) {
|
|
261
|
+
console.warn('[editorStore] mutate() called without a label');
|
|
262
|
+
}
|
|
263
|
+
if (transactionScope) {
|
|
264
|
+
// Inside a non-clipping scope: don't push individually; just mark the
|
|
265
|
+
// scope dirty and apply. The scope's commit pushes one collapsed entry.
|
|
266
|
+
transactionScope.changed = true;
|
|
267
|
+
if (clippingScope) clippingScope.changed = true;
|
|
268
|
+
store.update((s) => { fn(s); return s; });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// No transaction scope: each mutate is its own history entry. Inside a
|
|
272
|
+
// clipping scope this still pushes per-mutate entries (so undo within the
|
|
273
|
+
// scope walks them back), and the scope's commit will collapse them.
|
|
274
|
+
if (clippingScope) clippingScope.changed = true;
|
|
275
|
+
const current = get(store);
|
|
276
|
+
pushPast(structuredClone(current));
|
|
277
|
+
store.update((s) => { fn(s); return s; });
|
|
278
|
+
bumpTick();
|
|
279
|
+
persistHook();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Slider-drag helper: opens a non-clipping scope on pointerdown and commits
|
|
284
|
+
* it on the next window-level pointerup / pointercancel. Groups a drag
|
|
285
|
+
* gesture under one history entry so undo rolls the whole drag back as a
|
|
286
|
+
* single step.
|
|
287
|
+
*/
|
|
288
|
+
export function beginSliderGesture(label: string): void {
|
|
289
|
+
if (typeof window === 'undefined') return;
|
|
290
|
+
const scope = beginScope({ label, collapseToOne: true, clipUndoFloor: false });
|
|
291
|
+
const end = () => {
|
|
292
|
+
commitScope(scope);
|
|
293
|
+
window.removeEventListener('pointerup', end);
|
|
294
|
+
window.removeEventListener('pointercancel', end);
|
|
295
|
+
};
|
|
296
|
+
window.addEventListener('pointerup', end);
|
|
297
|
+
window.addEventListener('pointercancel', end);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Sync closure form: opens a non-clipping scope, runs `fn`, and commits.
|
|
302
|
+
* If `fn` throws, the scope is silently cancelled (state reverted, no
|
|
303
|
+
* persist) and the error is re-thrown.
|
|
304
|
+
*/
|
|
305
|
+
export function transaction<T>(label: string, fn: (draft: EditorState) => T): T {
|
|
306
|
+
const scope = beginScope({ label, collapseToOne: true, clipUndoFloor: false });
|
|
307
|
+
try {
|
|
308
|
+
let result!: T;
|
|
309
|
+
mutate(label, (draft) => { result = fn(draft); });
|
|
310
|
+
commitScope(scope);
|
|
311
|
+
return result;
|
|
312
|
+
} catch (e) {
|
|
313
|
+
cancelScope(scope, { silent: true });
|
|
314
|
+
throw e;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Undo / redo ───────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
export function undo(): boolean {
|
|
321
|
+
if (transactionScope) cancelScope(transactionScope, { silent: true });
|
|
322
|
+
const floor = clippingScope ? clippingScope.historyIdx : 0;
|
|
323
|
+
if (past.length <= floor) return false;
|
|
324
|
+
future.push(structuredClone(get(store)));
|
|
325
|
+
const previous = past.pop()!;
|
|
326
|
+
store.set(previous);
|
|
327
|
+
bumpTick();
|
|
328
|
+
persistHook();
|
|
329
|
+
if (import.meta.env.DEV) {
|
|
330
|
+
console.debug('[editorStore] undo →', {
|
|
331
|
+
pastLen: past.length, floor,
|
|
332
|
+
palettes: paletteSnapshot(previous),
|
|
333
|
+
inClippingScope: !!clippingScope,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function redo(): boolean {
|
|
340
|
+
if (transactionScope) cancelScope(transactionScope, { silent: true });
|
|
341
|
+
if (future.length === 0) return false;
|
|
342
|
+
past.push(structuredClone(get(store)));
|
|
343
|
+
const next = future.pop()!;
|
|
344
|
+
store.set(next);
|
|
345
|
+
bumpTick();
|
|
346
|
+
persistHook();
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const canUndo: Readable<boolean> = derived(historyTick, () => past.length > 0);
|
|
351
|
+
export const canRedo: Readable<boolean> = derived(historyTick, () => future.length > 0);
|
|
352
|
+
export const dirty: Readable<boolean> = derived(historyTick, () => past.length !== savedAtIndex);
|
|
353
|
+
|
|
354
|
+
export function markSaved(): void {
|
|
355
|
+
savedAtIndex = past.length;
|
|
356
|
+
bumpTick();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Reset history + transient scope state without touching the store. Used by
|
|
361
|
+
* `loadFromFile` ("open a different document" — undo cannot cross a theme
|
|
362
|
+
* load). The caller is responsible for `store.set(next)` and `persistHook()`.
|
|
363
|
+
*/
|
|
364
|
+
export function resetHistoryForLoad(): void {
|
|
365
|
+
past.length = 0;
|
|
366
|
+
future.length = 0;
|
|
367
|
+
savedAtIndex = 0;
|
|
368
|
+
transactionScope = null;
|
|
369
|
+
bumpTick();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Test-only: clear all core history + transient scope state and reset the
|
|
374
|
+
* store to `emptyStateFactory()`. The full `__resetForTests` in editorStore
|
|
375
|
+
* also resets renderer + per-slice baselines.
|
|
376
|
+
*/
|
|
377
|
+
export function __resetCoreForTests(): void {
|
|
378
|
+
past.length = 0;
|
|
379
|
+
future.length = 0;
|
|
380
|
+
savedAtIndex = 0;
|
|
381
|
+
transactionScope = null;
|
|
382
|
+
clippingScope = null;
|
|
383
|
+
store.set(emptyStateFactory());
|
|
384
|
+
bumpTick();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Test-only accessors — internal history state is module-private otherwise. */
|
|
388
|
+
export function __getHistoryLengths(): { past: number; future: number } {
|
|
389
|
+
return { past: past.length, future: future.length };
|
|
390
|
+
}
|
|
391
|
+
export function __getPastAt(idx: number): EditorState | undefined {
|
|
392
|
+
return past[idx];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Dev-only: compact digest of each palette's baseColor + overrides count. */
|
|
396
|
+
function paletteSnapshot(s: EditorState): Record<string, { base: string; overrides: number }> {
|
|
397
|
+
const out: Record<string, { base: string; overrides: number }> = {};
|
|
398
|
+
for (const [k, v] of Object.entries(s.palettes ?? {})) {
|
|
399
|
+
out[k] = { base: v.baseColor, overrides: Object.keys(v.overrides).length };
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { undo, redo } from './editorStore';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Install Cmd/Ctrl+Z → undo and Cmd/Ctrl+Shift+Z → redo.
|
|
5
|
+
*
|
|
6
|
+
* Native browser text-undo inside inputs / textareas / contenteditable is
|
|
7
|
+
* left intact: those targets are passed through untouched. Editor undo fires
|
|
8
|
+
* only when focus is outside editable elements.
|
|
9
|
+
*
|
|
10
|
+
* Scope this installer to the pages where the editor is actually mounted
|
|
11
|
+
* (Editor.svelte) so Cmd+Z on the landing / component-editor pages isn't hijacked.
|
|
12
|
+
*
|
|
13
|
+
* Returns a cleanup function (use in Svelte's onMount return).
|
|
14
|
+
*/
|
|
15
|
+
export function installEditorKeybindings(): () => void {
|
|
16
|
+
if (typeof window === 'undefined') return () => {};
|
|
17
|
+
|
|
18
|
+
window.addEventListener('keydown', handleKeydown);
|
|
19
|
+
return () => window.removeEventListener('keydown', handleKeydown);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handleKeydown(e: KeyboardEvent): void {
|
|
23
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
24
|
+
if (!mod) return;
|
|
25
|
+
if (e.key !== 'z' && e.key !== 'Z') return;
|
|
26
|
+
|
|
27
|
+
const target = e.target as HTMLElement | null;
|
|
28
|
+
if (target && isEditable(target)) return;
|
|
29
|
+
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
if (e.shiftKey) redo();
|
|
32
|
+
else undo();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Input types whose native Cmd+Z editing we want to preserve. Everything
|
|
36
|
+
// else (range, checkbox, radio, button, color, etc.) has no meaningful
|
|
37
|
+
// native undo — passing the event through would mean editor undo silently
|
|
38
|
+
// does nothing whenever a slider or checkbox has focus.
|
|
39
|
+
const TEXT_INPUT_TYPES = new Set([
|
|
40
|
+
'text', 'search', 'email', 'url', 'tel', 'password', 'number',
|
|
41
|
+
'date', 'datetime-local', 'month', 'time', 'week',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function isEditable(el: HTMLElement): boolean {
|
|
45
|
+
const tag = el.tagName;
|
|
46
|
+
if (tag === 'TEXTAREA') return true;
|
|
47
|
+
if (tag === 'INPUT') {
|
|
48
|
+
const type = ((el as HTMLInputElement).type || 'text').toLowerCase();
|
|
49
|
+
return TEXT_INPUT_TYPES.has(type);
|
|
50
|
+
}
|
|
51
|
+
return el.isContentEditable;
|
|
52
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for the editor store — debounced localStorage write +
|
|
3
|
+
* eager hydrate at module load.
|
|
4
|
+
*
|
|
5
|
+
* `editorStore.ts` wires the persist hook to `schedulePersist` exported here
|
|
6
|
+
* so editorCore's mutate/undo/redo debounce-write through the same path.
|
|
7
|
+
* Seed paths that bypass `mutate` call `editorCore.persist()` (which routes
|
|
8
|
+
* to `schedulePersist`).
|
|
9
|
+
*
|
|
10
|
+
* Hydrate runs eagerly at first import so child components reading
|
|
11
|
+
* `$editorState` in their onMount see persisted state, not the transient
|
|
12
|
+
* empty default — Svelte mounts children before parents, so waiting for
|
|
13
|
+
* Editor's onMount is too late.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { get } from 'svelte/store';
|
|
17
|
+
import type { EditorState } from './editorTypes';
|
|
18
|
+
import { storageKey } from './editorConfig';
|
|
19
|
+
import { store } from './editorCore';
|
|
20
|
+
import { quietGet, quietSet } from './storage';
|
|
21
|
+
import { makeDefaultGradients } from './slices/gradients';
|
|
22
|
+
import { seedShadowsFromDom } from './slices/shadows';
|
|
23
|
+
|
|
24
|
+
// Resolve the persist key lazily (per-call) so library consumers that invoke
|
|
25
|
+
// `configureEditor({storagePrefix})` before the first store write get the
|
|
26
|
+
// configured prefix even if `editorPersistence` was imported at module-load
|
|
27
|
+
// time (M8). The function call is cheap; do not memoise — that defeats the point.
|
|
28
|
+
function getPersistKey(): string {
|
|
29
|
+
return storageKey('editor-state');
|
|
30
|
+
}
|
|
31
|
+
export const PERSIST_DEBOUNCE_MS = 300;
|
|
32
|
+
|
|
33
|
+
let emptyStateFactory: () => EditorState = () => ({} as EditorState);
|
|
34
|
+
|
|
35
|
+
/** Wired by editorStore at boot — the factory composes per-slice defaults. */
|
|
36
|
+
export function setEmptyStateFactory(fn: () => EditorState): void {
|
|
37
|
+
emptyStateFactory = fn;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
|
|
42
|
+
export function schedulePersist(): void {
|
|
43
|
+
if (typeof localStorage === 'undefined') return;
|
|
44
|
+
if (persistTimer) clearTimeout(persistTimer);
|
|
45
|
+
persistTimer = setTimeout(persistNow, PERSIST_DEBOUNCE_MS);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function persistNow(): void {
|
|
49
|
+
persistTimer = null;
|
|
50
|
+
// quota / serialization errors are not fatal; quietSet swallows them.
|
|
51
|
+
quietSet(getPersistKey(), JSON.stringify(get(store)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function migrateGradients(state: EditorState): EditorState {
|
|
55
|
+
// Gradients are a fixed-slot scale (--gradient-1 … --gradient-4). If the
|
|
56
|
+
// persisted state predates the migration to fixed slots (e.g. it still has
|
|
57
|
+
// a token named --gradient-progress, or the count doesn't match), replace
|
|
58
|
+
// the gradients block with defaults rather than carrying stale entries.
|
|
59
|
+
const expected = makeDefaultGradients().map((g) => g.variable).sort();
|
|
60
|
+
const have = (state.gradients?.tokens ?? []).map((g) => g.variable).sort();
|
|
61
|
+
const matches =
|
|
62
|
+
have.length === expected.length &&
|
|
63
|
+
expected.every((v, i) => v === have[i]);
|
|
64
|
+
if (matches) return state;
|
|
65
|
+
return { ...state, gradients: { tokens: makeDefaultGradients() } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hydrate(): void {
|
|
69
|
+
// Corrupt state, missing key, or unavailable storage all return null;
|
|
70
|
+
// the editor falls through to the empty default in that case.
|
|
71
|
+
const parsed = quietGet<unknown>(getPersistKey(), { parse: true });
|
|
72
|
+
if (parsed && typeof parsed === 'object') {
|
|
73
|
+
// Shallow-merge onto default shape so older persisted state missing
|
|
74
|
+
// newly-added domain fields still loads.
|
|
75
|
+
const merged = { ...emptyStateFactory(), ...(parsed as object) } as EditorState;
|
|
76
|
+
store.set(migrateGradients(merged));
|
|
77
|
+
}
|
|
78
|
+
// m13 fix: seed shadows from the DOM at hydrate time so the editor
|
|
79
|
+
// captures the tokens.css baseline regardless of whether the user opens
|
|
80
|
+
// the shadows tab. `seedShadowsFromDom` is a no-op when state already
|
|
81
|
+
// has tokens (e.g. we just loaded persisted state with shadows saved),
|
|
82
|
+
// so the seed only fires on a truly fresh boot. Deferred to next frame
|
|
83
|
+
// because tokens.css may not be applied yet at module-load time —
|
|
84
|
+
// `getComputedStyle` would return empty strings and `parseShadowCss`
|
|
85
|
+
// would reject every entry.
|
|
86
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
87
|
+
requestAnimationFrame(() => seedShadowsFromDom());
|
|
88
|
+
} else {
|
|
89
|
+
seedShadowsFromDom();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let hydrated = false;
|
|
94
|
+
export function ensureHydrated(): void {
|
|
95
|
+
if (hydrated) return;
|
|
96
|
+
hydrated = true;
|
|
97
|
+
hydrate();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Kept for API parity with callers that opt-in from onMount. A no-op after
|
|
102
|
+
* the eager load below, but cheap to call multiple times.
|
|
103
|
+
*/
|
|
104
|
+
export async function initializeEditorStore(): Promise<void> {
|
|
105
|
+
ensureHydrated();
|
|
106
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM renderer for the editor store.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to the store and writes derived CSS variables to :root via
|
|
5
|
+
* `cssVarSync` (which fans out to the parent document for the overlay
|
|
6
|
+
* iframe). This module is where all DOM side effects live; everything
|
|
7
|
+
* upstream of `deriveCssVars` is pure-data.
|
|
8
|
+
*
|
|
9
|
+
* Importing this module on the client triggers `store.subscribe(...)`. The
|
|
10
|
+
* editorStore barrel re-exports from here so legacy `import '.../editorStore'`
|
|
11
|
+
* paths still wire up the renderer at module load.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EditorState } from './editorTypes';
|
|
15
|
+
import { setCssVar, removeCssVar } from './cssVarSync';
|
|
16
|
+
import { palettesToVars } from './paletteDerivation';
|
|
17
|
+
import {
|
|
18
|
+
editorState,
|
|
19
|
+
columnsToVars,
|
|
20
|
+
columnsEqualsDefault,
|
|
21
|
+
overlaysToVars,
|
|
22
|
+
overlaysEqualsDefault,
|
|
23
|
+
shadowsToVars,
|
|
24
|
+
gradientsToVars,
|
|
25
|
+
componentsToVars,
|
|
26
|
+
} from './editorStore';
|
|
27
|
+
|
|
28
|
+
export function deriveCssVars(state: EditorState): Record<string, string> {
|
|
29
|
+
const out: Record<string, string> = { ...state.cssVars };
|
|
30
|
+
if (!columnsEqualsDefault(state.columns)) {
|
|
31
|
+
Object.assign(out, columnsToVars(state.columns));
|
|
32
|
+
}
|
|
33
|
+
if (!overlaysEqualsDefault(state.overlays)) {
|
|
34
|
+
Object.assign(out, overlaysToVars(state.overlays));
|
|
35
|
+
}
|
|
36
|
+
if (state.shadows.tokens.length > 0) {
|
|
37
|
+
Object.assign(out, shadowsToVars(state.shadows));
|
|
38
|
+
}
|
|
39
|
+
if (state.gradients.tokens.length > 0) {
|
|
40
|
+
Object.assign(out, gradientsToVars(state.gradients));
|
|
41
|
+
}
|
|
42
|
+
Object.assign(out, palettesToVars(state.palettes));
|
|
43
|
+
Object.assign(out, componentsToVars(state.components));
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let lastApplied: Record<string, string> = {};
|
|
48
|
+
let installed = false;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Install the DOM subscriber. Called once from `editorStore.ts` after its
|
|
52
|
+
* exports are initialized — calling it earlier would TDZ-trip on `editorState`
|
|
53
|
+
* because of the editorStore ↔ editorRenderer circular import.
|
|
54
|
+
*/
|
|
55
|
+
export function installRenderer(): void {
|
|
56
|
+
if (installed) return;
|
|
57
|
+
installed = true;
|
|
58
|
+
if (typeof window === 'undefined') return;
|
|
59
|
+
editorState.subscribe((state) => {
|
|
60
|
+
const next = deriveCssVars(state);
|
|
61
|
+
for (const name in next) {
|
|
62
|
+
if (next[name] !== lastApplied[name]) setCssVar(name, next[name]);
|
|
63
|
+
}
|
|
64
|
+
for (const name in lastApplied) {
|
|
65
|
+
if (!(name in next)) removeCssVar(name);
|
|
66
|
+
}
|
|
67
|
+
lastApplied = next;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Test-only: clear the diff cache so independent test runs don't leak applied vars. */
|
|
72
|
+
export function __resetRendererCacheForTests(): void {
|
|
73
|
+
lastApplied = {};
|
|
74
|
+
}
|