@motion-proto/live-tokens 0.6.2 → 0.8.0

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 (232) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +854 -226
  3. package/dist-plugin/index.d.cts +2 -1
  4. package/dist-plugin/index.d.ts +2 -1
  5. package/dist-plugin/index.js +852 -225
  6. package/package.json +26 -40
  7. package/src/{styles → app}/site.css +1 -1
  8. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  9. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  10. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  11. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +37 -30
  12. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  13. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  14. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  15. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  16. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  17. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +67 -38
  18. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  19. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  20. package/src/editor/component-editor/SectionDividerEditor.svelte +565 -0
  21. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  22. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +29 -21
  23. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  24. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  25. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  26. package/src/editor/component-editor/editors.d.ts +10 -0
  27. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  28. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +54 -15
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +151 -424
  31. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  32. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  33. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  34. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  35. package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
  36. package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -12
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  39. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  40. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +85 -0
  41. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  42. package/src/editor/component-editor/scaffolding/StateBlock.svelte +345 -0
  43. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +17 -12
  44. package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +13 -1
  45. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +858 -0
  46. package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +1 -0
  47. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  48. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  49. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +25 -0
  50. package/src/{lib → editor/core/components}/componentConfigKeys.ts +8 -0
  51. package/src/{lib → editor/core/components}/componentConfigService.ts +3 -3
  52. package/src/{lib → editor/core/components}/componentPersist.ts +11 -9
  53. package/src/editor/core/flashStatus.ts +30 -0
  54. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  55. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  56. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  57. package/src/editor/core/manifests/manifestService.ts +171 -0
  58. package/src/editor/core/palettes/familySwap.ts +99 -0
  59. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +71 -2
  60. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +9 -6
  61. package/src/editor/core/productionPulse.ts +37 -0
  62. package/src/{lib → editor/core/routing}/router.ts +1 -1
  63. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  64. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  65. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  66. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  67. package/src/{lib → editor/core/store}/editorStore.ts +222 -28
  68. package/src/{lib → editor/core/store}/editorTypes.ts +56 -13
  69. package/src/editor/core/store/gradientSource.ts +192 -0
  70. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  71. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  72. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  73. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  74. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  75. package/src/{lib → editor/core/themes}/migrations/index.ts +10 -0
  76. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  77. package/src/{lib → editor/core/themes}/slices/components.ts +20 -6
  78. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  79. package/src/{lib → editor/core/themes}/slices/gradients.ts +89 -14
  80. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  81. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  82. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  83. package/src/{lib → editor/core/themes}/themeInit.ts +8 -8
  84. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  85. package/src/{lib → editor/core/themes}/themeTypes.ts +67 -8
  86. package/src/editor/index.ts +69 -0
  87. package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -1
  88. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +80 -129
  89. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  90. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  91. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  92. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  93. package/src/{styles → editor/styles}/ui-editor.css +43 -22
  94. package/src/{styles → editor/styles}/ui-form-controls.css +23 -24
  95. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +119 -68
  96. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  97. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +7 -6
  98. package/src/editor/ui/FileLoadList.svelte +367 -0
  99. package/src/editor/ui/FilePill.svelte +80 -0
  100. package/src/editor/ui/FontStackEditor.svelte +499 -0
  101. package/src/editor/ui/GradientEditor.svelte +690 -0
  102. package/src/{ui → editor/ui}/GradientStopPicker.svelte +12 -4
  103. package/src/editor/ui/ManifestFileManager.svelte +438 -0
  104. package/src/{ui → editor/ui}/PaletteEditor.svelte +180 -673
  105. package/src/editor/ui/ProjectFontsSection.svelte +638 -0
  106. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  107. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  108. package/src/editor/ui/ThemeFileManager.svelte +783 -0
  109. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  110. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -7
  111. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +4 -1
  112. package/src/editor/ui/UIInfoPopover.svelte +243 -0
  113. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  114. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  115. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  116. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  117. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +57 -30
  118. package/src/editor/ui/UIPillButton.svelte +168 -0
  119. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  120. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  121. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  122. package/src/editor/ui/UISquareButton.svelte +172 -0
  123. package/src/{ui → editor/ui}/UITokenSelector.svelte +14 -11
  124. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  125. package/src/{ui → editor/ui}/VariablesTab.svelte +46 -17
  126. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  127. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +24 -47
  128. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +11 -8
  129. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  130. package/src/editor/ui/palette/paletteMath.ts +275 -0
  131. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -18
  132. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +8 -8
  133. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +18 -18
  134. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +23 -23
  135. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  136. package/src/{components → system/components}/Badge.svelte +0 -36
  137. package/src/{components → system/components}/Button.svelte +2 -2
  138. package/src/{components → system/components}/Card.svelte +34 -60
  139. package/src/{components → system/components}/CollapsibleSection.svelte +25 -2
  140. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  141. package/src/{components → system/components}/Dialog.svelte +1 -1
  142. package/src/system/components/FloatingTokenTags.css +275 -0
  143. package/src/system/components/FloatingTokenTags.svelte +543 -0
  144. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  145. package/src/system/components/MenuSelect.svelte +229 -0
  146. package/src/{components → system/components}/Notification.svelte +8 -1
  147. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  148. package/src/system/components/SectionDivider.svelte +560 -0
  149. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  150. package/src/{components → system/components}/TabBar.svelte +81 -65
  151. package/src/{components → system/components}/Table.svelte +17 -3
  152. package/src/{components → system/components}/Tooltip.svelte +6 -4
  153. package/src/system/styles/CONVENTIONS.md +178 -0
  154. package/src/system/styles/fonts.css +20 -0
  155. package/src/system/styles/tokens.css +601 -0
  156. package/src/system/styles/tokens.generated.css +544 -0
  157. package/src/component-editor/ImageEditor.svelte +0 -74
  158. package/src/component-editor/SectionDividerEditor.svelte +0 -265
  159. package/src/component-editor/scaffolding/DividerEditor.svelte +0 -94
  160. package/src/component-editor/scaffolding/GradientCard.svelte +0 -296
  161. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  162. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  163. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  164. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  165. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  166. package/src/components/SectionDivider.svelte +0 -483
  167. package/src/data/google-fonts.json +0 -75
  168. package/src/lib/index.ts +0 -68
  169. package/src/lib/presetService.ts +0 -214
  170. package/src/lib/productionPulse.ts +0 -32
  171. package/src/styles/fonts.css +0 -30
  172. package/src/styles/tokens.css +0 -1324
  173. package/src/ui/FontStackEditor.svelte +0 -361
  174. package/src/ui/GradientEditor.svelte +0 -470
  175. package/src/ui/PresetFileManager.svelte +0 -1116
  176. package/src/ui/ProjectFontsSection.svelte +0 -645
  177. package/src/ui/ThemeFileManager.svelte +0 -1020
  178. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  179. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  180. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  181. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  182. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  183. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  184. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  185. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  186. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  187. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  188. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  189. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  190. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  191. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  192. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  193. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  194. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  195. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  196. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  197. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  198. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  199. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  200. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  201. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  202. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  203. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  204. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  205. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  206. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  207. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  208. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  209. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  210. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  211. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  212. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  213. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  214. /package/src/{ui → editor/ui}/index.ts +0 -0
  215. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  216. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  217. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  218. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  219. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  220. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  221. /package/src/{assets → system/assets}/offering.webp +0 -0
  222. /package/src/{components → system/components}/Callout.svelte +0 -0
  223. /package/src/{components → system/components}/Image.svelte +0 -0
  224. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  225. /package/src/{components → system/components}/types.ts +0 -0
  226. /package/src/{styles → system/styles}/_padding.scss +0 -0
  227. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  228. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  229. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  230. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  231. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  232. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -0,0 +1,638 @@
1
+ <script lang="ts">
2
+ import type { FontFamily, FontSource } from '../core/themes/themeTypes';
3
+ import { editorState, setFontSources, transaction } from '../core/store/editorStore';
4
+ import { applyFontSources, applyFontStacks } from '../core/fonts/fontLoader';
5
+ import {
6
+ buildSourceFromFontFaceText,
7
+ buildSourceFromUrl,
8
+ discoverFamiliesFromUrl,
9
+ parseFontFaceText,
10
+ type ParsedFamily,
11
+ } from '../core/fonts/fontParse';
12
+ import UIPillButton from './UIPillButton.svelte';
13
+ import UISegmentedControl from './UISegmentedControl.svelte';
14
+ import UIInfoPopover from './UIInfoPopover.svelte';
15
+
16
+ type AddMode = 'closed' | 'name' | 'paste';
17
+ let addMode: AddMode = $state('closed');
18
+
19
+ // By-name (Google Fonts) — type a family name; we build the CSS2 URL.
20
+ let nameInput = $state('');
21
+ let nameError = $state('');
22
+ let nameDiscovering = $state(false);
23
+ let nameParsed: ParsedFamily[] | null = $state(null);
24
+
25
+ // Unified paste — accepts a bare URL, `<link>` tag, `@import url(...)`,
26
+ // or one or more `@font-face { ... }` rules. We sniff which on Detect.
27
+ let pasteInput = $state('');
28
+ let pasteError = $state('');
29
+ let pasteDiscovering = $state(false);
30
+ let urlParsed: ParsedFamily[] | null = $state(null);
31
+ let urlPickedNames = $state(new Set<string>());
32
+ let urlNeedsManualFamilies = $state(false);
33
+ let urlManualFamilies = $state('');
34
+ let fontFaceParsed: ParsedFamily[] = $state([]);
35
+
36
+ function reset() {
37
+ addMode = 'closed';
38
+ nameInput = '';
39
+ nameError = '';
40
+ nameDiscovering = false;
41
+ nameParsed = null;
42
+ pasteInput = '';
43
+ pasteError = '';
44
+ pasteDiscovering = false;
45
+ urlParsed = null;
46
+ urlPickedNames = new Set();
47
+ urlNeedsManualFamilies = false;
48
+ urlManualFamilies = '';
49
+ fontFaceParsed = [];
50
+ }
51
+
52
+ // Pull a fonts URL out of whatever the user pastes: bare URL, link tag,
53
+ // @import url(...), or a full style-tag wrapper around an @import.
54
+ function extractFontsUrl(raw: string): string | null {
55
+ const text = raw.trim();
56
+ if (!text) return null;
57
+ const href = text.match(/href\s*=\s*["']([^"']+)["']/i);
58
+ if (href) return href[1];
59
+ const importUrl = text.match(/@import\s+url\(\s*['"]?([^'")]+)['"]?\s*\)/i);
60
+ if (importUrl) return importUrl[1];
61
+ const importBare = text.match(/@import\s+['"]([^'"]+)['"]/i);
62
+ if (importBare) return importBare[1];
63
+ if (/^https?:\/\//i.test(text)) return text;
64
+ return null;
65
+ }
66
+
67
+ let fontSourcesList = $derived($editorState.fonts.sources);
68
+ let fontStacksList = $derived($editorState.fonts.stacks);
69
+
70
+ function commitSources(next: FontSource[]) {
71
+ setFontSources(next);
72
+ applyFontSources(next);
73
+ applyFontStacks(fontStacksList, next);
74
+ }
75
+
76
+ /** One paste field → sniff whether it's @font-face or a URL/embed and
77
+ * populate the matching detected-families state. */
78
+ async function detectPaste() {
79
+ pasteError = '';
80
+ urlParsed = null;
81
+ urlNeedsManualFamilies = false;
82
+ fontFaceParsed = [];
83
+ const text = pasteInput.trim();
84
+ if (!text) {
85
+ pasteError = 'Paste a URL, embed, or @font-face rule';
86
+ return;
87
+ }
88
+ if (/@font-face/i.test(text)) {
89
+ fontFaceParsed = parseFontFaceText(text);
90
+ if (fontFaceParsed.length === 0) {
91
+ pasteError = "Couldn't parse @font-face rules";
92
+ }
93
+ return;
94
+ }
95
+ const url = extractFontsUrl(text);
96
+ if (!url) {
97
+ pasteError = "Couldn't find a fonts URL or @font-face rule in that paste";
98
+ return;
99
+ }
100
+ pasteDiscovering = true;
101
+ try {
102
+ const found = await discoverFamiliesFromUrl(url);
103
+ if (found && found.length > 0) {
104
+ urlParsed = found;
105
+ urlPickedNames = new Set(found.map((f) => f.name));
106
+ } else {
107
+ urlNeedsManualFamilies = true;
108
+ }
109
+ } catch (e) {
110
+ pasteError = 'Discovery failed';
111
+ urlNeedsManualFamilies = true;
112
+ }
113
+ pasteDiscovering = false;
114
+ }
115
+
116
+ /** Build a Google Fonts CSS2 URL for a family name, requesting a wide
117
+ * weight range with italics. Works for variable fonts and most static
118
+ * multi-weight families. Single-weight static fonts (e.g. GFS Didot) will
119
+ * reject the range axis with 400 Bad Request — Chrome then CORBs the
120
+ * response. Such fonts must be persisted as `?family=Name&display=swap`. */
121
+ function googleUrlForName(name: string): string {
122
+ const family = name.trim().replace(/\s+/g, '+');
123
+ return `https://fonts.googleapis.com/css2?family=${family}:ital,wght@0,100..900;1,100..900&display=swap`;
124
+ }
125
+
126
+ async function discoverByName() {
127
+ nameError = '';
128
+ nameParsed = null;
129
+ if (!nameInput.trim()) {
130
+ nameError = 'Enter a family name';
131
+ return;
132
+ }
133
+ nameDiscovering = true;
134
+ try {
135
+ const found = await discoverFamiliesFromUrl(googleUrlForName(nameInput));
136
+ if (found && found.length > 0) {
137
+ nameParsed = found;
138
+ } else {
139
+ nameError = `Couldn't find "${nameInput.trim()}" on Google Fonts`;
140
+ }
141
+ } catch {
142
+ nameError = `Couldn't reach Google Fonts for "${nameInput.trim()}"`;
143
+ }
144
+ nameDiscovering = false;
145
+ }
146
+
147
+ function addNameSource() {
148
+ if (!nameParsed || nameParsed.length === 0) return;
149
+ if (nameDuplicate) return;
150
+ // $state.snapshot() unwraps the reactive proxy. Without it the FontSource
151
+ // we hand to the store carries proxy arrays (weights, etc.) and the next
152
+ // `mutate()` call fails with DataCloneError inside structuredClone.
153
+ const families = $state.snapshot(nameParsed) as ParsedFamily[];
154
+ const source = buildSourceFromUrl(googleUrlForName(nameInput), families);
155
+ commitSources([...fontSourcesList, source]);
156
+ reset();
157
+ }
158
+
159
+ /** Case-insensitive family-name match against existing sources. Used to
160
+ * block duplicate adds and surface a notice under the Add button. */
161
+ function findExistingFamilyByName(name: string): string | null {
162
+ const lower = name.trim().toLowerCase();
163
+ if (!lower) return null;
164
+ for (const src of fontSourcesList) {
165
+ for (const fam of src.families) {
166
+ if (fam.name.toLowerCase() === lower) return fam.name;
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+
172
+ let nameDuplicate = $derived.by(() => {
173
+ if (!nameParsed || nameParsed.length === 0) return null;
174
+ return findExistingFamilyByName(nameParsed[0].name);
175
+ });
176
+
177
+ function addUrlSource() {
178
+ const url = extractFontsUrl(pasteInput);
179
+ if (!url) {
180
+ pasteError = "Couldn't find a fonts URL in that paste";
181
+ return;
182
+ }
183
+ let families: ParsedFamily[] = [];
184
+ if (urlParsed) {
185
+ // Snapshot to drop the $state proxy — see comment in addNameSource.
186
+ families = ($state.snapshot(urlParsed) as ParsedFamily[]).filter((f) => urlPickedNames.has(f.name));
187
+ } else if (urlNeedsManualFamilies) {
188
+ families = urlManualFamilies
189
+ .split(',')
190
+ .map((s) => s.trim())
191
+ .filter(Boolean)
192
+ .map((name) => ({ name }));
193
+ }
194
+ if (families.length === 0) {
195
+ pasteError = 'Pick at least one family';
196
+ return;
197
+ }
198
+ const source = buildSourceFromUrl(url, families);
199
+ commitSources([...fontSourcesList, source]);
200
+ reset();
201
+ }
202
+
203
+ function addFontFaceSource() {
204
+ if (fontFaceParsed.length === 0) return;
205
+ const families = $state.snapshot(fontFaceParsed) as ParsedFamily[];
206
+ const source = buildSourceFromFontFaceText(pasteInput, families);
207
+ commitSources([...fontSourcesList, source]);
208
+ reset();
209
+ }
210
+
211
+ function removeFamily(sourceId: string, familyId: string) {
212
+ const next = fontSourcesList
213
+ .map((s) => (s.id === sourceId ? { ...s, families: s.families.filter((f) => f.id !== familyId) } : s))
214
+ .filter((s) => s.families.length > 0);
215
+ const updatedStacks = fontStacksList.map((stack) => ({
216
+ ...stack,
217
+ slots: stack.slots.filter((slot) => !(slot.kind === 'project' && slot.familyId === familyId)),
218
+ }));
219
+ transaction('remove font family', (s) => {
220
+ s.fonts.sources = next;
221
+ s.fonts.stacks = updatedStacks;
222
+ });
223
+ applyFontSources(next);
224
+ applyFontStacks(updatedStacks, next);
225
+ }
226
+
227
+ /** Resolve a clickable target for the row. We prefer the human-readable
228
+ * specimen/family page (Google Fonts, Adobe Fonts) over the raw CSS/font
229
+ * file — those are rarely what a user wants to look at. */
230
+ function familyHref(source: FontSource, family: FontFamily): string | null {
231
+ if (source.kind === 'google') {
232
+ return `https://fonts.google.com/specimen/${family.name.trim().replace(/\s+/g, '+')}`;
233
+ }
234
+ if (source.kind === 'typekit') {
235
+ const slug = family.name.trim().toLowerCase().replace(/\s+/g, '-');
236
+ return `https://fonts.adobe.com/fonts/${slug}`;
237
+ }
238
+ if (source.kind === 'css-url') return source.url ?? null;
239
+ return null; // font-face: no public page exists
240
+ }
241
+
242
+ function sourceKindLabel(source: FontSource): string {
243
+ if (source.kind === 'google') return 'Google';
244
+ if (source.kind === 'typekit') return 'Typekit';
245
+ if (source.kind === 'font-face') return 'Local';
246
+ return 'CSS URL';
247
+ }
248
+ </script>
249
+
250
+ <section class="project-fonts">
251
+ <header class="pf-header">
252
+ <div class="pf-title-row">
253
+ <h3 class="group-title">Project Fonts</h3>
254
+ <UIInfoPopover title="Installing fonts" ariaLabel="How to install fonts">
255
+ <p>Three ways to install a font, depending on where it lives:</p>
256
+ <p>
257
+ <strong>Google Fonts</strong> — use <em>By name</em> and type the family
258
+ (e.g. <code>Inter</code>). We fetch the CSS2 URL for you.
259
+ </p>
260
+ <p>
261
+ <strong>Hosted CDN (Adobe, Fontshare, custom)</strong> — use <em>Paste</em>
262
+ with a fonts URL, a <code>&lt;link&gt;</code> tag, or an <code>@import url(...)</code> line.
263
+ </p>
264
+ <p>
265
+ <strong>Local files</strong> — drop your <code>.woff2</code> files into
266
+ <code>src/system/styles/fonts/&lt;Family&gt;/</code>, then paste the matching
267
+ <code>@font-face &#123; ... &#125;</code> rules into <em>Paste</em>. The folder ships
268
+ with the production build, so <code>src/...</code> paths resolve at runtime.
269
+ </p>
270
+ </UIInfoPopover>
271
+ </div>
272
+ <UIPillButton
273
+ variant="primary"
274
+ icon="fa-plus"
275
+ onclick={() => { addMode = addMode === 'closed' ? 'name' : 'closed'; }}
276
+ >Add Font</UIPillButton>
277
+ </header>
278
+
279
+ {#if fontSourcesList.length === 0}
280
+ <p class="pf-empty">No fonts loaded yet. Use the add button below.</p>
281
+ {/if}
282
+
283
+ <ul class="pf-family-list">
284
+ {#each fontSourcesList as source (source.id)}
285
+ {#each source.families as fam (fam.id)}
286
+ {@const href = familyHref(source, fam)}
287
+ <li class="pf-family">
288
+ <span class="pf-family-preview" style="font-family: {fam.cssName}, sans-serif;">Ag</span>
289
+ <span class="pf-family-name">{fam.name}</span>
290
+ <UIPillButton
291
+ variant="outline"
292
+ size="compact"
293
+ href={href ?? undefined}
294
+ target={href ? '_blank' : undefined}
295
+ disabled={!href}
296
+ title={href ? `Open ${fam.name} on ${sourceKindLabel(source)}` : 'No public page for local fonts'}
297
+ >{sourceKindLabel(source)}</UIPillButton>
298
+ <button
299
+ type="button"
300
+ class="pf-family-remove"
301
+ onclick={() => removeFamily(source.id, fam.id)}
302
+ aria-label={`Remove ${fam.name}`}
303
+ title="Remove family"
304
+ ><i class="fas fa-xmark" aria-hidden="true"></i></button>
305
+ </li>
306
+ {/each}
307
+ {/each}
308
+ </ul>
309
+
310
+ {#if addMode !== 'closed'}
311
+ <div class="pf-add-panel">
312
+ <div class="pf-add-head">
313
+ <span class="pf-add-eyebrow">Browse</span>
314
+ <div class="pf-browse-row">
315
+ <UIPillButton variant="outline" href="https://fonts.google.com/" target="_blank" icon="fa-arrow-up-right-from-square">
316
+ Google Fonts
317
+ </UIPillButton>
318
+ <UIPillButton variant="outline" href="https://fonts.adobe.com/" target="_blank" icon="fa-arrow-up-right-from-square">
319
+ Adobe Fonts
320
+ </UIPillButton>
321
+ <UIPillButton variant="outline" href="https://www.fontshare.com/" target="_blank" icon="fa-arrow-up-right-from-square">
322
+ Fontshare
323
+ </UIPillButton>
324
+ </div>
325
+ <button type="button" class="pf-add-close" onclick={reset} aria-label="Cancel">×</button>
326
+ </div>
327
+
328
+ <div class="pf-add-divider"><span>or add directly</span></div>
329
+
330
+ <UISegmentedControl
331
+ value={addMode}
332
+ options={[
333
+ { value: 'name', label: 'By name (Google)' },
334
+ { value: 'paste', label: 'Paste URL or @font-face' },
335
+ ] as const}
336
+ ariaLabel="Add font by"
337
+ onchange={(v) => (addMode = v)}
338
+ />
339
+
340
+ {#if addMode === 'name'}
341
+ <div class="pf-row">
342
+ <input
343
+ type="text"
344
+ class="ui-form-input pf-name-input"
345
+ placeholder="e.g. Inter, Fraunces, Space Mono"
346
+ bind:value={nameInput}
347
+ onkeydown={(e) => { if (e.key === 'Enter' && !nameParsed) discoverByName(); }}
348
+ />
349
+ {#if nameParsed}
350
+ <UIPillButton variant="primary" onclick={addNameSource} disabled={!!nameDuplicate}>Add</UIPillButton>
351
+ {:else}
352
+ <UIPillButton variant="secondary" onclick={discoverByName} disabled={!nameInput.trim() || nameDiscovering}>
353
+ {nameDiscovering ? 'Checking…' : 'Find'}
354
+ </UIPillButton>
355
+ {/if}
356
+ </div>
357
+ {#if nameError}<div class="pf-error">{nameError}</div>{/if}
358
+ {#if nameParsed}
359
+ {#if nameDuplicate}
360
+ <div class="pf-notice">
361
+ <strong>{nameDuplicate}</strong> is already in your project fonts.
362
+ </div>
363
+ {:else}
364
+ <div class="pf-detected">
365
+ Found <strong>{nameParsed[0].name}</strong>
366
+ {#if nameParsed[0].weights && nameParsed[0].weights.length > 0}
367
+ <span class="pf-check-meta">({nameParsed[0].weights.length} weights)</span>
368
+ {/if}
369
+ </div>
370
+ {/if}
371
+ {/if}
372
+ {:else if addMode === 'paste'}
373
+ <textarea
374
+ class="ui-form-input pf-textarea pf-url-input"
375
+ placeholder={'A fonts URL, <link> tag, or @import url(...)\n\nor\n\none or more @font-face { ... } rules'}
376
+ rows="5"
377
+ bind:value={pasteInput}
378
+ ></textarea>
379
+ <div class="pf-row">
380
+ <UIPillButton variant="secondary" onclick={detectPaste} disabled={!pasteInput.trim() || pasteDiscovering}>
381
+ {pasteDiscovering ? 'Checking…' : 'Detect'}
382
+ </UIPillButton>
383
+ </div>
384
+ {#if pasteError}<div class="pf-error">{pasteError}</div>{/if}
385
+ {#if fontFaceParsed.length > 0}
386
+ <div class="pf-detected">Detected @font-face: {fontFaceParsed.map((f) => f.name).join(', ')}</div>
387
+ <UIPillButton variant="primary" onclick={addFontFaceSource}>Add</UIPillButton>
388
+ {:else if urlParsed}
389
+ <div class="pf-detected">Detected families — pick which to add:</div>
390
+ <ul class="pf-checklist">
391
+ {#each urlParsed as f (f.name)}
392
+ <li>
393
+ <label>
394
+ <input
395
+ type="checkbox"
396
+ checked={urlPickedNames.has(f.name)}
397
+ onchange={(e) => {
398
+ const target = e.currentTarget;
399
+ const s = new Set(urlPickedNames);
400
+ if (target.checked) s.add(f.name); else s.delete(f.name);
401
+ urlPickedNames = s;
402
+ }}
403
+ />
404
+ <span class="pf-check-name">{f.name}</span>
405
+ {#if f.weights && f.weights.length > 0}
406
+ <span class="pf-check-meta">{f.weights.length}w</span>
407
+ {/if}
408
+ </label>
409
+ </li>
410
+ {/each}
411
+ </ul>
412
+ <UIPillButton variant="primary" onclick={addUrlSource}>Add {urlPickedNames.size} selected</UIPillButton>
413
+ {:else if urlNeedsManualFamilies}
414
+ <div class="pf-detected">Couldn't auto-detect families (CORS or no metadata). Name them:</div>
415
+ <input
416
+ type="text"
417
+ class="ui-form-input"
418
+ placeholder="Comma-separated family names"
419
+ bind:value={urlManualFamilies}
420
+ />
421
+ <UIPillButton variant="primary" onclick={addUrlSource} disabled={!urlManualFamilies.trim()}>Add</UIPillButton>
422
+ {/if}
423
+ {/if}
424
+ </div>
425
+ {/if}
426
+ </section>
427
+
428
+ <style>
429
+ .project-fonts {
430
+ display: flex;
431
+ flex-direction: column;
432
+ gap: var(--ui-space-12);
433
+ }
434
+
435
+ .pf-header {
436
+ display: flex;
437
+ align-items: baseline;
438
+ gap: var(--ui-space-12);
439
+ justify-content: space-between;
440
+ }
441
+
442
+ .pf-title-row {
443
+ display: flex;
444
+ align-items: center;
445
+ gap: var(--ui-space-4);
446
+ }
447
+
448
+ .group-title {
449
+ margin: 0;
450
+ font-size: var(--ui-font-size-xl);
451
+ font-weight: var(--ui-font-weight-bold);
452
+ color: var(--ui-text-primary);
453
+ }
454
+
455
+ .pf-empty {
456
+ margin: 0;
457
+ padding: var(--ui-space-8) 0;
458
+ color: var(--ui-text-muted);
459
+ font-size: var(--ui-font-size-sm);
460
+ }
461
+
462
+ .pf-family-list {
463
+ list-style: none;
464
+ margin: 0;
465
+ padding: 0;
466
+ display: grid;
467
+ grid-template-columns: repeat(auto-fill, minmax(min(22rem, 100%), 1fr));
468
+ gap: var(--ui-space-6);
469
+ align-items: start;
470
+ }
471
+
472
+ .pf-family {
473
+ display: grid;
474
+ grid-template-columns: 1.75rem 1fr auto 24px;
475
+ align-items: center;
476
+ gap: var(--ui-space-8);
477
+ min-width: 0;
478
+ padding: var(--ui-space-4) var(--ui-space-8);
479
+ border: 1px solid var(--ui-border-low);
480
+ border-radius: var(--ui-radius-md);
481
+ background: var(--ui-surface-subtle, rgba(255,255,255,0.02));
482
+ min-height: 36px;
483
+ }
484
+ .pf-family:hover {
485
+ background: var(--ui-surface-hover, rgba(255,255,255,0.04));
486
+ }
487
+
488
+ .pf-family-remove {
489
+ display: inline-flex;
490
+ align-items: center;
491
+ justify-content: center;
492
+ width: 24px;
493
+ height: 24px;
494
+ padding: 0;
495
+ background: none;
496
+ border: none;
497
+ border-radius: var(--ui-radius-sm);
498
+ color: var(--ui-text-muted);
499
+ cursor: pointer;
500
+ font-size: var(--ui-font-size-sm);
501
+ line-height: 1;
502
+ }
503
+ .pf-family-remove:hover {
504
+ color: var(--ui-text-primary);
505
+ background: var(--ui-surface-hover, rgba(255,255,255,0.06));
506
+ }
507
+
508
+ .pf-family-preview {
509
+ font-size: var(--ui-font-size-md);
510
+ color: var(--ui-text-primary);
511
+ min-width: 1.75rem;
512
+ text-align: center;
513
+ line-height: 1.2;
514
+ }
515
+
516
+ .pf-family-name {
517
+ font-size: var(--ui-font-size-sm);
518
+ color: var(--ui-text-primary);
519
+ min-width: 0;
520
+ overflow: hidden;
521
+ text-overflow: ellipsis;
522
+ white-space: nowrap;
523
+ }
524
+
525
+ .pf-add-panel {
526
+ display: flex;
527
+ flex-direction: column;
528
+ gap: var(--ui-space-10);
529
+ padding: var(--ui-space-12);
530
+ border: 1px solid rgba(255, 255, 255, 0.5);
531
+ border-radius: var(--ui-radius-md);
532
+ background: rgba(255, 255, 255, 0.15);
533
+ }
534
+
535
+ .pf-add-head {
536
+ display: grid;
537
+ grid-template-columns: auto 1fr auto;
538
+ align-items: center;
539
+ gap: var(--ui-space-12);
540
+ }
541
+ .pf-add-eyebrow {
542
+ font-size: var(--ui-font-size-xs);
543
+ font-weight: var(--ui-font-weight-semibold);
544
+ color: var(--ui-text-tertiary);
545
+ text-transform: uppercase;
546
+ letter-spacing: 0.04em;
547
+ }
548
+ .pf-browse-row {
549
+ display: flex;
550
+ flex-wrap: wrap;
551
+ gap: var(--ui-space-8);
552
+ }
553
+ .pf-add-close {
554
+ background: none;
555
+ border: none;
556
+ color: var(--ui-text-muted);
557
+ font-size: var(--ui-font-size-lg);
558
+ line-height: 1;
559
+ padding: var(--ui-space-2) var(--ui-space-6);
560
+ border-radius: var(--ui-radius-sm);
561
+ cursor: pointer;
562
+ justify-self: end;
563
+ }
564
+ .pf-add-close:hover {
565
+ color: var(--ui-text-primary);
566
+ background: var(--ui-surface-hover, rgba(255,255,255,0.06));
567
+ }
568
+
569
+ .pf-add-divider {
570
+ display: flex;
571
+ align-items: center;
572
+ gap: var(--ui-space-8);
573
+ color: var(--ui-text-tertiary);
574
+ font-size: var(--ui-font-size-xs);
575
+ text-transform: uppercase;
576
+ letter-spacing: 0.04em;
577
+ }
578
+ .pf-add-divider::before,
579
+ .pf-add-divider::after {
580
+ content: '';
581
+ flex: 1;
582
+ height: 1px;
583
+ background: var(--ui-border-low);
584
+ }
585
+
586
+ .pf-name-input {
587
+ flex: 1;
588
+ min-width: 0;
589
+ }
590
+
591
+ .pf-row {
592
+ display: flex;
593
+ gap: var(--ui-space-8);
594
+ align-items: center;
595
+ }
596
+
597
+ .pf-error { color: var(--ui-text-danger, #ff6b6b); font-size: var(--ui-font-size-sm); }
598
+
599
+ .pf-detected { color: var(--ui-text-secondary); font-size: var(--ui-font-size-sm); }
600
+
601
+ .pf-notice {
602
+ color: var(--ui-text-secondary);
603
+ font-size: var(--ui-font-size-sm);
604
+ }
605
+ .pf-notice strong { color: var(--ui-text-primary); }
606
+
607
+ .pf-checklist {
608
+ list-style: none;
609
+ margin: 0;
610
+ padding: 0;
611
+ display: flex;
612
+ flex-direction: column;
613
+ gap: var(--ui-space-2);
614
+ }
615
+ .pf-checklist label {
616
+ display: flex;
617
+ align-items: center;
618
+ gap: var(--ui-space-6);
619
+ font-size: var(--ui-font-size-sm);
620
+ cursor: pointer;
621
+ }
622
+ .pf-check-name { color: var(--ui-text-primary); }
623
+ .pf-check-meta { color: var(--ui-text-tertiary); font-family: var(--ui-font-mono); font-size: var(--ui-font-size-xs); }
624
+
625
+ .pf-textarea {
626
+ font-family: var(--ui-font-mono);
627
+ font-size: var(--ui-font-size-xs);
628
+ resize: vertical;
629
+ color: var(--ui-text-primary);
630
+ }
631
+ .pf-textarea::placeholder { color: var(--ui-text-muted); opacity: 1; }
632
+
633
+ .pf-url-input {
634
+ white-space: pre;
635
+ overflow-x: auto;
636
+ }
637
+
638
+ </style>
@@ -272,12 +272,12 @@
272
272
  }
273
273
 
274
274
  .section-title {
275
- font-size: var(--ui-font-size-lg);
275
+ font-size: var(--ui-font-size-2xl);
276
276
  font-weight: var(--ui-font-weight-semibold);
277
277
  color: var(--ui-text-primary);
278
278
  margin: 0;
279
279
  padding-bottom: var(--ui-space-8);
280
- border-bottom: 1px solid var(--ui-border-subtle);
280
+ border-bottom: 2px solid var(--ui-border-high);
281
281
  }
282
282
 
283
283
  .swatch-groups-grid {
@@ -294,7 +294,7 @@
294
294
  }
295
295
 
296
296
  .group-title {
297
- font-size: var(--ui-font-size-sm);
297
+ font-size: var(--ui-font-size-lg);
298
298
  font-weight: var(--ui-font-weight-semibold);
299
299
  color: var(--ui-text-secondary);
300
300
  margin: 0;
@@ -125,12 +125,12 @@
125
125
  }
126
126
 
127
127
  .group-title {
128
- font-size: var(--ui-font-size-sm);
128
+ font-size: var(--ui-font-size-lg);
129
129
  font-weight: var(--ui-font-weight-semibold);
130
130
  color: var(--ui-text-secondary);
131
131
  margin: 0;
132
132
  padding-bottom: var(--ui-space-4);
133
- border-bottom: 1px solid var(--ui-border-faint);
133
+ border-bottom: 1px solid var(--ui-border-low);
134
134
  }
135
135
 
136
136
  .text-colors-grid {
@@ -141,7 +141,7 @@
141
141
 
142
142
  .text-color-card {
143
143
  background: var(--ui-surface-low);
144
- border: 1px solid var(--ui-border-subtle);
144
+ border: 1px solid var(--ui-border-low);
145
145
  border-radius: var(--ui-radius-md);
146
146
  padding: var(--ui-space-12);
147
147
  display: flex;