@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.
Files changed (224) hide show
  1. package/README.md +168 -21
  2. package/dist-plugin/index.cjs +823 -336
  3. package/dist-plugin/index.d.cts +9 -7
  4. package/dist-plugin/index.d.ts +9 -7
  5. package/dist-plugin/index.js +822 -335
  6. package/package.json +46 -20
  7. package/src/assets/newspaper.webp +0 -0
  8. package/src/assets/offering.webp +0 -0
  9. package/src/component-editor/BadgeEditor.svelte +170 -0
  10. package/src/component-editor/CalloutEditor.svelte +103 -0
  11. package/src/component-editor/CardEditor.svelte +184 -0
  12. package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
  13. package/src/component-editor/CornerBadgeEditor.svelte +207 -0
  14. package/src/component-editor/DialogEditor.svelte +172 -0
  15. package/src/component-editor/ImageEditor.svelte +72 -0
  16. package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
  17. package/src/component-editor/NotificationEditor.svelte +160 -0
  18. package/src/component-editor/ProgressBarEditor.svelte +124 -0
  19. package/src/component-editor/RadioButtonEditor.svelte +140 -0
  20. package/src/component-editor/SectionDividerEditor.svelte +263 -0
  21. package/src/component-editor/SegmentedControlEditor.svelte +154 -0
  22. package/src/component-editor/StandardButtonsEditor.svelte +178 -0
  23. package/src/component-editor/TabBarEditor.svelte +137 -0
  24. package/src/component-editor/TableEditor.svelte +128 -0
  25. package/src/component-editor/TooltipEditor.svelte +122 -0
  26. package/src/component-editor/editorTokens.test.ts +93 -0
  27. package/src/component-editor/groupKeySlots.test.ts +67 -0
  28. package/src/component-editor/groupKeySnapshot.test.ts +52 -0
  29. package/src/component-editor/index.ts +5 -0
  30. package/src/component-editor/registry.ts +246 -0
  31. package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
  32. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
  33. package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
  34. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
  35. package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
  36. package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
  37. package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
  38. package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
  39. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
  40. package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
  41. package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
  42. package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
  43. package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
  44. package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
  45. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
  46. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
  47. package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
  48. package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
  49. package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
  50. package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
  51. package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
  52. package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
  53. package/src/component-editor/scaffolding/componentSources.ts +9 -0
  54. package/src/component-editor/scaffolding/defaultSections.ts +16 -0
  55. package/src/component-editor/scaffolding/editorContext.ts +44 -0
  56. package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
  57. package/src/component-editor/scaffolding/siblings.ts +33 -0
  58. package/src/component-editor/scaffolding/types.ts +39 -0
  59. package/src/components/Badge.svelte +231 -42
  60. package/src/components/Button.svelte +324 -124
  61. package/src/components/Callout.svelte +145 -0
  62. package/src/components/Card.svelte +123 -25
  63. package/src/components/CollapsibleSection.svelte +213 -35
  64. package/src/components/CornerBadge.svelte +224 -0
  65. package/src/components/Dialog.svelte +137 -114
  66. package/src/components/Image.svelte +43 -0
  67. package/src/components/InlineEditActions.svelte +74 -14
  68. package/src/components/Notification.svelte +184 -163
  69. package/src/components/ProgressBar.svelte +216 -22
  70. package/src/components/RadioButton.svelte +110 -40
  71. package/src/components/SectionDivider.svelte +428 -74
  72. package/src/components/SegmentedControl.svelte +203 -0
  73. package/src/components/TabBar.svelte +146 -21
  74. package/src/components/Table.svelte +102 -0
  75. package/src/components/Tooltip.svelte +45 -19
  76. package/src/components/types.ts +51 -0
  77. package/src/data/google-fonts.json +75 -0
  78. package/src/lib/ColumnsOverlay.svelte +20 -7
  79. package/src/lib/LiveEditorOverlay.svelte +257 -78
  80. package/src/lib/columnsOverlay.ts +21 -17
  81. package/src/lib/componentConfig.test.ts +204 -0
  82. package/src/lib/componentConfigKeys.ts +19 -0
  83. package/src/lib/componentConfigService.ts +88 -0
  84. package/src/lib/copyPopover.ts +30 -0
  85. package/src/lib/cssVarSync.ts +59 -7
  86. package/src/lib/editorConfigStore.ts +0 -10
  87. package/src/lib/editorCore.ts +402 -0
  88. package/src/lib/editorKeybindings.ts +52 -0
  89. package/src/lib/editorPersistence.ts +106 -0
  90. package/src/lib/editorRenderer.ts +74 -0
  91. package/src/lib/editorStore.test.ts +328 -0
  92. package/src/lib/editorStore.ts +412 -0
  93. package/src/lib/editorTypes.ts +100 -0
  94. package/src/lib/editorViewStore.ts +55 -0
  95. package/src/lib/files/versionedFileResource.ts +140 -0
  96. package/src/lib/fontLoader.ts +130 -0
  97. package/src/lib/fontMigration.ts +140 -0
  98. package/src/lib/fontParse.ts +168 -0
  99. package/src/lib/index.ts +48 -30
  100. package/src/lib/lazyConfig.test.ts +54 -0
  101. package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
  102. package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
  103. package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
  104. package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
  105. package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
  106. package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
  107. package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
  108. package/src/lib/migrations/index.ts +93 -0
  109. package/src/lib/migrations/migrations.test.ts +341 -0
  110. package/src/lib/navLinkTypes.ts +1 -0
  111. package/src/lib/overlayState.ts +3 -0
  112. package/src/lib/paletteDerivation.ts +300 -0
  113. package/src/lib/parentRouteStore.ts +42 -0
  114. package/src/lib/parsers/globalRootBlock.ts +32 -0
  115. package/src/lib/presetService.ts +94 -0
  116. package/src/lib/router.ts +42 -10
  117. package/src/lib/scrollSection.ts +45 -0
  118. package/src/lib/slices/columns.ts +59 -0
  119. package/src/lib/slices/components.ts +362 -0
  120. package/src/lib/slices/domainVars.ts +15 -0
  121. package/src/lib/slices/fonts.ts +30 -0
  122. package/src/lib/slices/gradients.ts +153 -0
  123. package/src/lib/slices/overlays.ts +132 -0
  124. package/src/lib/slices/palettes.ts +26 -0
  125. package/src/lib/slices/shadows.ts +123 -0
  126. package/src/lib/storage.ts +88 -0
  127. package/src/lib/themeInit.ts +74 -0
  128. package/src/lib/themeService.ts +101 -0
  129. package/src/lib/themeTypes.ts +146 -0
  130. package/src/lib/tokenRegistry.ts +148 -0
  131. package/src/pages/ComponentEditorPage.svelte +384 -0
  132. package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
  133. package/src/pages/Editor.svelte +98 -0
  134. package/src/pages/Editor.svelte.d.ts +2 -0
  135. package/src/pages/EditorShell.svelte +348 -0
  136. package/src/styles/_padding.scss +34 -0
  137. package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  138. package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  139. package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  140. package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  141. package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  142. package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
  143. package/src/styles/fonts.css +22 -10
  144. package/src/styles/form-controls.css +14 -16
  145. package/src/styles/tokens.css +1322 -0
  146. package/src/styles/ui-editor.css +126 -0
  147. package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
  148. package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
  149. package/src/ui/EditorViewSwitcher.svelte +180 -0
  150. package/src/ui/FontStackEditor.svelte +360 -0
  151. package/src/ui/GradientEditor.svelte +461 -0
  152. package/src/ui/GradientStopPicker.svelte +74 -0
  153. package/src/ui/PaletteEditor.svelte +1590 -0
  154. package/src/ui/PaletteEditor.test.ts +108 -0
  155. package/src/ui/PresetFileManager.svelte +567 -0
  156. package/src/ui/ProjectFontsSection.svelte +645 -0
  157. package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
  158. package/src/{showcase → ui}/TextTab.svelte +27 -27
  159. package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
  160. package/src/ui/Toggle.svelte +108 -0
  161. package/src/ui/UICopyPopover.svelte +78 -0
  162. package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
  163. package/src/ui/UIFontFamilySelector.svelte +309 -0
  164. package/src/ui/UIFontSizeSelector.svelte +165 -0
  165. package/src/ui/UIFontWeightSelector.svelte +52 -0
  166. package/src/ui/UILineHeightSelector.svelte +47 -0
  167. package/src/ui/UILinkToggle.svelte +60 -0
  168. package/src/ui/UIOptionItem.svelte +74 -0
  169. package/src/ui/UIOptionList.svelte +27 -0
  170. package/src/ui/UIPaddingSelector.svelte +661 -0
  171. package/src/ui/UIPaletteSelector.svelte +1084 -0
  172. package/src/ui/UIRadio.svelte +72 -0
  173. package/src/ui/UIRadioGroup.svelte +59 -0
  174. package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
  175. package/src/ui/UITokenSelector.svelte +509 -0
  176. package/src/ui/UIVariantSelector.svelte +145 -0
  177. package/src/ui/VariablesTab.svelte +252 -0
  178. package/src/ui/index.ts +31 -0
  179. package/src/ui/keepInViewport.ts +84 -0
  180. package/src/ui/palette/GradientStopEditor.svelte +482 -0
  181. package/src/ui/palette/OverridesPanel.svelte +526 -0
  182. package/src/ui/palette/PaletteBase.svelte +165 -0
  183. package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
  184. package/src/ui/palette/paletteEditorState.ts +89 -0
  185. package/src/ui/sections/ColumnsSection.svelte +273 -0
  186. package/src/ui/sections/GradientsSection.svelte +147 -0
  187. package/src/ui/sections/OverlaysSection.svelte +670 -0
  188. package/src/ui/sections/ShadowsSection.svelte +1250 -0
  189. package/src/ui/sections/TokenScaleTable.svelte +332 -0
  190. package/src/ui/sections/tokenScales.ts +81 -0
  191. package/src/ui/variantScales.ts +108 -0
  192. package/src/components/DetailNav.svelte +0 -78
  193. package/src/components/Toggle.svelte +0 -86
  194. package/src/lib/tokenInit.ts +0 -29
  195. package/src/lib/tokenService.ts +0 -144
  196. package/src/lib/tokenTypes.ts +0 -45
  197. package/src/pages/Admin.svelte +0 -100
  198. package/src/pages/ShowcasePage.svelte +0 -144
  199. package/src/showcase/BackupBrowser.svelte +0 -617
  200. package/src/showcase/ComponentsTab.svelte +0 -105
  201. package/src/showcase/PaletteEditor.svelte +0 -2579
  202. package/src/showcase/PaletteSelector.svelte +0 -627
  203. package/src/showcase/TokenMap.svelte +0 -54
  204. package/src/showcase/VariablesTab.svelte +0 -2655
  205. package/src/showcase/VisualsTab.svelte +0 -231
  206. package/src/showcase/demos/BadgeDemo.svelte +0 -56
  207. package/src/showcase/demos/CardDemo.svelte +0 -50
  208. package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
  209. package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
  210. package/src/showcase/demos/DialogDemo.svelte +0 -42
  211. package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
  212. package/src/showcase/demos/NotificationDemo.svelte +0 -147
  213. package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
  214. package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
  215. package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
  216. package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
  217. package/src/showcase/demos/TabBarDemo.svelte +0 -58
  218. package/src/showcase/demos/TooltipDemo.svelte +0 -52
  219. package/src/showcase/editor.css +0 -93
  220. package/src/showcase/index.ts +0 -17
  221. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  222. package/src/styles/fonts/Domine/OFL.txt +0 -97
  223. package/src/styles/fonts/Domine/README.txt +0 -66
  224. /package/src/{showcase → ui}/curveEngine.ts +0 -0
@@ -0,0 +1,108 @@
1
+ // @vitest-environment happy-dom
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import { get } from 'svelte/store';
4
+ import PaletteEditor from './PaletteEditor.svelte';
5
+ import {
6
+ editorState,
7
+ mutate,
8
+ beginScope,
9
+ commitScope,
10
+ cancelScope,
11
+ beginSliderGesture,
12
+ setPaletteConfig,
13
+ undo,
14
+ __resetForTests,
15
+ } from '../lib/editorStore';
16
+ import type { PaletteConfig } from '../lib/themeTypes';
17
+
18
+ function makePaletteConfig(baseColor: string): PaletteConfig {
19
+ return {
20
+ baseColor,
21
+ tintHue: 0,
22
+ tintChroma: 0.04,
23
+ lightnessCurve: [],
24
+ saturationCurve: [],
25
+ grayLightnessCurve: [],
26
+ graySaturationCurve: [],
27
+ scaleCurves: {},
28
+ curveOffset: {},
29
+ overrides: {},
30
+ snappedScales: [],
31
+ };
32
+ }
33
+
34
+ const sessionOpts = { label: 'palette session', collapseToOne: true, clipUndoFloor: true } as const;
35
+
36
+ beforeEach(() => {
37
+ __resetForTests();
38
+ document.body.innerHTML = '';
39
+ });
40
+
41
+ describe('PaletteEditor — store-first integration', () => {
42
+ // Mounts the real component to exercise the $: derivations off the store
43
+ // and prove the sync/auto-persist round-trip has been removed. If the
44
+ // previous two-writer loop were reintroduced, per-tick mutations during a
45
+ // session would be pulled back to the pre-session snapshot — this test
46
+ // would fail.
47
+ it('mounts against the editor store without throwing', () => {
48
+ setPaletteConfig('Background', makePaletteConfig('#8d7f74'));
49
+
50
+ const target = document.createElement('div');
51
+ document.body.appendChild(target);
52
+
53
+ const component = new PaletteEditor({
54
+ target,
55
+ props: { label: 'Background', initialColor: '#8d7f74', mode: 'chromatic' },
56
+ });
57
+
58
+ expect(get(editorState).palettes.Background.baseColor).toBe('#8d7f74');
59
+ component.$destroy();
60
+ });
61
+
62
+ it('per-tick store mutations are visible immediately during a session', () => {
63
+ setPaletteConfig('Background', makePaletteConfig('#8d7f74'));
64
+
65
+ const target = document.createElement('div');
66
+ document.body.appendChild(target);
67
+ const component = new PaletteEditor({
68
+ target,
69
+ props: { label: 'Background', initialColor: '#8d7f74', mode: 'chromatic' },
70
+ });
71
+
72
+ const session = beginScope({ ...sessionOpts });
73
+ beginSliderGesture('drag base');
74
+
75
+ for (const hex of ['#8c7f73', '#8b7f72', '#8a7f71']) {
76
+ mutate('drag tick', (s) => { s.palettes.Background.baseColor = hex; });
77
+ expect(get(editorState).palettes.Background.baseColor).toBe(hex);
78
+ }
79
+
80
+ window.dispatchEvent(new Event('pointerup'));
81
+ commitScope(session);
82
+
83
+ expect(get(editorState).palettes.Background.baseColor).toBe('#8a7f71');
84
+
85
+ undo();
86
+ expect(get(editorState).palettes.Background.baseColor).toBe('#8d7f74');
87
+
88
+ component.$destroy();
89
+ });
90
+
91
+ it('cancel after drag snaps the store back to pre-session', () => {
92
+ setPaletteConfig('Background', makePaletteConfig('#8d7f74'));
93
+
94
+ const target = document.createElement('div');
95
+ document.body.appendChild(target);
96
+ const component = new PaletteEditor({
97
+ target,
98
+ props: { label: 'Background', initialColor: '#8d7f74', mode: 'chromatic' },
99
+ });
100
+
101
+ const session = beginScope({ ...sessionOpts });
102
+ mutate('drag', (s) => { s.palettes.Background.baseColor = '#112233'; });
103
+ cancelScope(session);
104
+
105
+ expect(get(editorState).palettes.Background.baseColor).toBe('#8d7f74');
106
+ component.$destroy();
107
+ });
108
+ });
@@ -0,0 +1,567 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { PresetMeta } from '../lib/themeTypes';
4
+ import {
5
+ listPresets,
6
+ deletePreset,
7
+ captureCurrentAsPreset,
8
+ applyPreset,
9
+ getActivePreset,
10
+ } from '../lib/presetService';
11
+ import { sanitizeFileName } from '../lib/themeService';
12
+ import { dirty } from '../lib/editorStore';
13
+ import UIDialog from './UIDialog.svelte';
14
+
15
+ let files: PresetMeta[] = [];
16
+ let showFileList = false;
17
+ let saveAsEditing = false;
18
+ let saveAsName = '';
19
+ let saveAsInput: HTMLInputElement;
20
+
21
+ let activeFileName = 'default';
22
+ let currentDisplayName = 'Default';
23
+
24
+ let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = 'idle';
25
+ let applyStatus: 'idle' | 'applying' = 'idle';
26
+
27
+ async function refreshFiles() {
28
+ try {
29
+ files = await listPresets();
30
+ const active = files.find((f) => f.isActive);
31
+ if (active) {
32
+ activeFileName = active.fileName;
33
+ currentDisplayName = active.name;
34
+ }
35
+ } catch {
36
+ // silent — empty list
37
+ }
38
+ }
39
+
40
+ async function refreshActive() {
41
+ try {
42
+ const active = await getActivePreset();
43
+ if (active) {
44
+ activeFileName = active._fileName ?? activeFileName;
45
+ currentDisplayName = active.name;
46
+ }
47
+ } catch {
48
+ // silent
49
+ }
50
+ }
51
+
52
+ onMount(async () => {
53
+ await refreshActive();
54
+ await refreshFiles();
55
+ });
56
+
57
+ /** Standard "save dirty editor first?" gate. Returns true if the caller
58
+ * should proceed with capturing from disk; false to abort. */
59
+ function confirmDirtyCapture(): boolean {
60
+ if (!$dirty) return true;
61
+ return window.confirm(
62
+ 'You have unsaved changes in the editor. They will not be included in the preset (presets are built from the named files on disk). Continue saving the preset anyway?',
63
+ );
64
+ }
65
+
66
+ async function doCapture(fileName: string, displayName: string) {
67
+ saveStatus = 'saving';
68
+ try {
69
+ await captureCurrentAsPreset(fileName, displayName);
70
+ activeFileName = fileName;
71
+ currentDisplayName = displayName;
72
+ saveStatus = 'saved';
73
+ setTimeout(() => { saveStatus = 'idle'; }, 2000);
74
+ await refreshFiles();
75
+ } catch {
76
+ saveStatus = 'error';
77
+ setTimeout(() => { saveStatus = 'idle'; }, 3000);
78
+ }
79
+ }
80
+
81
+ async function handleSave() {
82
+ if (!confirmDirtyCapture()) return;
83
+ await doCapture(activeFileName, currentDisplayName);
84
+ }
85
+
86
+ async function handleSaveIncrement() {
87
+ if (!confirmDirtyCapture()) return;
88
+ const baseName = currentDisplayName.replace(/_\d+$/, '');
89
+ const baseFileName = sanitizeFileName(baseName);
90
+ const existingNums = files
91
+ .filter(
92
+ (f) => f.fileName === baseFileName || f.fileName.match(new RegExp(`^${baseFileName}_\\d+$`)),
93
+ )
94
+ .map((f) => {
95
+ const m = f.fileName.match(/_(\d+)$/);
96
+ return m ? parseInt(m[1], 10) : 0;
97
+ });
98
+ const next = (existingNums.length > 0 ? Math.max(...existingNums) : 0) + 1;
99
+ const suffix = String(next).padStart(2, '0');
100
+ const displayName = `${baseName}_${suffix}`;
101
+ const fileName = `${baseFileName}_${suffix}`;
102
+ await doCapture(fileName, displayName);
103
+ }
104
+
105
+ function openSaveAs() {
106
+ saveAsName = currentDisplayName;
107
+ saveAsEditing = true;
108
+ showFileList = false;
109
+ setTimeout(() => saveAsInput?.select(), 0);
110
+ }
111
+
112
+ async function confirmSaveAs() {
113
+ const displayName = saveAsName.trim();
114
+ if (!displayName) return;
115
+ const fileName = sanitizeFileName(displayName);
116
+ if (fileName === 'default') {
117
+ saveAsName = '';
118
+ return;
119
+ }
120
+ if (!confirmDirtyCapture()) return;
121
+ saveAsEditing = false;
122
+ await doCapture(fileName, displayName);
123
+ }
124
+
125
+ function cancelSaveAs() {
126
+ saveAsEditing = false;
127
+ saveAsName = '';
128
+ }
129
+
130
+ function handleSaveAsKeydown(e: KeyboardEvent) {
131
+ if (e.key === 'Enter') confirmSaveAs();
132
+ if (e.key === 'Escape') cancelSaveAs();
133
+ }
134
+
135
+ async function handleApply(file: PresetMeta) {
136
+ if ($dirty) {
137
+ const ok = window.confirm(
138
+ 'Loading a preset will reload the editor and discard unsaved changes. Continue?',
139
+ );
140
+ if (!ok) return;
141
+ }
142
+ showFileList = false;
143
+ applyStatus = 'applying';
144
+ try {
145
+ await applyPreset(file.fileName);
146
+ // Page reload: simplest path — editor rehydrates from the now-active
147
+ // theme + component configs the server just pinned for us.
148
+ window.location.reload();
149
+ } catch (err) {
150
+ applyStatus = 'idle';
151
+ window.alert(`Failed to apply preset: ${(err as Error).message}`);
152
+ }
153
+ }
154
+
155
+ async function handleDelete(file: PresetMeta) {
156
+ if (file.fileName === 'default') return;
157
+ try {
158
+ await deletePreset(file.fileName);
159
+ await refreshFiles();
160
+ if (file.fileName === activeFileName) {
161
+ activeFileName = 'default';
162
+ currentDisplayName = 'Default';
163
+ }
164
+ } catch {
165
+ // silent
166
+ }
167
+ }
168
+
169
+ function toggleFileList() {
170
+ showFileList = !showFileList;
171
+ saveAsEditing = false;
172
+ if (showFileList) refreshFiles();
173
+ }
174
+ </script>
175
+
176
+ <div class="preset-file-manager">
177
+ <div class="active-file">
178
+ <span class="active-label">Preset</span>
179
+ <span class="active-name">{currentDisplayName}</span>
180
+ </div>
181
+
182
+ <div class="button-grid">
183
+ <div class="save-row">
184
+ <button
185
+ class="pfm-btn save-btn"
186
+ class:saving={saveStatus === 'saving'}
187
+ class:saved={saveStatus === 'saved'}
188
+ class:error={saveStatus === 'error'}
189
+ on:click={handleSave}
190
+ disabled={saveStatus === 'saving' || applyStatus === 'applying'}
191
+ title="Capture the current theme + component configs into this preset"
192
+ >
193
+ <i
194
+ class="fas"
195
+ class:fa-save={saveStatus === 'idle'}
196
+ class:fa-spinner={saveStatus === 'saving'}
197
+ class:fa-check={saveStatus === 'saved'}
198
+ class:fa-times={saveStatus === 'error'}
199
+ ></i>
200
+ <span>
201
+ {#if saveStatus === 'idle'}Save{:else if saveStatus === 'saving'}Saving{:else if saveStatus === 'saved'}Saved{:else}Error{/if}
202
+ </span>
203
+ </button>
204
+ <button
205
+ class="pfm-btn increment-btn"
206
+ on:click={handleSaveIncrement}
207
+ disabled={saveStatus === 'saving' || applyStatus === 'applying'}
208
+ title="Save as incremented preset"
209
+ >
210
+ <i class="fas fa-plus"></i>
211
+ </button>
212
+ </div>
213
+
214
+ {#if saveAsEditing}
215
+ <div class="save-as-inline">
216
+ <input
217
+ class="save-as-input"
218
+ type="text"
219
+ bind:value={saveAsName}
220
+ bind:this={saveAsInput}
221
+ on:keydown={handleSaveAsKeydown}
222
+ placeholder="Preset name..."
223
+ />
224
+ <div class="save-as-actions">
225
+ <button
226
+ class="inline-btn confirm-btn"
227
+ on:click={confirmSaveAs}
228
+ disabled={!saveAsName.trim()}
229
+ title="Save"
230
+ >
231
+ <i class="fas fa-check"></i>
232
+ </button>
233
+ <button class="inline-btn cancel-btn" on:click={cancelSaveAs} title="Cancel">
234
+ <i class="fas fa-times"></i>
235
+ </button>
236
+ </div>
237
+ </div>
238
+ {:else}
239
+ <button class="pfm-btn" on:click={openSaveAs} title="Save as new preset">
240
+ <i class="fas fa-copy"></i>
241
+ <span>Save As</span>
242
+ </button>
243
+ {/if}
244
+
245
+ <button
246
+ class="pfm-btn"
247
+ class:active={showFileList}
248
+ on:click={toggleFileList}
249
+ disabled={applyStatus === 'applying'}
250
+ title="Load a preset"
251
+ >
252
+ <i class="fas fa-folder-open"></i>
253
+ <span>Load</span>
254
+ </button>
255
+ </div>
256
+
257
+ {#if applyStatus === 'applying'}
258
+ <span class="apply-status">Applying preset…</span>
259
+ {/if}
260
+ </div>
261
+
262
+ <UIDialog bind:show={showFileList} title="Load Preset" cancelLabel="Close" width="420px">
263
+ <div class="load-list">
264
+ {#each files as file}
265
+ <div class="load-item" class:active={file.fileName === activeFileName}>
266
+ <button class="load-name-btn" on:click={() => handleApply(file)}>
267
+ {file.name}
268
+ </button>
269
+ {#if file.fileName === activeFileName}
270
+ <span class="active-badge">active</span>
271
+ {/if}
272
+ {#if file.fileName !== 'default'}
273
+ <button
274
+ class="file-delete-btn"
275
+ on:click|stopPropagation={() => handleDelete(file)}
276
+ title="Delete {file.name}"
277
+ >
278
+ <i class="fas fa-trash-alt"></i>
279
+ </button>
280
+ {/if}
281
+ </div>
282
+ {/each}
283
+ {#if files.length === 0}
284
+ <div class="load-item empty">No saved presets</div>
285
+ {/if}
286
+ </div>
287
+ </UIDialog>
288
+
289
+ <style>
290
+ .preset-file-manager {
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: var(--ui-space-8);
294
+ }
295
+
296
+ .active-file {
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: var(--ui-space-2);
300
+ padding: 0 var(--ui-space-4);
301
+ }
302
+
303
+ .active-label {
304
+ font-size: var(--ui-font-size-xs);
305
+ color: var(--ui-text-secondary);
306
+ text-transform: uppercase;
307
+ letter-spacing: 0.05em;
308
+ }
309
+
310
+ .active-name {
311
+ font-size: var(--ui-font-size-md);
312
+ font-weight: var(--ui-font-weight-semibold);
313
+ color: var(--ui-text-primary);
314
+ }
315
+
316
+ .button-grid {
317
+ display: flex;
318
+ flex-direction: column;
319
+ gap: var(--ui-space-4);
320
+ }
321
+
322
+ .pfm-btn {
323
+ display: flex;
324
+ align-items: center;
325
+ gap: var(--ui-space-4);
326
+ width: 100%;
327
+ padding: var(--ui-space-6) var(--ui-space-8);
328
+ background: var(--ui-surface-low);
329
+ border: 1px solid var(--ui-border-subtle);
330
+ border-radius: var(--ui-radius-md);
331
+ color: var(--ui-text-secondary);
332
+ font-size: var(--ui-font-size-md);
333
+ cursor: pointer;
334
+ transition: all var(--ui-transition-fast);
335
+ white-space: nowrap;
336
+ }
337
+
338
+ .pfm-btn i {
339
+ width: 1rem;
340
+ text-align: center;
341
+ }
342
+
343
+ .pfm-btn:hover:not(:disabled) {
344
+ background: var(--ui-surface);
345
+ color: var(--ui-text-primary);
346
+ border-color: var(--ui-border-default);
347
+ }
348
+
349
+ .pfm-btn:disabled {
350
+ opacity: 0.5;
351
+ cursor: not-allowed;
352
+ }
353
+
354
+ .pfm-btn.active {
355
+ background: var(--ui-surface);
356
+ border-color: var(--ui-border-default);
357
+ color: var(--ui-text-primary);
358
+ }
359
+
360
+ .save-row {
361
+ display: flex;
362
+ gap: var(--ui-space-4);
363
+ }
364
+
365
+ .save-row .save-btn {
366
+ flex: 1;
367
+ min-width: 0;
368
+ }
369
+
370
+ .increment-btn {
371
+ flex: 0 0 auto;
372
+ width: 34px;
373
+ padding: 0;
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ }
378
+
379
+ .save-btn {
380
+ background: var(--ui-surface-high);
381
+ border-color: var(--ui-border-medium);
382
+ color: var(--ui-text-primary);
383
+ }
384
+
385
+ .save-btn:hover:not(:disabled) {
386
+ background: var(--ui-surface-higher);
387
+ border-color: var(--ui-border-strong);
388
+ }
389
+
390
+ .save-btn.saving i {
391
+ animation: spin 1s linear infinite;
392
+ }
393
+ .save-btn.saved {
394
+ background: var(--ui-surface-highest);
395
+ color: var(--ui-text-success);
396
+ }
397
+ .save-btn.error {
398
+ background: var(--ui-surface-high);
399
+ color: var(--ui-text-muted);
400
+ }
401
+
402
+ .save-as-inline {
403
+ display: flex;
404
+ flex-direction: column;
405
+ gap: var(--ui-space-4);
406
+ }
407
+
408
+ .save-as-actions {
409
+ display: flex;
410
+ gap: var(--ui-space-4);
411
+ justify-content: flex-end;
412
+ }
413
+
414
+ .save-as-input {
415
+ flex: 1;
416
+ min-width: 0;
417
+ padding: var(--ui-space-6) var(--ui-space-8);
418
+ background: var(--ui-surface-lowest);
419
+ border: 1px solid var(--ui-border-subtle);
420
+ border-radius: var(--ui-radius-md);
421
+ color: var(--ui-text-primary);
422
+ font-size: var(--ui-font-size-md);
423
+ outline: none;
424
+ }
425
+
426
+ .save-as-input:focus {
427
+ border-color: var(--ui-border-medium);
428
+ }
429
+
430
+ .save-as-input::placeholder {
431
+ color: var(--ui-text-muted);
432
+ }
433
+
434
+ .inline-btn {
435
+ flex: 0 0 auto;
436
+ display: flex;
437
+ align-items: center;
438
+ justify-content: center;
439
+ width: 30px;
440
+ height: 30px;
441
+ padding: 0;
442
+ background: var(--ui-surface-low);
443
+ border: 1px solid var(--ui-border-subtle);
444
+ border-radius: var(--ui-radius-md);
445
+ color: var(--ui-text-secondary);
446
+ font-size: var(--ui-font-size-sm);
447
+ cursor: pointer;
448
+ transition: all var(--ui-transition-fast);
449
+ }
450
+
451
+ .inline-btn:hover:not(:disabled) {
452
+ background: var(--ui-surface);
453
+ color: var(--ui-text-primary);
454
+ border-color: var(--ui-border-default);
455
+ }
456
+
457
+ .inline-btn:disabled {
458
+ opacity: 0.5;
459
+ cursor: not-allowed;
460
+ }
461
+
462
+ .inline-btn.confirm-btn {
463
+ background: var(--ui-surface-high);
464
+ border-color: var(--ui-border-medium);
465
+ color: var(--ui-text-primary);
466
+ }
467
+
468
+ .load-list {
469
+ display: flex;
470
+ flex-direction: column;
471
+ max-height: 60vh;
472
+ overflow-y: auto;
473
+ }
474
+
475
+ .load-item {
476
+ display: flex;
477
+ align-items: center;
478
+ gap: 6px;
479
+ padding: 4px 6px;
480
+ border-bottom: 1px solid #2a2a2a;
481
+ }
482
+
483
+ .load-item:last-child {
484
+ border-bottom: none;
485
+ }
486
+
487
+ .load-item.empty {
488
+ padding: 16px;
489
+ color: #888;
490
+ font-size: 14px;
491
+ text-align: center;
492
+ }
493
+
494
+ .load-name-btn {
495
+ flex: 1;
496
+ min-width: 0;
497
+ overflow: hidden;
498
+ text-overflow: ellipsis;
499
+ white-space: nowrap;
500
+ padding: 6px 4px;
501
+ background: none;
502
+ border: none;
503
+ color: #aaa;
504
+ font-size: 14px;
505
+ cursor: pointer;
506
+ text-align: left;
507
+ border-radius: 3px;
508
+ }
509
+
510
+ .load-name-btn:hover {
511
+ color: #e0e0e0;
512
+ }
513
+
514
+ .load-item.active .load-name-btn {
515
+ color: #e0e0e0;
516
+ font-weight: 600;
517
+ }
518
+
519
+ .active-badge {
520
+ flex-shrink: 0;
521
+ font-size: 12px;
522
+ padding: 1px 6px;
523
+ border-radius: 3px;
524
+ background: #333;
525
+ color: #ccc;
526
+ }
527
+
528
+ .file-delete-btn {
529
+ flex-shrink: 0;
530
+ display: flex;
531
+ align-items: center;
532
+ justify-content: center;
533
+ width: 24px;
534
+ height: 24px;
535
+ padding: 0;
536
+ background: none;
537
+ border: none;
538
+ color: #555;
539
+ font-size: 12px;
540
+ cursor: pointer;
541
+ opacity: 0;
542
+ }
543
+
544
+ .load-item:hover .file-delete-btn {
545
+ opacity: 1;
546
+ }
547
+
548
+ .file-delete-btn:hover {
549
+ color: #ccc;
550
+ }
551
+
552
+ .apply-status {
553
+ font-size: var(--ui-font-size-xs);
554
+ color: var(--ui-text-secondary);
555
+ padding: 0 var(--ui-space-4);
556
+ letter-spacing: 0.02em;
557
+ }
558
+
559
+ @keyframes spin {
560
+ from {
561
+ transform: rotate(0deg);
562
+ }
563
+ to {
564
+ transform: rotate(360deg);
565
+ }
566
+ }
567
+ </style>