@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,26 @@
1
+ /**
2
+ * Palettes slice — each PaletteEditor instance is keyed by `label`
3
+ * (e.g. "Neutral", "Primary"). The store owns the full `PaletteConfig` for
4
+ * each label; CSS-var emission is still done by `paletteDerivation`
5
+ * (palette derivation involves OKLCH + bezier curves) at render time.
6
+ */
7
+ import type { PaletteConfig } from '../themeTypes';
8
+ import { store, mutate, persist } from '../editorCore';
9
+
10
+ export function setPaletteConfig(label: string, config: PaletteConfig): void {
11
+ mutate(`update palette ${label}`, (s) => {
12
+ s.palettes[label] = config;
13
+ });
14
+ }
15
+
16
+ /**
17
+ * Server-boot path: populate all palettes at once without a history entry.
18
+ * Mirrors `seedFontsFromTheme`.
19
+ */
20
+ export function seedPalettesFromTheme(palettes: Record<string, PaletteConfig>): void {
21
+ store.update((s) => {
22
+ s.palettes = structuredClone(palettes);
23
+ return s;
24
+ });
25
+ persist();
26
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Shadows slice — five-token scale (sm/md/lg/xl/2xl) plus globals/overrides
3
+ * for the editor UI's shared sliders. Defaults come from tokens.css (not the
4
+ * editor), so state.shadows starts with `tokens: []` and we do not emit any
5
+ * shadow CSS vars until the editor has populated tokens (via
6
+ * `seedShadowsFromDom` on hydrate, or via `loadFromFile`). Once tokens exist,
7
+ * the renderer writes one CSS var per token derived from its
8
+ * x/y/blur/spread/hsla fields.
9
+ */
10
+ import { get } from 'svelte/store';
11
+ import type { EditorState, ShadowToken } from '../editorTypes';
12
+ import { store, persist } from '../editorCore';
13
+
14
+ export const SHADOW_VAR_NAMES = [
15
+ '--shadow-sm', '--shadow-md', '--shadow-lg', '--shadow-xl', '--shadow-2xl',
16
+ ] as const;
17
+
18
+ // Identical literal set as SHADOW_VAR_NAMES — derived to avoid drift.
19
+ export const SCALE_SHADOW_VARIABLES: ReadonlySet<string> = new Set(SHADOW_VAR_NAMES);
20
+
21
+ export function computeShadowXY(angle: number, distance: number): { x: number; y: number } {
22
+ const rad = angle * (Math.PI / 180);
23
+ return {
24
+ x: Math.round(-distance * Math.cos(rad)),
25
+ y: Math.round(distance * Math.sin(rad)),
26
+ };
27
+ }
28
+
29
+ function computeAngleDistance(x: number, y: number): { angle: number; distance: number } {
30
+ const distance = Math.round(Math.sqrt(x * x + y * y));
31
+ if (distance === 0) return { angle: 135, distance: 0 };
32
+ let angle = Math.atan2(y, -x) * (180 / Math.PI);
33
+ if (angle < 0) angle += 360;
34
+ return { angle: Math.round(angle), distance };
35
+ }
36
+
37
+ export function shadowTokenCss(t: ShadowToken): string {
38
+ return `${t.x}px ${t.y}px ${t.blur}px ${t.spread}px hsla(${t.hue}, ${t.saturation}%, ${t.lightness}%, ${t.opacity})`;
39
+ }
40
+
41
+ export function defaultShadowOverride(): import('../editorTypes').ShadowOverrideFlags {
42
+ return { angle: false, opacity: false, color: false, distance: false, blur: false, size: false };
43
+ }
44
+
45
+ export function parseShadowCss(variable: string, raw: string): ShadowToken | null {
46
+ const m = raw.trim().match(/^(-?\d+)px\s+(-?\d+)px\s+(\d+)px\s+(-?\d+)px\s+hsla\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)$/);
47
+ if (!m) return null;
48
+ const x = parseInt(m[1], 10);
49
+ const y = parseInt(m[2], 10);
50
+ const blur = parseInt(m[3], 10);
51
+ const spread = parseInt(m[4], 10);
52
+ const hue = Math.round(parseFloat(m[5]));
53
+ const saturation = Math.round(parseFloat(m[6]));
54
+ const lightness = Math.round(parseFloat(m[7]));
55
+ const opacity = parseFloat(m[8]);
56
+ const { angle, distance } = computeAngleDistance(x, y);
57
+ return { variable, x, y, blur, spread, opacity, hue, saturation, lightness, angle, distance };
58
+ }
59
+
60
+ export function shadowsToVars(shadows: EditorState['shadows']): Record<string, string> {
61
+ const out: Record<string, string> = {};
62
+ for (const t of shadows.tokens) out[t.variable] = shadowTokenCss(t);
63
+ return out;
64
+ }
65
+
66
+ export function applyShadowVarsToState(shadows: EditorState['shadows'], vars: Record<string, string>): void {
67
+ const parsed: ShadowToken[] = [];
68
+ for (const name of SHADOW_VAR_NAMES) {
69
+ const raw = vars[name];
70
+ if (!raw) continue;
71
+ const tok = parseShadowCss(name, raw);
72
+ if (tok) parsed.push(tok);
73
+ }
74
+ if (parsed.length > 0) shadows.tokens = parsed;
75
+ }
76
+
77
+ /**
78
+ * Loader: route shadow scale tokens from a freshly-loaded theme's vars bag
79
+ * into `next.shadows.tokens` and remove them from the bag. Globals/overrides
80
+ * are preserved across theme loads from the *current* state — themes don't
81
+ * carry editor-UI state — so the caller copies them in. Mutates `next` and
82
+ * `rawVars` in place.
83
+ */
84
+ export function loadShadowsFromVars(
85
+ next: EditorState,
86
+ rawVars: Record<string, string>,
87
+ ): void {
88
+ applyShadowVarsToState(next.shadows, rawVars);
89
+ for (const name of SHADOW_VAR_NAMES) delete rawVars[name];
90
+ // Preserve shadow globals/overrides across theme loads so the editor UI
91
+ // reopens with the same controls the user was working with.
92
+ const current = get(store).shadows;
93
+ next.shadows.globals = structuredClone(current.globals);
94
+ next.shadows.overrides = structuredClone(current.overrides);
95
+ }
96
+
97
+ /**
98
+ * Seed state.shadows.tokens from computed styles on the document element.
99
+ * Captures the tokens.css baseline so the editor can mutate it. Does NOT push
100
+ * a history entry; the seed is treated as an initial snapshot, not a user
101
+ * edit. Persists so a reload doesn't re-seed from the DOM on every fresh
102
+ * session.
103
+ *
104
+ * Called from the persistence layer's `hydrate()` so the seed lands once on
105
+ * boot regardless of whether the user opens the shadows tab — m13 cleanup.
106
+ */
107
+ export function seedShadowsFromDom(): void {
108
+ if (typeof document === 'undefined') return;
109
+ const current = get(store);
110
+ if (current.shadows.tokens.length > 0) return;
111
+ const cs = getComputedStyle(document.documentElement);
112
+ const parsed: ShadowToken[] = [];
113
+ for (const name of SHADOW_VAR_NAMES) {
114
+ const raw = cs.getPropertyValue(name).trim();
115
+ if (!raw) continue;
116
+ const tok = parseShadowCss(name, raw);
117
+ if (tok) parsed.push(tok);
118
+ }
119
+ if (parsed.length === 0) return;
120
+ store.update((s) => { s.shadows.tokens = parsed; return s; });
121
+ // No bumpTick — seed is hydration-equivalent, not an edit.
122
+ persist();
123
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Helpers for operations that are *intentionally* allowed to fail silently:
3
+ * boot-time storage reads, debounced storage writes, and dev-server fetches.
4
+ *
5
+ * Naming the silence ("quietGet", "quietSet", "safeFetch") communicates intent
6
+ * better than scattered `try { ... } catch {}` blocks, where the empty catch
7
+ * leaves it ambiguous whether the author considered the failure mode or just
8
+ * copied a nearby pattern.
9
+ */
10
+
11
+ interface QuietGetOptions {
12
+ /** When true, the stored string is parsed as JSON. Parse failures return null. */
13
+ parse?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Read a value from `localStorage`. Returns `null` on:
18
+ * - Missing key
19
+ * - Storage unavailable (private/incognito mode, quota, browser disabled)
20
+ * - JSON parse failure (when `parse: true`)
21
+ *
22
+ * Boot-time hydrate paths can call this without a try/catch and trust that
23
+ * a fresh-boot fallback will be used when storage isn't reachable.
24
+ */
25
+ export function quietGet<T = string>(
26
+ key: string,
27
+ opts?: QuietGetOptions,
28
+ ): T | string | null {
29
+ try {
30
+ if (typeof localStorage === 'undefined') return null;
31
+ const raw = localStorage.getItem(key);
32
+ if (raw === null) return null;
33
+ if (opts?.parse) {
34
+ try {
35
+ return JSON.parse(raw) as T;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ return raw;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Write a value to `localStorage`. Returns `true` on success, `false` if
48
+ * storage was unavailable or the write failed (quota, security errors).
49
+ *
50
+ * Callers that want to know whether the write landed can branch on the
51
+ * return value; callers that don't care can ignore it.
52
+ */
53
+ export function quietSet(key: string, value: string): boolean {
54
+ try {
55
+ if (typeof localStorage === 'undefined') return false;
56
+ localStorage.setItem(key, value);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Fetch a JSON resource. Returns `null` on:
65
+ * - Network failure (offline, dev-server not running)
66
+ * - Non-2xx response
67
+ * - JSON parse failure
68
+ *
69
+ * Used for boot-time API calls where the editor must continue working even
70
+ * if the dev-server's themeFileApi (or component-config endpoints) is not
71
+ * available — the caller falls through to defaults.
72
+ */
73
+ export async function safeFetch<T = unknown>(
74
+ url: string,
75
+ opts?: RequestInit,
76
+ ): Promise<T | null> {
77
+ try {
78
+ const res = await fetch(url, opts);
79
+ if (!res.ok) return null;
80
+ try {
81
+ return (await res.json()) as T;
82
+ } catch {
83
+ return null;
84
+ }
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
@@ -0,0 +1,74 @@
1
+ import type { Theme } from './themeTypes';
2
+ import { activeFileName } from './editorConfigStore';
3
+ import { migrateThemeFonts } from './fontMigration';
4
+ import { applyFontSources, applyFontStacks } from './fontLoader';
5
+ import { loadFromFile, seedComponentsFromApi } from './editorStore';
6
+ import { getActiveComponentConfig } from './componentConfigService';
7
+ import { safeFetch } from './storage';
8
+
9
+ interface ComponentSummaryDto {
10
+ name: string;
11
+ activeFile: string;
12
+ productionFile: string;
13
+ }
14
+
15
+ interface ListComponentsDto {
16
+ components: ComponentSummaryDto[];
17
+ }
18
+
19
+ /**
20
+ * Fetch the active theme from the server and apply its CSS variables
21
+ * to :root before the app mounts. Seeds the editor store so PaletteEditors
22
+ * initialize from the theme instead of stale localStorage.
23
+ *
24
+ * Routes the theme through `loadFromFile` so palette-derived vars in
25
+ * `deriveCssVars` correctly overwrite any stale hexes baked into
26
+ * `theme.cssVariables` by `handleSave`'s scrape. Writing
27
+ * `theme.cssVariables` directly to inline :root (the previous approach)
28
+ * bypassed the store and left the subscriber's `lastApplied` diff cache
29
+ * out of sync, so palette-derived values stayed stale until a
30
+ * `PaletteEditor` mounted and re-emitted them.
31
+ *
32
+ * Network/parse failures fall through silently — `tokens.css` provides
33
+ * defaults and the components slice stays empty until first edit. We use
34
+ * `safeFetch` (instead of empty try/catch) to make the silence intentional.
35
+ */
36
+ export async function initializeTheme(): Promise<void> {
37
+ const theme = await safeFetch<Theme>('/api/themes/active');
38
+ if (theme) {
39
+ migrateThemeFonts(theme);
40
+ loadFromFile(theme);
41
+ if (theme.fontSources && theme.fontSources.length > 0) {
42
+ applyFontSources(theme.fontSources);
43
+ }
44
+ if (theme.fontStacks && theme.fontStacks.length > 0) {
45
+ applyFontStacks(theme.fontStacks, theme.fontSources ?? []);
46
+ }
47
+ const fileName = theme._fileName || 'default';
48
+ activeFileName.set(fileName);
49
+ }
50
+
51
+ const list = await safeFetch<ListComponentsDto>('/api/component-configs');
52
+ if (list && Array.isArray(list.components)) {
53
+ const configs: Record<
54
+ string,
55
+ { activeFile: string; aliases: Record<string, string>; config?: Record<string, unknown>; schemaVersion?: number }
56
+ > = {};
57
+ await Promise.all(
58
+ list.components.map(async (c) => {
59
+ const cfg = await getActiveComponentConfig(c.name);
60
+ if (cfg) {
61
+ configs[c.name] = {
62
+ activeFile: c.activeFile,
63
+ aliases: cfg.aliases,
64
+ config: cfg.config,
65
+ schemaVersion: cfg.schemaVersion,
66
+ };
67
+ }
68
+ }),
69
+ );
70
+ if (Object.keys(configs).length > 0) {
71
+ seedComponentsFromApi(configs);
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,101 @@
1
+ import { tick } from 'svelte';
2
+ import type { Theme, ThemeMeta } from './themeTypes';
3
+ import type { EditorState } from './editorTypes';
4
+ import {
5
+ versionedFileResource,
6
+ sanitizeFileName as sanitizeFileNameImpl,
7
+ } from './files/versionedFileResource';
8
+ import { loadFromFile as loadEditorState, toTheme, markSaved } from './editorStore';
9
+ import { activeFileName } from './editorConfigStore';
10
+ import { applyFontSources, applyFontStacks } from './fontLoader';
11
+ import { migrateThemeFonts } from './fontMigration';
12
+
13
+ // ── API helpers ──────────────────────────────────────────────
14
+ //
15
+ // All theme CRUD goes through `versionedFileResource('/api/themes')` —
16
+ // shared with `componentConfigService`'s per-component clients. Theme-specific
17
+ // response shapes (ThemeMeta list payload, ProductionInfo) are layered on top
18
+ // via the generic type parameters.
19
+
20
+ export interface ProductionInfo {
21
+ fileName: string;
22
+ name: string;
23
+ updatedAt: string;
24
+ cssVariables: Record<string, string>;
25
+ }
26
+
27
+ const themeResource = versionedFileResource<Theme, ThemeMeta, ProductionInfo>({
28
+ baseUrl: '/api/themes',
29
+ });
30
+
31
+ export async function listThemes(): Promise<ThemeMeta[]> {
32
+ const data = await themeResource.list();
33
+ return data.files;
34
+ }
35
+
36
+ export const loadTheme = (fileName: string): Promise<Theme> => themeResource.load(fileName);
37
+ export const saveTheme = (fileName: string, data: Theme): Promise<void> =>
38
+ themeResource.save(fileName, data);
39
+ export const deleteTheme = (fileName: string): Promise<void> => themeResource.remove(fileName);
40
+
41
+ export async function getActiveTheme(): Promise<Theme | null> {
42
+ return themeResource.getActive();
43
+ }
44
+
45
+ export const setActiveFile = (fileName: string): Promise<void> => themeResource.setActive(fileName);
46
+
47
+ // ── Production API helpers ─────────────────────────────────
48
+
49
+ export const getProductionInfo = (): Promise<ProductionInfo> => themeResource.getProductionInfo();
50
+
51
+ export async function setProductionFile(
52
+ fileName: string,
53
+ ): Promise<{ ok: boolean; fileName: string; name: string }> {
54
+ const data = await themeResource.setProduction(fileName);
55
+ return { ok: data.ok, fileName: data.fileName, name: data.name };
56
+ }
57
+
58
+ /** Sanitize a display name to a safe file name. Re-exported from the shared
59
+ * `files/versionedFileResource` so the dev-server plugin can import the
60
+ * canonical pure helper without depending on this module's CSS imports. */
61
+ export const sanitizeFileName = sanitizeFileNameImpl;
62
+
63
+ // ── Theme save/load orchestration ──────────────────────────
64
+ //
65
+ // `persistTheme` and `hydrateTheme` are the canonical entry points for
66
+ // round-tripping editor state to disk. Callers (e.g. `EditorShell`) need
67
+ // only handle UI-level concerns (status flashing, error chrome) and
68
+ // delegate the actual orchestration here.
69
+
70
+ /** Snapshot the editor state to disk under `fileName`, mark the file active,
71
+ * and clear the dirty flag. The caller is responsible for surfacing
72
+ * saving / saved / error UI states around this call. */
73
+ export async function persistTheme(
74
+ state: EditorState,
75
+ fileName: string,
76
+ displayName: string,
77
+ ): Promise<void> {
78
+ await tick();
79
+ const theme = toTheme(state, { name: displayName });
80
+ await saveTheme(fileName, theme);
81
+ await setActiveFile(fileName);
82
+ activeFileName.set(fileName);
83
+ markSaved();
84
+ }
85
+
86
+ /** Load a theme file into the editor state and re-apply font side-effects
87
+ * (@font-face rules + `--font-*` CSS vars on :root). */
88
+ export async function hydrateTheme(fileName: string): Promise<void> {
89
+ const theme = await loadTheme(fileName);
90
+ migrateThemeFonts(theme);
91
+ loadEditorState(theme);
92
+ // Font data is in state.fonts via loadEditorState; the DOM-side-effect
93
+ // helpers still need to run so @font-face rules and --font-* CSS vars
94
+ // land on :root.
95
+ if (theme.fontSources && theme.fontSources.length > 0) {
96
+ applyFontSources(theme.fontSources);
97
+ }
98
+ if (theme.fontStacks && theme.fontStacks.length > 0) {
99
+ applyFontStacks(theme.fontStacks, theme.fontSources ?? []);
100
+ }
101
+ }
@@ -0,0 +1,146 @@
1
+ import type { CurveAnchor } from '../ui/curveEngine';
2
+
3
+ export type GradientStyle = 'linear' | 'radial' | 'conic';
4
+
5
+ export interface GradientStop {
6
+ position: number;
7
+ paletteLabel: string;
8
+ }
9
+
10
+ export interface PaletteConfig {
11
+ baseColor: string;
12
+ tintHue: number;
13
+ tintChroma?: number;
14
+ lightnessCurve: CurveAnchor[];
15
+ saturationCurve: CurveAnchor[];
16
+ grayLightnessCurve: CurveAnchor[];
17
+ graySaturationCurve: CurveAnchor[];
18
+ scaleCurves: Record<string, { lightness: CurveAnchor[]; saturation: CurveAnchor[] }>;
19
+ curveOffset: Record<string, number>;
20
+ overrides: Record<string, string>;
21
+ snappedScales: string[];
22
+ emptyMode?: 'solid' | 'gradient';
23
+ emptyStep?: string;
24
+ gradientStyle?: GradientStyle;
25
+ gradientAngle?: number;
26
+ gradientReverse?: boolean;
27
+ gradientStops?: GradientStop[];
28
+ gradientSize?: 'page' | 'window';
29
+ anchorToBase?: boolean;
30
+ }
31
+
32
+ export type FontSourceKind = 'google' | 'typekit' | 'css-url' | 'font-face';
33
+
34
+ export interface FontFamily {
35
+ id: string;
36
+ name: string;
37
+ cssName: string;
38
+ weights?: number[];
39
+ italics?: boolean;
40
+ }
41
+
42
+ export interface FontSource {
43
+ id: string;
44
+ kind: FontSourceKind;
45
+ url?: string;
46
+ cssText?: string;
47
+ families: FontFamily[];
48
+ label?: string;
49
+ }
50
+
51
+ export type SystemCascadePreset = 'system-ui-sans' | 'system-ui-serif' | 'system-ui-mono';
52
+ export type GenericFamily = 'sans-serif' | 'serif' | 'monospace' | 'cursive' | 'fantasy';
53
+ export type FontStackVariable = '--font-display' | '--font-sans' | '--font-serif' | '--font-mono';
54
+
55
+ export type FontStackSlot =
56
+ | { kind: 'project'; familyId: string }
57
+ | { kind: 'system'; preset: SystemCascadePreset }
58
+ | { kind: 'generic'; value: GenericFamily };
59
+
60
+ export interface FontStack {
61
+ variable: FontStackVariable;
62
+ slots: FontStackSlot[];
63
+ }
64
+
65
+ export interface Theme {
66
+ name: string;
67
+ createdAt: string;
68
+ updatedAt: string;
69
+ editorConfigs: Record<string, PaletteConfig>;
70
+ cssVariables: Record<string, string>;
71
+ fontSources?: FontSource[];
72
+ fontStacks?: FontStack[];
73
+ /**
74
+ * Server-attached file-name marker for round-tripping the file identity
75
+ * back to the client. Set by `themeFileApi`'s GET handlers; read by
76
+ * `themeInit` to seed `activeFileName`. Optional and not persisted to disk.
77
+ */
78
+ _fileName?: string;
79
+ /**
80
+ * Migration stamp. Absent on legacy files, treated as 0; the loader runs
81
+ * any registered theme migrations whose `fromVersion >= file.schemaVersion`.
82
+ * Save paths stamp the current value so resaved files skip past
83
+ * migrations.
84
+ */
85
+ schemaVersion?: number;
86
+ }
87
+
88
+ export interface ThemeMeta {
89
+ name: string;
90
+ fileName: string;
91
+ updatedAt: string;
92
+ isActive: boolean;
93
+ }
94
+
95
+ export interface ComponentConfig {
96
+ name: string;
97
+ component: string;
98
+ createdAt: string;
99
+ updatedAt: string;
100
+ aliases: Record<string, string>;
101
+ config?: Record<string, unknown>;
102
+ /**
103
+ * Server-attached file-name marker. Same role as `Theme._fileName`. Set by
104
+ * the component-configs GET handlers; not persisted to disk.
105
+ */
106
+ _fileName?: string;
107
+ /**
108
+ * Migration stamp. Absent on legacy files, treated as 0. See `Theme.schemaVersion`.
109
+ */
110
+ schemaVersion?: number;
111
+ }
112
+
113
+ export interface ComponentConfigMeta {
114
+ name: string;
115
+ fileName: string;
116
+ updatedAt: string;
117
+ isActive: boolean;
118
+ isProduction: boolean;
119
+ }
120
+
121
+ /**
122
+ * Manifest that captures an entire site state — the active theme plus the
123
+ * active config for every component. Loading a preset flips the relevant
124
+ * `_active.json` pointers; the underlying theme + component-config files stay
125
+ * the source of truth, so editing them flows through any preset that
126
+ * references them.
127
+ */
128
+ export interface Preset {
129
+ name: string;
130
+ createdAt: string;
131
+ updatedAt: string;
132
+ /** File basename (no `.json`) of the theme this preset pins. */
133
+ theme: string;
134
+ /** Map of componentId → config file basename. Components omitted here fall
135
+ * back to "default" at apply time. */
136
+ componentConfigs: Record<string, string>;
137
+ /** Server-attached file-name marker. Same role as `Theme._fileName`. */
138
+ _fileName?: string;
139
+ }
140
+
141
+ export interface PresetMeta {
142
+ name: string;
143
+ fileName: string;
144
+ updatedAt: string;
145
+ isActive: boolean;
146
+ }