@motion-proto/live-tokens 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +160 -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 +51 -23
  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 +265 -82
  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 -31
  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 +49 -0
  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 -41
  158. package/src/{showcase → ui}/TextTab.svelte +27 -29
  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/pageSource.ts +0 -6
  195. package/src/lib/tokenInit.ts +0 -29
  196. package/src/lib/tokenService.ts +0 -144
  197. package/src/lib/tokenTypes.ts +0 -45
  198. package/src/pages/Admin.svelte +0 -100
  199. package/src/pages/ShowcasePage.svelte +0 -146
  200. package/src/showcase/BackupBrowser.svelte +0 -617
  201. package/src/showcase/ComponentsTab.svelte +0 -107
  202. package/src/showcase/PaletteEditor.svelte +0 -2579
  203. package/src/showcase/PaletteSelector.svelte +0 -627
  204. package/src/showcase/TokenMap.svelte +0 -54
  205. package/src/showcase/VariablesTab.svelte +0 -2657
  206. package/src/showcase/VisualsTab.svelte +0 -233
  207. package/src/showcase/demos/BadgeDemo.svelte +0 -58
  208. package/src/showcase/demos/CardDemo.svelte +0 -52
  209. package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -194
  210. package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -56
  211. package/src/showcase/demos/DialogDemo.svelte +0 -42
  212. package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -27
  213. package/src/showcase/demos/NotificationDemo.svelte +0 -149
  214. package/src/showcase/demos/ProgressBarDemo.svelte +0 -56
  215. package/src/showcase/demos/RadioButtonDemo.svelte +0 -58
  216. package/src/showcase/demos/SectionDividerDemo.svelte +0 -79
  217. package/src/showcase/demos/StandardButtonsDemo.svelte +0 -457
  218. package/src/showcase/demos/TabBarDemo.svelte +0 -60
  219. package/src/showcase/demos/TooltipDemo.svelte +0 -54
  220. package/src/showcase/editor.css +0 -93
  221. package/src/showcase/index.ts +0 -17
  222. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  223. package/src/styles/fonts/Domine/OFL.txt +0 -97
  224. package/src/styles/fonts/Domine/README.txt +0 -66
  225. /package/src/{showcase → ui}/curveEngine.ts +0 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Aggregated CSS-var names owned by store domains. Consumers that scrape
3
+ * the DOM (e.g. the save flow still pulling palette ramps emitted by
4
+ * PaletteEditor) use this to drop domain-owned keys from their scraped bag
5
+ * so the store stays the single source of truth.
6
+ */
7
+ import { COLUMN_VAR_NAMES } from './columns';
8
+ import { OVERLAY_VAR_NAMES } from './overlays';
9
+ import { SHADOW_VAR_NAMES } from './shadows';
10
+
11
+ export const DOMAIN_VAR_NAMES: readonly string[] = [
12
+ ...COLUMN_VAR_NAMES,
13
+ ...OVERLAY_VAR_NAMES,
14
+ ...SHADOW_VAR_NAMES,
15
+ ];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Fonts slice — sources + stacks. No derived CSS vars owned by this store
3
+ * (the `--font-*` vars are written by `applyFontStacks` in fontLoader, and
4
+ * @font-face rules by `applyFontSources`). We own the *data* (sources +
5
+ * stacks); callers still invoke the DOM-side-effect helpers themselves
6
+ * after mutating.
7
+ */
8
+ import type { FontSource, FontStack } from '../themeTypes';
9
+ import { store, mutate, persist } from '../editorCore';
10
+
11
+ export function setFontSources(sources: FontSource[]): void {
12
+ mutate('update font sources', (s) => { s.fonts.sources = sources; });
13
+ }
14
+
15
+ export function setFontStacks(stacks: FontStack[]): void {
16
+ mutate('update font stacks', (s) => { s.fonts.stacks = stacks; });
17
+ }
18
+
19
+ /**
20
+ * Populate fonts from the server's active theme at boot. Does not push
21
+ * a history entry — the boot load is a starting point, not an edit.
22
+ */
23
+ export function seedFontsFromTheme(sources: FontSource[], stacks: FontStack[]): void {
24
+ store.update((s) => {
25
+ s.fonts.sources = structuredClone(sources);
26
+ s.fonts.stacks = structuredClone(stacks);
27
+ return s;
28
+ });
29
+ persist();
30
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Gradients slice — fixed four-slot scale (--gradient-1 … --gradient-4),
3
+ * each rendering to a single CSS var. Stops carry token-name references
4
+ * (`--color-brand-500`); the renderer wraps them in `var(...)` so palette
5
+ * edits flow through.
6
+ */
7
+ import type { EditorState, GradientToken, GradientTokenStop, GradientType } from '../editorTypes';
8
+ import { mutate } from '../editorCore';
9
+
10
+ export function makeDefaultGradients(): GradientToken[] {
11
+ return [
12
+ {
13
+ variable: '--gradient-1',
14
+ type: 'linear',
15
+ angle: 90,
16
+ stops: [
17
+ { position: 0, color: '--color-brand-500' },
18
+ { position: 100, color: '--color-accent-500' },
19
+ ],
20
+ },
21
+ {
22
+ variable: '--gradient-2',
23
+ type: 'linear',
24
+ angle: 135,
25
+ stops: [
26
+ { position: 0, color: '--color-brand-500' },
27
+ { position: 100, color: '--color-special-500' },
28
+ ],
29
+ },
30
+ {
31
+ variable: '--gradient-3',
32
+ type: 'linear',
33
+ angle: 90,
34
+ stops: [
35
+ { position: 0, color: '--color-success-500' },
36
+ { position: 100, color: '--color-info-500' },
37
+ ],
38
+ },
39
+ {
40
+ variable: '--gradient-4',
41
+ type: 'linear',
42
+ angle: 45,
43
+ stops: [
44
+ { position: 0, color: '--color-danger-500' },
45
+ { position: 100, color: '--color-warning-500' },
46
+ ],
47
+ },
48
+ ];
49
+ }
50
+
51
+ function formatGradientStop(s: GradientTokenStop): string {
52
+ const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
53
+ const opacity = s.opacity ?? 100;
54
+ const color = opacity >= 100
55
+ ? base
56
+ : `color-mix(in srgb, ${base} ${opacity}%, transparent)`;
57
+ return `${color} ${s.position}%`;
58
+ }
59
+
60
+ /** Stops portion only — used by the palette selector to materialize a
61
+ * linear-gradient with a per-slot angle override while keeping the token's
62
+ * stop list (and its `var(--color-…)` refs, which propagate palette edits). */
63
+ export function formatGradientStops(t: GradientToken): string {
64
+ return t.stops.map(formatGradientStop).join(', ');
65
+ }
66
+
67
+ function formatGradient(t: GradientToken): string {
68
+ const stops = formatGradientStops(t);
69
+ if (t.type === 'linear') return `linear-gradient(${t.angle}deg, ${stops})`;
70
+ return `radial-gradient(${stops})`;
71
+ }
72
+
73
+ export function gradientsToVars(g: EditorState['gradients']): Record<string, string> {
74
+ const out: Record<string, string> = {};
75
+ for (const t of g.tokens) out[t.variable] = formatGradient(t);
76
+ return out;
77
+ }
78
+
79
+ function findGradient(s: EditorState, variable: string): GradientToken | undefined {
80
+ return s.gradients.tokens.find((t) => t.variable === variable);
81
+ }
82
+
83
+ /** Replace a gradient's type, angle, and stops in one shot. Used by the editor
84
+ * to restore a pre-edit snapshot on Cancel. */
85
+ export function setGradient(
86
+ variable: string,
87
+ next: { type: GradientType; angle: number; stops: GradientTokenStop[] },
88
+ ): void {
89
+ mutate(`replace gradient ${variable}`, (s) => {
90
+ const t = findGradient(s, variable);
91
+ if (!t) return;
92
+ t.type = next.type;
93
+ t.angle = next.angle;
94
+ t.stops = next.stops.map((st) => ({ ...st }));
95
+ });
96
+ }
97
+
98
+ export function setGradientType(variable: string, type: GradientType): void {
99
+ mutate(`set gradient type ${variable}`, (s) => {
100
+ const t = findGradient(s, variable);
101
+ if (t) t.type = type;
102
+ });
103
+ }
104
+
105
+ export function setGradientAngle(variable: string, angle: number): void {
106
+ mutate(`set gradient angle ${variable}`, (s) => {
107
+ const t = findGradient(s, variable);
108
+ if (t) t.angle = angle;
109
+ });
110
+ }
111
+
112
+ export function setGradientStop(variable: string, index: number, stop: Partial<GradientTokenStop>): void {
113
+ mutate(`set gradient stop ${variable}[${index}]`, (s) => {
114
+ const t = findGradient(s, variable);
115
+ if (!t || !t.stops[index]) return;
116
+ if (stop.position !== undefined) t.stops[index].position = stop.position;
117
+ if (stop.color !== undefined) t.stops[index].color = stop.color;
118
+ if (stop.opacity !== undefined) t.stops[index].opacity = stop.opacity;
119
+ });
120
+ }
121
+
122
+ export function addGradientStop(variable: string, stop: GradientTokenStop): void {
123
+ mutate(`add gradient stop ${variable}`, (s) => {
124
+ const t = findGradient(s, variable);
125
+ if (!t) return;
126
+ t.stops.push(stop);
127
+ t.stops.sort((a, b) => a.position - b.position);
128
+ });
129
+ }
130
+
131
+ export function removeGradientStop(variable: string, index: number): void {
132
+ mutate(`remove gradient stop ${variable}[${index}]`, (s) => {
133
+ const t = findGradient(s, variable);
134
+ if (!t || t.stops.length <= 2) return;
135
+ t.stops.splice(index, 1);
136
+ });
137
+ }
138
+
139
+ export function addGradientToken(token: GradientToken): void {
140
+ mutate(`add gradient ${token.variable}`, (s) => {
141
+ if (findGradient(s, token.variable)) return;
142
+ s.gradients.tokens.push({
143
+ ...token,
144
+ stops: token.stops.map((st) => ({ ...st })),
145
+ });
146
+ });
147
+ }
148
+
149
+ export function removeGradientToken(variable: string): void {
150
+ mutate(`remove gradient ${variable}`, (s) => {
151
+ s.gradients.tokens = s.gradients.tokens.filter((t) => t.variable !== variable);
152
+ });
153
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Overlays slice — overlay + hover RGBA tokens. Defaults are editor-defined
3
+ * (mirror what `VariablesTab` historically initialised into local let state)
4
+ * and diverge from tokens.css by design: the editor starts with a neutral
5
+ * palette and tokens.css continues to win until first edit.
6
+ */
7
+ import type { EditorState, OverlayToken } from '../editorTypes';
8
+
9
+ export function makeDefaultOverlayTokens(): OverlayToken[] {
10
+ return [
11
+ { variable: '--overlay-lowest', label: 'Lowest', r: 0, g: 0, b: 0, opacity: 0.05 },
12
+ { variable: '--overlay-lower', label: 'Lower', r: 0, g: 0, b: 0, opacity: 0.1 },
13
+ { variable: '--overlay-low', label: 'Low', r: 0, g: 0, b: 0, opacity: 0.2 },
14
+ { variable: '--overlay', label: 'Base', r: 0, g: 0, b: 0, opacity: 0.3 },
15
+ { variable: '--overlay-high', label: 'High', r: 0, g: 0, b: 0, opacity: 0.5 },
16
+ { variable: '--overlay-higher', label: 'Higher', r: 0, g: 0, b: 0, opacity: 0.7 },
17
+ { variable: '--overlay-highest',label: 'Highest',r: 0, g: 0, b: 0, opacity: 0.95 },
18
+ ];
19
+ }
20
+
21
+ export function makeDefaultHoverTokens(): OverlayToken[] {
22
+ return [
23
+ { variable: '--hover-low', label: 'Low', r: 255, g: 255, b: 255, opacity: 0.05 },
24
+ { variable: '--hover', label: 'Base', r: 255, g: 255, b: 255, opacity: 0.1 },
25
+ { variable: '--hover-high', label: 'High', r: 255, g: 255, b: 255, opacity: 0.15 },
26
+ ];
27
+ }
28
+
29
+ export function makeDefaultOverlaysState(): EditorState['overlays'] {
30
+ return {
31
+ tokens: makeDefaultOverlayTokens(),
32
+ hoverTokens: makeDefaultHoverTokens(),
33
+ globals: {
34
+ overlay: { hue: 0, saturation: 0, lightness: 0, opacityMin: 0.05, opacityMax: 0.95 },
35
+ hover: { hue: 0, saturation: 0, lightness: 100, opacityMin: 0.05, opacityMax: 0.15 },
36
+ },
37
+ };
38
+ }
39
+
40
+ export const OVERLAY_VAR_NAMES = [
41
+ '--overlay-lowest', '--overlay-lower', '--overlay-low', '--overlay',
42
+ '--overlay-high', '--overlay-higher', '--overlay-highest',
43
+ '--hover-low', '--hover', '--hover-high',
44
+ ] as const;
45
+
46
+ // Accepts rgb(), rgba(), and #rrggbb[aa] — themes saved by the editor
47
+ // always use rgba(), but loading hand-written files shouldn't break.
48
+ export const RGBA_RE = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i;
49
+ export const HEX_RE = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i;
50
+
51
+ export function parseRgba(raw: string): { r: number; g: number; b: number; opacity: number } | null {
52
+ const s = raw.trim();
53
+ const m = s.match(RGBA_RE);
54
+ if (m) {
55
+ const r = parseInt(m[1], 10);
56
+ const g = parseInt(m[2], 10);
57
+ const b = parseInt(m[3], 10);
58
+ const a = m[4] !== undefined ? parseFloat(m[4]) : 1;
59
+ if (![r, g, b].every((n) => Number.isFinite(n) && n >= 0 && n <= 255)) return null;
60
+ return { r, g, b, opacity: Number.isFinite(a) ? a : 1 };
61
+ }
62
+ const h = s.match(HEX_RE);
63
+ if (h) {
64
+ const hex = h[1];
65
+ const alpha = h[2] !== undefined ? parseInt(h[2], 16) / 255 : 1;
66
+ return {
67
+ r: parseInt(hex.slice(0, 2), 16),
68
+ g: parseInt(hex.slice(2, 4), 16),
69
+ b: parseInt(hex.slice(4, 6), 16),
70
+ opacity: Math.round(alpha * 100) / 100,
71
+ };
72
+ }
73
+ return null;
74
+ }
75
+
76
+ export function overlayTokenToRgba(t: OverlayToken): string {
77
+ return `rgba(${t.r}, ${t.g}, ${t.b}, ${t.opacity})`;
78
+ }
79
+
80
+ export function overlaysToVars(o: EditorState['overlays']): Record<string, string> {
81
+ const out: Record<string, string> = {};
82
+ for (const t of o.tokens) out[t.variable] = overlayTokenToRgba(t);
83
+ for (const t of o.hoverTokens) out[t.variable] = overlayTokenToRgba(t);
84
+ return out;
85
+ }
86
+
87
+ function tokensEqualDefault(tokens: OverlayToken[], defaults: OverlayToken[]): boolean {
88
+ if (tokens.length !== defaults.length) return false;
89
+ for (let i = 0; i < tokens.length; i++) {
90
+ const a = tokens[i]; const b = defaults[i];
91
+ if (a.variable !== b.variable || a.r !== b.r || a.g !== b.g || a.b !== b.b || a.opacity !== b.opacity) return false;
92
+ }
93
+ return true;
94
+ }
95
+
96
+ /**
97
+ * Same pattern as columns: only emit overlay CSS vars once state diverges
98
+ * from the editor defaults. tokens.css owns the rgba values until the
99
+ * user touches any overlay control (or loads a theme that already
100
+ * contains overrides).
101
+ */
102
+ export function overlaysEqualsDefault(o: EditorState['overlays']): boolean {
103
+ return tokensEqualDefault(o.tokens, makeDefaultOverlayTokens())
104
+ && tokensEqualDefault(o.hoverTokens, makeDefaultHoverTokens());
105
+ }
106
+
107
+ export function applyOverlayVarsToState(overlays: EditorState['overlays'], vars: Record<string, string>): void {
108
+ const applyTo = (list: OverlayToken[]) => {
109
+ for (const t of list) {
110
+ const raw = vars[t.variable];
111
+ if (!raw) continue;
112
+ const parsed = parseRgba(raw);
113
+ if (!parsed) continue;
114
+ t.r = parsed.r; t.g = parsed.g; t.b = parsed.b; t.opacity = parsed.opacity;
115
+ }
116
+ };
117
+ applyTo(overlays.tokens);
118
+ applyTo(overlays.hoverTokens);
119
+ }
120
+
121
+ /**
122
+ * Loader: route overlay/hover entries from a freshly-loaded theme's vars
123
+ * bag into `next.overlays` and remove them from the bag. Mutates `next`
124
+ * and `rawVars` in place.
125
+ */
126
+ export function loadOverlaysFromVars(
127
+ next: EditorState,
128
+ rawVars: Record<string, string>,
129
+ ): void {
130
+ applyOverlayVarsToState(next.overlays, rawVars);
131
+ for (const name of OVERLAY_VAR_NAMES) delete rawVars[name];
132
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Palettes slice — each PaletteEditor instance is keyed by `label`
3
+ * (e.g. "Neutral", "Primary"). The store owns the full `PaletteConfig` for
4
+ * each label; CSS-var emission is still done by `paletteDerivation`
5
+ * (palette derivation involves OKLCH + bezier curves) at render time.
6
+ */
7
+ import type { PaletteConfig } from '../themeTypes';
8
+ import { store, mutate, persist } from '../editorCore';
9
+
10
+ export function setPaletteConfig(label: string, config: PaletteConfig): void {
11
+ mutate(`update palette ${label}`, (s) => {
12
+ s.palettes[label] = config;
13
+ });
14
+ }
15
+
16
+ /**
17
+ * Server-boot path: populate all palettes at once without a history entry.
18
+ * Mirrors `seedFontsFromTheme`.
19
+ */
20
+ export function seedPalettesFromTheme(palettes: Record<string, PaletteConfig>): void {
21
+ store.update((s) => {
22
+ s.palettes = structuredClone(palettes);
23
+ return s;
24
+ });
25
+ persist();
26
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Shadows slice — five-token scale (sm/md/lg/xl/2xl) plus globals/overrides
3
+ * for the editor UI's shared sliders. Defaults come from tokens.css (not the
4
+ * editor), so state.shadows starts with `tokens: []` and we do not emit any
5
+ * shadow CSS vars until the editor has populated tokens (via
6
+ * `seedShadowsFromDom` on hydrate, or via `loadFromFile`). Once tokens exist,
7
+ * the renderer writes one CSS var per token derived from its
8
+ * x/y/blur/spread/hsla fields.
9
+ */
10
+ import { get } from 'svelte/store';
11
+ import type { EditorState, ShadowToken } from '../editorTypes';
12
+ import { store, persist } from '../editorCore';
13
+
14
+ export const SHADOW_VAR_NAMES = [
15
+ '--shadow-sm', '--shadow-md', '--shadow-lg', '--shadow-xl', '--shadow-2xl',
16
+ ] as const;
17
+
18
+ // Identical literal set as SHADOW_VAR_NAMES — derived to avoid drift.
19
+ export const SCALE_SHADOW_VARIABLES: ReadonlySet<string> = new Set(SHADOW_VAR_NAMES);
20
+
21
+ export function computeShadowXY(angle: number, distance: number): { x: number; y: number } {
22
+ const rad = angle * (Math.PI / 180);
23
+ return {
24
+ x: Math.round(-distance * Math.cos(rad)),
25
+ y: Math.round(distance * Math.sin(rad)),
26
+ };
27
+ }
28
+
29
+ function computeAngleDistance(x: number, y: number): { angle: number; distance: number } {
30
+ const distance = Math.round(Math.sqrt(x * x + y * y));
31
+ if (distance === 0) return { angle: 135, distance: 0 };
32
+ let angle = Math.atan2(y, -x) * (180 / Math.PI);
33
+ if (angle < 0) angle += 360;
34
+ return { angle: Math.round(angle), distance };
35
+ }
36
+
37
+ export function shadowTokenCss(t: ShadowToken): string {
38
+ return `${t.x}px ${t.y}px ${t.blur}px ${t.spread}px hsla(${t.hue}, ${t.saturation}%, ${t.lightness}%, ${t.opacity})`;
39
+ }
40
+
41
+ export function defaultShadowOverride(): import('../editorTypes').ShadowOverrideFlags {
42
+ return { angle: false, opacity: false, color: false, distance: false, blur: false, size: false };
43
+ }
44
+
45
+ export function parseShadowCss(variable: string, raw: string): ShadowToken | null {
46
+ const m = raw.trim().match(/^(-?\d+)px\s+(-?\d+)px\s+(\d+)px\s+(-?\d+)px\s+hsla\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)$/);
47
+ if (!m) return null;
48
+ const x = parseInt(m[1], 10);
49
+ const y = parseInt(m[2], 10);
50
+ const blur = parseInt(m[3], 10);
51
+ const spread = parseInt(m[4], 10);
52
+ const hue = Math.round(parseFloat(m[5]));
53
+ const saturation = Math.round(parseFloat(m[6]));
54
+ const lightness = Math.round(parseFloat(m[7]));
55
+ const opacity = parseFloat(m[8]);
56
+ const { angle, distance } = computeAngleDistance(x, y);
57
+ return { variable, x, y, blur, spread, opacity, hue, saturation, lightness, angle, distance };
58
+ }
59
+
60
+ export function shadowsToVars(shadows: EditorState['shadows']): Record<string, string> {
61
+ const out: Record<string, string> = {};
62
+ for (const t of shadows.tokens) out[t.variable] = shadowTokenCss(t);
63
+ return out;
64
+ }
65
+
66
+ export function applyShadowVarsToState(shadows: EditorState['shadows'], vars: Record<string, string>): void {
67
+ const parsed: ShadowToken[] = [];
68
+ for (const name of SHADOW_VAR_NAMES) {
69
+ const raw = vars[name];
70
+ if (!raw) continue;
71
+ const tok = parseShadowCss(name, raw);
72
+ if (tok) parsed.push(tok);
73
+ }
74
+ if (parsed.length > 0) shadows.tokens = parsed;
75
+ }
76
+
77
+ /**
78
+ * Loader: route shadow scale tokens from a freshly-loaded theme's vars bag
79
+ * into `next.shadows.tokens` and remove them from the bag. Globals/overrides
80
+ * are preserved across theme loads from the *current* state — themes don't
81
+ * carry editor-UI state — so the caller copies them in. Mutates `next` and
82
+ * `rawVars` in place.
83
+ */
84
+ export function loadShadowsFromVars(
85
+ next: EditorState,
86
+ rawVars: Record<string, string>,
87
+ ): void {
88
+ applyShadowVarsToState(next.shadows, rawVars);
89
+ for (const name of SHADOW_VAR_NAMES) delete rawVars[name];
90
+ // Preserve shadow globals/overrides across theme loads so the editor UI
91
+ // reopens with the same controls the user was working with.
92
+ const current = get(store).shadows;
93
+ next.shadows.globals = structuredClone(current.globals);
94
+ next.shadows.overrides = structuredClone(current.overrides);
95
+ }
96
+
97
+ /**
98
+ * Seed state.shadows.tokens from computed styles on the document element.
99
+ * Captures the tokens.css baseline so the editor can mutate it. Does NOT push
100
+ * a history entry; the seed is treated as an initial snapshot, not a user
101
+ * edit. Persists so a reload doesn't re-seed from the DOM on every fresh
102
+ * session.
103
+ *
104
+ * Called from the persistence layer's `hydrate()` so the seed lands once on
105
+ * boot regardless of whether the user opens the shadows tab — m13 cleanup.
106
+ */
107
+ export function seedShadowsFromDom(): void {
108
+ if (typeof document === 'undefined') return;
109
+ const current = get(store);
110
+ if (current.shadows.tokens.length > 0) return;
111
+ const cs = getComputedStyle(document.documentElement);
112
+ const parsed: ShadowToken[] = [];
113
+ for (const name of SHADOW_VAR_NAMES) {
114
+ const raw = cs.getPropertyValue(name).trim();
115
+ if (!raw) continue;
116
+ const tok = parseShadowCss(name, raw);
117
+ if (tok) parsed.push(tok);
118
+ }
119
+ if (parsed.length === 0) return;
120
+ store.update((s) => { s.shadows.tokens = parsed; return s; });
121
+ // No bumpTick — seed is hydration-equivalent, not an edit.
122
+ persist();
123
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Helpers for operations that are *intentionally* allowed to fail silently:
3
+ * boot-time storage reads, debounced storage writes, and dev-server fetches.
4
+ *
5
+ * Naming the silence ("quietGet", "quietSet", "safeFetch") communicates intent
6
+ * better than scattered `try { ... } catch {}` blocks, where the empty catch
7
+ * leaves it ambiguous whether the author considered the failure mode or just
8
+ * copied a nearby pattern.
9
+ */
10
+
11
+ interface QuietGetOptions {
12
+ /** When true, the stored string is parsed as JSON. Parse failures return null. */
13
+ parse?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Read a value from `localStorage`. Returns `null` on:
18
+ * - Missing key
19
+ * - Storage unavailable (private/incognito mode, quota, browser disabled)
20
+ * - JSON parse failure (when `parse: true`)
21
+ *
22
+ * Boot-time hydrate paths can call this without a try/catch and trust that
23
+ * a fresh-boot fallback will be used when storage isn't reachable.
24
+ */
25
+ export function quietGet<T = string>(
26
+ key: string,
27
+ opts?: QuietGetOptions,
28
+ ): T | string | null {
29
+ try {
30
+ if (typeof localStorage === 'undefined') return null;
31
+ const raw = localStorage.getItem(key);
32
+ if (raw === null) return null;
33
+ if (opts?.parse) {
34
+ try {
35
+ return JSON.parse(raw) as T;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ return raw;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Write a value to `localStorage`. Returns `true` on success, `false` if
48
+ * storage was unavailable or the write failed (quota, security errors).
49
+ *
50
+ * Callers that want to know whether the write landed can branch on the
51
+ * return value; callers that don't care can ignore it.
52
+ */
53
+ export function quietSet(key: string, value: string): boolean {
54
+ try {
55
+ if (typeof localStorage === 'undefined') return false;
56
+ localStorage.setItem(key, value);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Fetch a JSON resource. Returns `null` on:
65
+ * - Network failure (offline, dev-server not running)
66
+ * - Non-2xx response
67
+ * - JSON parse failure
68
+ *
69
+ * Used for boot-time API calls where the editor must continue working even
70
+ * if the dev-server's themeFileApi (or component-config endpoints) is not
71
+ * available — the caller falls through to defaults.
72
+ */
73
+ export async function safeFetch<T = unknown>(
74
+ url: string,
75
+ opts?: RequestInit,
76
+ ): Promise<T | null> {
77
+ try {
78
+ const res = await fetch(url, opts);
79
+ if (!res.ok) return null;
80
+ try {
81
+ return (await res.json()) as T;
82
+ } catch {
83
+ return null;
84
+ }
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
@@ -0,0 +1,74 @@
1
+ import type { Theme } from './themeTypes';
2
+ import { activeFileName } from './editorConfigStore';
3
+ import { migrateThemeFonts } from './fontMigration';
4
+ import { applyFontSources, applyFontStacks } from './fontLoader';
5
+ import { loadFromFile, seedComponentsFromApi } from './editorStore';
6
+ import { getActiveComponentConfig } from './componentConfigService';
7
+ import { safeFetch } from './storage';
8
+
9
+ interface ComponentSummaryDto {
10
+ name: string;
11
+ activeFile: string;
12
+ productionFile: string;
13
+ }
14
+
15
+ interface ListComponentsDto {
16
+ components: ComponentSummaryDto[];
17
+ }
18
+
19
+ /**
20
+ * Fetch the active theme from the server and apply its CSS variables
21
+ * to :root before the app mounts. Seeds the editor store so PaletteEditors
22
+ * initialize from the theme instead of stale localStorage.
23
+ *
24
+ * Routes the theme through `loadFromFile` so palette-derived vars in
25
+ * `deriveCssVars` correctly overwrite any stale hexes baked into
26
+ * `theme.cssVariables` by `handleSave`'s scrape. Writing
27
+ * `theme.cssVariables` directly to inline :root (the previous approach)
28
+ * bypassed the store and left the subscriber's `lastApplied` diff cache
29
+ * out of sync, so palette-derived values stayed stale until a
30
+ * `PaletteEditor` mounted and re-emitted them.
31
+ *
32
+ * Network/parse failures fall through silently — `tokens.css` provides
33
+ * defaults and the components slice stays empty until first edit. We use
34
+ * `safeFetch` (instead of empty try/catch) to make the silence intentional.
35
+ */
36
+ export async function initializeTheme(): Promise<void> {
37
+ const theme = await safeFetch<Theme>('/api/themes/active');
38
+ if (theme) {
39
+ migrateThemeFonts(theme);
40
+ loadFromFile(theme);
41
+ if (theme.fontSources && theme.fontSources.length > 0) {
42
+ applyFontSources(theme.fontSources);
43
+ }
44
+ if (theme.fontStacks && theme.fontStacks.length > 0) {
45
+ applyFontStacks(theme.fontStacks, theme.fontSources ?? []);
46
+ }
47
+ const fileName = theme._fileName || 'default';
48
+ activeFileName.set(fileName);
49
+ }
50
+
51
+ const list = await safeFetch<ListComponentsDto>('/api/component-configs');
52
+ if (list && Array.isArray(list.components)) {
53
+ const configs: Record<
54
+ string,
55
+ { activeFile: string; aliases: Record<string, string>; config?: Record<string, unknown>; schemaVersion?: number }
56
+ > = {};
57
+ await Promise.all(
58
+ list.components.map(async (c) => {
59
+ const cfg = await getActiveComponentConfig(c.name);
60
+ if (cfg) {
61
+ configs[c.name] = {
62
+ activeFile: c.activeFile,
63
+ aliases: cfg.aliases,
64
+ config: cfg.config,
65
+ schemaVersion: cfg.schemaVersion,
66
+ };
67
+ }
68
+ }),
69
+ );
70
+ if (Object.keys(configs).length > 0) {
71
+ seedComponentsFromApi(configs);
72
+ }
73
+ }
74
+ }