@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,130 @@
1
+ import type {
2
+ FontFamily,
3
+ FontSource,
4
+ FontStack,
5
+ FontStackSlot,
6
+ FontStackVariable,
7
+ SystemCascadePreset,
8
+ } from './themeTypes';
9
+ import { setCssVar, getSyncedDocuments } from './cssVarSync';
10
+
11
+ export const SYSTEM_CASCADES: Record<SystemCascadePreset, string> = {
12
+ 'system-ui-sans':
13
+ 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
14
+ 'system-ui-serif': 'Georgia, "Times New Roman", serif',
15
+ 'system-ui-mono': 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
16
+ };
17
+
18
+ const LINK_ATTR = 'data-font-source-id';
19
+ const STYLE_ATTR = 'data-font-source-id';
20
+
21
+ function syncedHeads(): HTMLHeadElement[] {
22
+ return getSyncedDocuments().map((d) => d.head);
23
+ }
24
+
25
+ function findExistingNode(head: HTMLHeadElement, sourceId: string): Element | null {
26
+ return head.querySelector(`[${LINK_ATTR}="${CSS.escape(sourceId)}"]`);
27
+ }
28
+
29
+ function buildNodeFor(source: FontSource, doc: Document): Element | null {
30
+ if (source.kind === 'font-face') {
31
+ if (!source.cssText) return null;
32
+ const style = doc.createElement('style');
33
+ style.setAttribute(STYLE_ATTR, source.id);
34
+ style.textContent = source.cssText;
35
+ return style;
36
+ }
37
+ if (!source.url) return null;
38
+ const link = doc.createElement('link');
39
+ link.rel = 'stylesheet';
40
+ link.href = source.url;
41
+ link.setAttribute(LINK_ATTR, source.id);
42
+ return link;
43
+ }
44
+
45
+ /**
46
+ * Inject/update <link>/<style> elements in <head> (and parent <head> when in
47
+ * an iframe) to match the given sources. Existing nodes are diffed by id so
48
+ * repeated calls don't thrash the DOM. Sources present in the DOM but absent
49
+ * from the list are removed.
50
+ */
51
+ export function applyFontSources(sources: FontSource[]): void {
52
+ const wanted = new Map(sources.map((s) => [s.id, s]));
53
+ for (const head of syncedHeads()) {
54
+ const existing = head.querySelectorAll(`[${LINK_ATTR}]`);
55
+ const seen = new Set<string>();
56
+ existing.forEach((node) => {
57
+ const id = node.getAttribute(LINK_ATTR);
58
+ if (!id) return;
59
+ const next = wanted.get(id);
60
+ if (!next) {
61
+ node.remove();
62
+ return;
63
+ }
64
+ seen.add(id);
65
+ if (next.kind === 'font-face') {
66
+ if (node.tagName !== 'STYLE' || node.textContent !== (next.cssText ?? '')) {
67
+ node.remove();
68
+ const replacement = buildNodeFor(next, head.ownerDocument!);
69
+ if (replacement) head.appendChild(replacement);
70
+ }
71
+ } else {
72
+ if (node.tagName !== 'LINK' || (node as HTMLLinkElement).href !== (next.url ?? '')) {
73
+ node.remove();
74
+ const replacement = buildNodeFor(next, head.ownerDocument!);
75
+ if (replacement) head.appendChild(replacement);
76
+ }
77
+ }
78
+ });
79
+ for (const [id, source] of wanted) {
80
+ if (seen.has(id)) continue;
81
+ const node = buildNodeFor(source, head.ownerDocument!);
82
+ if (node) head.appendChild(node);
83
+ }
84
+ }
85
+ }
86
+
87
+ function resolveSlot(slot: FontStackSlot, familyById: Map<string, FontFamily>): string | null {
88
+ if (slot.kind === 'project') {
89
+ const fam = familyById.get(slot.familyId);
90
+ return fam ? fam.cssName : null;
91
+ }
92
+ if (slot.kind === 'system') return SYSTEM_CASCADES[slot.preset];
93
+ return slot.value;
94
+ }
95
+
96
+ /**
97
+ * Resolve a fontStacks list to a flat var-name → css-value map. Exported so
98
+ * callers (e.g. the stack editor) can preview composed values without writing.
99
+ */
100
+ export function resolveFontStackValues(
101
+ stacks: FontStack[],
102
+ sources: FontSource[],
103
+ ): Record<FontStackVariable, string> {
104
+ const familyById = new Map<string, FontFamily>();
105
+ for (const src of sources) {
106
+ for (const f of src.families) familyById.set(f.id, f);
107
+ }
108
+ const out: Partial<Record<FontStackVariable, string>> = {};
109
+ for (const stack of stacks) {
110
+ const parts: string[] = [];
111
+ for (const slot of stack.slots) {
112
+ const v = resolveSlot(slot, familyById);
113
+ if (v) parts.push(v);
114
+ }
115
+ if (parts.length > 0) out[stack.variable] = parts.join(', ');
116
+ }
117
+ return out as Record<FontStackVariable, string>;
118
+ }
119
+
120
+ /**
121
+ * Compose each stack into its resolved "family1, family2, ..." string and
122
+ * write it to the matching --font-* variable on :root (and parent :root when
123
+ * in an iframe) via the same pipeline used for color variables.
124
+ */
125
+ export function applyFontStacks(stacks: FontStack[], sources: FontSource[]): void {
126
+ const resolved = resolveFontStackValues(stacks, sources);
127
+ for (const [name, value] of Object.entries(resolved)) {
128
+ setCssVar(name, value);
129
+ }
130
+ }
@@ -0,0 +1,140 @@
1
+ import type { FontFamily, FontSource, FontStack, Theme } from './themeTypes';
2
+ import frauncesRomanLatin from '../styles/fonts/Fraunces/Fraunces-roman-latin.woff2?url';
3
+ import frauncesItalicLatin from '../styles/fonts/Fraunces/Fraunces-italic-latin.woff2?url';
4
+ import manropeLatin from '../styles/fonts/Manrope/Manrope-latin.woff2?url';
5
+
6
+ function makeId(prefix: string): string {
7
+ return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
8
+ }
9
+
10
+ function familyId(sourceId: string, name: string): string {
11
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
12
+ return `${sourceId}:${slug}`;
13
+ }
14
+
15
+ function fam(sourceId: string, name: string, cssName?: string, weights?: number[]): FontFamily {
16
+ return {
17
+ id: familyId(sourceId, name),
18
+ name,
19
+ cssName: cssName ?? `"${name}"`,
20
+ ...(weights ? { weights } : {}),
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Build the default fontSources that match the hand-written src/styles/fonts.css.
26
+ * Used when a theme has no fontSources yet.
27
+ */
28
+ export function defaultFontSources(): FontSource[] {
29
+ const typekitId = 'src_typekit_jes8oow';
30
+ const frauncesId = 'src_fraunces_local';
31
+ const manropeId = 'src_manrope_local';
32
+
33
+ return [
34
+ {
35
+ id: typekitId,
36
+ kind: 'typekit',
37
+ url: 'https://use.typekit.net/jes8oow.css',
38
+ label: 'Adobe Typekit',
39
+ families: [
40
+ fam(typekitId, 'fira-code', '"fira-code"'),
41
+ ],
42
+ },
43
+ {
44
+ id: frauncesId,
45
+ kind: 'font-face',
46
+ cssText: `@font-face {\n font-family: "Fraunces";\n src: url('${frauncesRomanLatin}') format('woff2');\n font-weight: 100 900;\n font-style: normal;\n font-display: swap;\n}\n@font-face {\n font-family: "Fraunces";\n src: url('${frauncesItalicLatin}') format('woff2');\n font-weight: 100 900;\n font-style: italic;\n font-display: swap;\n}`,
47
+ label: 'Local',
48
+ families: [
49
+ fam(frauncesId, 'Fraunces', '"Fraunces"', [100, 200, 300, 400, 500, 600, 700, 800, 900]),
50
+ ],
51
+ },
52
+ {
53
+ id: manropeId,
54
+ kind: 'font-face',
55
+ cssText: `@font-face {\n font-family: "Manrope";\n src: url('${manropeLatin}') format('woff2');\n font-weight: 200 800;\n font-style: normal;\n font-display: swap;\n}`,
56
+ label: 'Local',
57
+ families: [
58
+ fam(manropeId, 'Manrope', '"Manrope"', [200, 300, 400, 500, 600, 700, 800]),
59
+ ],
60
+ },
61
+ ];
62
+ }
63
+
64
+ /**
65
+ * Build the default fontStacks matching the previous hard-coded display in
66
+ * VariablesTab.svelte plus tokens.css. Each stack references families from
67
+ * defaultFontSources() by id.
68
+ */
69
+ export function defaultFontStacks(sources: FontSource[]): FontStack[] {
70
+ const byName = new Map<string, string>();
71
+ for (const src of sources) {
72
+ for (const f of src.families) byName.set(f.name.toLowerCase(), f.id);
73
+ }
74
+ const p = (name: string): { kind: 'project'; familyId: string } | null => {
75
+ const id = byName.get(name.toLowerCase());
76
+ return id ? { kind: 'project', familyId: id } : null;
77
+ };
78
+ const pick = (...names: string[]) => names.map(p).filter((x): x is { kind: 'project'; familyId: string } => !!x);
79
+
80
+ return [
81
+ {
82
+ variable: '--font-display',
83
+ slots: [
84
+ ...pick('Fraunces'),
85
+ { kind: 'generic', value: 'serif' },
86
+ ],
87
+ },
88
+ {
89
+ variable: '--font-sans',
90
+ slots: [
91
+ ...pick('Manrope'),
92
+ { kind: 'system', preset: 'system-ui-sans' },
93
+ { kind: 'generic', value: 'sans-serif' },
94
+ ],
95
+ },
96
+ {
97
+ variable: '--font-serif',
98
+ slots: [
99
+ ...pick('Fraunces'),
100
+ { kind: 'generic', value: 'serif' },
101
+ ],
102
+ },
103
+ {
104
+ variable: '--font-mono',
105
+ slots: [
106
+ ...pick('fira-code'),
107
+ { kind: 'system', preset: 'system-ui-mono' },
108
+ { kind: 'generic', value: 'monospace' },
109
+ ],
110
+ },
111
+ ];
112
+ }
113
+
114
+ /**
115
+ * Ensure the loaded Theme has fontSources and fontStacks. Mutates in place
116
+ * only when missing; safe to call on already-migrated themes. Also strips any
117
+ * stale --font-* entries from cssVariables since those are now derived.
118
+ */
119
+ export function migrateThemeFonts(theme: Theme): { migrated: boolean } {
120
+ let migrated = false;
121
+ if (!theme.fontSources || theme.fontSources.length === 0) {
122
+ theme.fontSources = defaultFontSources();
123
+ migrated = true;
124
+ }
125
+ if (!theme.fontStacks || theme.fontStacks.length === 0) {
126
+ theme.fontStacks = defaultFontStacks(theme.fontSources);
127
+ migrated = true;
128
+ }
129
+ if (theme.cssVariables) {
130
+ for (const key of ['--font-display', '--font-sans', '--font-serif', '--font-mono']) {
131
+ if (key in theme.cssVariables) {
132
+ delete theme.cssVariables[key];
133
+ migrated = true;
134
+ }
135
+ }
136
+ }
137
+ return { migrated };
138
+ }
139
+
140
+ export { makeId };
@@ -0,0 +1,168 @@
1
+ import type { FontFamily, FontSource, FontSourceKind } from './themeTypes';
2
+
3
+ export interface ParsedFamily {
4
+ name: string;
5
+ weights?: number[];
6
+ italics?: boolean;
7
+ }
8
+
9
+ export interface ParsedSource {
10
+ kind: FontSourceKind;
11
+ families: ParsedFamily[];
12
+ }
13
+
14
+ /**
15
+ * Parse a Google Fonts CSS2 URL (`?family=Inter:wght@400;700&family=Roboto`)
16
+ * into the families and weights it declares. Pure; no network.
17
+ */
18
+ export function parseGoogleFontsUrl(url: string): ParsedFamily[] | null {
19
+ let u: URL;
20
+ try { u = new URL(url); } catch { return null; }
21
+ if (!u.hostname.includes('fonts.googleapis.com')) return null;
22
+ const familyParams = u.searchParams.getAll('family');
23
+ if (familyParams.length === 0) return null;
24
+
25
+ return familyParams.map((raw) => {
26
+ const [rawName, spec] = raw.split(':');
27
+ const name = decodeURIComponent(rawName).replace(/\+/g, ' ');
28
+ const parsed: ParsedFamily = { name };
29
+
30
+ if (spec) {
31
+ const axisMatch = spec.match(/(?:ital,)?wght@([^&]+)/);
32
+ const italics = spec.startsWith('ital,');
33
+ if (italics) parsed.italics = true;
34
+ if (axisMatch) {
35
+ const tokens = axisMatch[1].split(';');
36
+ const weights = new Set<number>();
37
+ for (const tok of tokens) {
38
+ const parts = tok.split(',');
39
+ const wghtStr = parts.length === 2 ? parts[1] : parts[0];
40
+ if (wghtStr.includes('..')) {
41
+ const [lo, hi] = wghtStr.split('..').map((n) => parseInt(n, 10));
42
+ if (!isNaN(lo) && !isNaN(hi)) {
43
+ for (let w = lo; w <= hi; w += 100) weights.add(w);
44
+ }
45
+ } else {
46
+ const w = parseInt(wghtStr, 10);
47
+ if (!isNaN(w)) weights.add(w);
48
+ }
49
+ }
50
+ if (weights.size > 0) parsed.weights = [...weights].sort((a, b) => a - b);
51
+ }
52
+ }
53
+ return parsed;
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Regex-extract family name + weight from arbitrary CSS text containing
59
+ * @font-face rules. Used for pasted raw @font-face blocks and for
60
+ * fetch-and-parse of opaque URLs (Typekit, generic).
61
+ */
62
+ export function parseFontFaceText(css: string): ParsedFamily[] {
63
+ const rules = [...css.matchAll(/@font-face\s*\{([^}]*)\}/g)];
64
+ const byName = new Map<string, { weights: Set<number>; italics: boolean }>();
65
+ for (const [, body] of rules) {
66
+ const nameMatch = body.match(/font-family\s*:\s*([^;]+);/i);
67
+ if (!nameMatch) continue;
68
+ const name = nameMatch[1].trim().replace(/^['"]|['"]$/g, '');
69
+ const weightMatch = body.match(/font-weight\s*:\s*([^;]+);/i);
70
+ const styleMatch = body.match(/font-style\s*:\s*([^;]+);/i);
71
+
72
+ const entry = byName.get(name) ?? { weights: new Set<number>(), italics: false };
73
+ if (weightMatch) {
74
+ const val = weightMatch[1].trim();
75
+ const rangeParts = val.split(/\s+/).map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
76
+ if (rangeParts.length === 2) {
77
+ const [lo, hi] = rangeParts;
78
+ for (let w = lo; w <= hi; w += 100) entry.weights.add(w);
79
+ } else if (rangeParts.length === 1) {
80
+ entry.weights.add(rangeParts[0]);
81
+ }
82
+ }
83
+ if (styleMatch && /italic|oblique/i.test(styleMatch[1])) entry.italics = true;
84
+ byName.set(name, entry);
85
+ }
86
+ return [...byName.entries()].map(([name, e]) => ({
87
+ name,
88
+ ...(e.weights.size > 0 ? { weights: [...e.weights].sort((a, b) => a - b) } : {}),
89
+ ...(e.italics ? { italics: true } : {}),
90
+ }));
91
+ }
92
+
93
+ /**
94
+ * Fetch a CSS URL and extract its @font-face families. Used for Typekit kits
95
+ * and generic CSS URLs whose families aren't encoded in the URL itself.
96
+ * Returns null if the fetch fails (CORS or network).
97
+ */
98
+ export async function fetchAndParseCss(url: string): Promise<ParsedFamily[] | null> {
99
+ try {
100
+ const res = await fetch(url, { method: 'GET' });
101
+ if (!res.ok) return null;
102
+ const text = await res.text();
103
+ const families = parseFontFaceText(text);
104
+ return families.length > 0 ? families : null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ export function guessKindFromUrl(url: string): FontSourceKind | null {
111
+ try {
112
+ const host = new URL(url).hostname;
113
+ if (host.includes('fonts.googleapis.com')) return 'google';
114
+ if (host.includes('use.typekit.net') || host.includes('typekit.com')) return 'typekit';
115
+ return 'css-url';
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ export function familiesToFontFamilies(sourceId: string, parsed: ParsedFamily[]): FontFamily[] {
122
+ return parsed.map((p) => ({
123
+ id: `${sourceId}:${p.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`,
124
+ name: p.name,
125
+ cssName: `"${p.name}"`,
126
+ ...(p.weights ? { weights: p.weights } : {}),
127
+ ...(p.italics ? { italics: p.italics } : {}),
128
+ }));
129
+ }
130
+
131
+ /**
132
+ * Given a URL, return a best-effort parsed set of families. For Google URLs,
133
+ * families are derivable from the querystring directly. For other URLs we
134
+ * attempt to fetch-and-parse the CSS. `null` means "unknown — ask the user
135
+ * to name the families manually."
136
+ */
137
+ export async function discoverFamiliesFromUrl(url: string): Promise<ParsedFamily[] | null> {
138
+ const kind = guessKindFromUrl(url);
139
+ if (!kind) return null;
140
+ if (kind === 'google') {
141
+ const direct = parseGoogleFontsUrl(url);
142
+ if (direct && direct.length > 0) return direct;
143
+ }
144
+ return fetchAndParseCss(url);
145
+ }
146
+
147
+ export function buildSourceFromUrl(url: string, pickedFamilies: ParsedFamily[]): FontSource {
148
+ const kind = guessKindFromUrl(url) ?? 'css-url';
149
+ const id = `src_${kind}_${Math.random().toString(36).slice(2, 10)}`;
150
+ return {
151
+ id,
152
+ kind,
153
+ url,
154
+ label: kind === 'google' ? 'Google Fonts' : kind === 'typekit' ? 'Adobe Typekit' : 'CSS URL',
155
+ families: familiesToFontFamilies(id, pickedFamilies),
156
+ };
157
+ }
158
+
159
+ export function buildSourceFromFontFaceText(css: string, pickedFamilies: ParsedFamily[]): FontSource {
160
+ const id = `src_fontface_${Math.random().toString(36).slice(2, 10)}`;
161
+ return {
162
+ id,
163
+ kind: 'font-face',
164
+ cssText: css,
165
+ label: 'Local @font-face',
166
+ families: familiesToFontFamilies(id, pickedFamilies),
167
+ };
168
+ }
package/src/lib/index.ts CHANGED
@@ -1,50 +1,68 @@
1
1
  export { default as LiveEditorOverlay } from './LiveEditorOverlay.svelte';
2
- export type { NavLink } from './LiveEditorOverlay.svelte';
2
+ export type { NavLink } from './navLinkTypes';
3
3
  export { default as ColumnsOverlay } from './ColumnsOverlay.svelte';
4
4
 
5
- export { columnsVisible, toggleColumns } from './columnsOverlay';
5
+ export { columnsVisible, toggleColumns, init as initColumnsOverlay } from './columnsOverlay';
6
6
  export { configureEditor, storageKey } from './editorConfig';
7
- export {
8
- editorConfigs,
9
- loadedConfigs,
10
- configsLoadedFromFile,
11
- activeFileName,
12
- } from './editorConfigStore';
7
+ export { activeFileName } from './editorConfigStore';
8
+ export { init as initRouter, route, navigate } from './router';
9
+ export { init as initCssVarSync } from './cssVarSync';
10
+ export { init as initEditorStore } from './editorStore';
13
11
 
14
- export {
15
- setCssVar,
16
- removeCssVar,
17
- applyCssVariables,
18
- clearAllCssVarOverrides,
19
- scrapeCssVariables,
20
- } from './cssVarSync';
12
+ export { setCssVar, removeCssVar } from './cssVarSync';
21
13
 
22
14
  export {
23
- listTokenFiles,
24
- loadTokenFile,
25
- saveTokenFile,
26
- deleteTokenFile,
27
- getActiveTokens,
15
+ listThemes,
16
+ loadTheme,
17
+ saveTheme,
18
+ deleteTheme,
19
+ getActiveTheme,
28
20
  setActiveFile,
29
21
  getProductionInfo,
30
22
  setProductionFile,
31
- listBackups,
32
- getBackupContent,
33
- restoreBackup,
34
- getCurrentCss,
35
23
  sanitizeFileName,
36
- } from './tokenService';
37
- export type { ProductionInfo, BackupEntry } from './tokenService';
24
+ } from './themeService';
25
+ export type { ProductionInfo } from './themeService';
38
26
 
39
27
  export type {
40
28
  PaletteConfig,
41
- TokenFile,
42
- TokenFileMeta,
29
+ Theme,
30
+ ThemeMeta,
43
31
  GradientStyle,
44
32
  GradientStop,
45
- } from './tokenTypes';
33
+ FontSource,
34
+ FontSourceKind,
35
+ FontFamily,
36
+ FontStack,
37
+ FontStackSlot,
38
+ FontStackVariable,
39
+ SystemCascadePreset,
40
+ GenericFamily,
41
+ Preset,
42
+ PresetMeta,
43
+ } from './themeTypes';
44
+
45
+ export {
46
+ listPresets,
47
+ loadPreset,
48
+ savePreset,
49
+ deletePreset,
50
+ getActivePreset,
51
+ setActivePreset,
52
+ applyPreset,
53
+ captureCurrentAsPreset,
54
+ } from './presetService';
55
+ export type { ApplyPresetResult } from './presetService';
56
+
57
+ export {
58
+ applyFontSources,
59
+ applyFontStacks,
60
+ resolveFontStackValues,
61
+ SYSTEM_CASCADES,
62
+ } from './fontLoader';
63
+ export { migrateThemeFonts, defaultFontSources, defaultFontStacks } from './fontMigration';
46
64
 
47
65
  export { hexToOklch, oklchToHex, gamutClamp } from './oklch';
48
66
  export type { Oklch } from './oklch';
49
67
 
50
- export { initializeTokens } from './tokenInit';
68
+ export { initializeTheme } from './themeInit';
@@ -0,0 +1,54 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
3
+ import { get } from 'svelte/store';
4
+ import { configureEditor, storageKey } from './editorConfig';
5
+ import { route, navigate, init as initRouter } from './router';
6
+
7
+ describe('M8 — lazy storage prefix resolution', () => {
8
+ it('storageKey() reflects the prefix at call time, not at module-load time', () => {
9
+ // The default prefix is `lt-`. configureEditor mutates it; subsequent
10
+ // storageKey() calls must observe the new prefix.
11
+ expect(storageKey('editor-state')).toBe('lt-editor-state');
12
+
13
+ configureEditor({ storagePrefix: 'test-' });
14
+ expect(storageKey('editor-state')).toBe('test-editor-state');
15
+
16
+ // Reset for any later tests in the file.
17
+ configureEditor({ storagePrefix: 'lt-' });
18
+ expect(storageKey('editor-state')).toBe('lt-editor-state');
19
+ });
20
+
21
+ it('editorStore persistNow uses the configured prefix at write time', async () => {
22
+ // Reconfigure BEFORE importing editorStore — but since these tests share
23
+ // module state with other suites, we can only verify that getPersistKey
24
+ // behaviour is lazy via the storageKey contract above. The functional
25
+ // round-trip is covered by manual library-import tests.
26
+ configureEditor({ storagePrefix: 'lazy-' });
27
+ expect(storageKey('editor-state')).toBe('lazy-editor-state');
28
+ configureEditor({ storagePrefix: 'lt-' });
29
+ });
30
+ });
31
+
32
+ describe('m9 — navigate() emits exactly one route store update', () => {
33
+ beforeEach(() => {
34
+ // Pin a stable starting route. Router.init() is idempotent; calling it
35
+ // here is safe even if some earlier test already initialised it.
36
+ initRouter();
37
+ // Reset history pushState side effects to a clean state by overwriting
38
+ // the current route to '/'.
39
+ history.replaceState(null, '', '/');
40
+ route.set('/');
41
+ });
42
+
43
+ it('produces a single store write per navigate() call', () => {
44
+ const sub = vi.fn();
45
+ const unsub = route.subscribe(sub);
46
+ sub.mockClear(); // drop the immediate-emit on subscribe
47
+
48
+ navigate('/foo');
49
+ expect(sub).toHaveBeenCalledTimes(1);
50
+ expect(get(route)).toBe('/foo');
51
+
52
+ unsub();
53
+ });
54
+ });
@@ -0,0 +1,64 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * Component-config migration (2026-04-24): unabbreviate component prefixes
5
+ * and apply the per-component suffix rewrites that landed alongside the
6
+ * prefix rename.
7
+ *
8
+ * Prefix layer: `--segment-*` → `--segmentedcontrol-*`, etc. — undoes the
9
+ * abbreviated namespaces that were used briefly before we standardised on
10
+ * full component names.
11
+ *
12
+ * Suffix layer: progressbar's `-track-bg` → `-track-surface`, and
13
+ * inlineeditactions' `-bg-hover` / `-bg` → `-hover-surface` / `-surface`.
14
+ *
15
+ * Whole-key rename: detailnav's only abbreviated token (`--detail-nav-bg`
16
+ * → `--detailnav-surface`) is handled inline.
17
+ */
18
+ const COMPONENT_PREFIX_RENAMES: Record<string, string> = {
19
+ '--segment-': '--segmentedcontrol-',
20
+ '--collapsible-': '--collapsiblesection-',
21
+ '--progress-': '--progressbar-',
22
+ '--section-divider-': '--sectiondivider-',
23
+ '--radio-': '--radiobutton-',
24
+ '--inline-edit-': '--inlineeditactions-',
25
+ };
26
+
27
+ const COMPONENT_SUFFIX_RENAMES: Record<string, Array<[string, string]>> = {
28
+ progressbar: [['-track-bg', '-track-surface']],
29
+ inlineeditactions: [
30
+ ['-bg-hover', '-hover-surface'],
31
+ ['-bg', '-surface'],
32
+ ],
33
+ };
34
+
35
+ export const componentMigration_2026_04_24_prefixAndSuffixRenames: Migration = {
36
+ id: '2026-04-24-component-prefix-and-suffix-renames',
37
+ fromVersion: 0,
38
+ toVersion: 1,
39
+ appliesTo: 'component-config',
40
+ apply(rawVars, meta) {
41
+ const component = meta.component ?? '';
42
+ const out: Record<string, string> = {};
43
+ const suffixRules = COMPONENT_SUFFIX_RENAMES[component] ?? [];
44
+ for (const [oldKey, value] of Object.entries(rawVars)) {
45
+ let key = oldKey;
46
+ // One-off whole-key rename (detailnav's only abbreviated token).
47
+ if (key === '--detail-nav-bg') key = '--detailnav-surface';
48
+ for (const [oldPrefix, newPrefix] of Object.entries(COMPONENT_PREFIX_RENAMES)) {
49
+ if (key.startsWith(oldPrefix)) {
50
+ key = newPrefix + key.slice(oldPrefix.length);
51
+ break;
52
+ }
53
+ }
54
+ for (const [oldSuffix, newSuffix] of suffixRules) {
55
+ if (key.endsWith(oldSuffix)) {
56
+ key = key.slice(0, -oldSuffix.length) + newSuffix;
57
+ break;
58
+ }
59
+ }
60
+ if (!(key in out)) out[key] = value;
61
+ }
62
+ return out;
63
+ },
64
+ };