@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,645 @@
1
+ <script lang="ts">
2
+ import type { FontSource } from '../lib/themeTypes';
3
+ import { editorState, setFontSources, transaction } from '../lib/editorStore';
4
+ import { applyFontSources, applyFontStacks } from '../lib/fontLoader';
5
+ import {
6
+ buildSourceFromFontFaceText,
7
+ buildSourceFromUrl,
8
+ discoverFamiliesFromUrl,
9
+ parseFontFaceText,
10
+ type ParsedFamily,
11
+ } from '../lib/fontParse';
12
+ import googleFontsData from '../data/google-fonts.json';
13
+
14
+ interface GoogleFontEntry { family: string; category: string; variants: number[] }
15
+ const GOOGLE_FONTS: GoogleFontEntry[] = (googleFontsData as any).fonts;
16
+
17
+ type AddMode = 'closed' | 'google' | 'url' | 'fontface';
18
+ let addMode: AddMode = 'closed';
19
+
20
+ // Google search
21
+ let googleQuery = '';
22
+ $: googleMatches = googleQuery.trim().length >= 1
23
+ ? GOOGLE_FONTS
24
+ .filter((f) => f.family.toLowerCase().includes(googleQuery.toLowerCase()))
25
+ .slice(0, 20)
26
+ : GOOGLE_FONTS.slice(0, 10);
27
+
28
+ // URL paste
29
+ let urlInput = '';
30
+ let urlError = '';
31
+ let urlDiscovering = false;
32
+ let urlParsed: ParsedFamily[] | null = null;
33
+ let urlPickedNames = new Set<string>();
34
+ let urlNeedsManualFamilies = false;
35
+ let urlManualFamilies = '';
36
+
37
+ // @font-face paste
38
+ let fontFaceText = '';
39
+ let fontFaceParsed: ParsedFamily[] = [];
40
+
41
+ function reset() {
42
+ addMode = 'closed';
43
+ googleQuery = '';
44
+ urlInput = '';
45
+ urlError = '';
46
+ urlDiscovering = false;
47
+ urlParsed = null;
48
+ urlPickedNames = new Set();
49
+ urlNeedsManualFamilies = false;
50
+ urlManualFamilies = '';
51
+ fontFaceText = '';
52
+ fontFaceParsed = [];
53
+ }
54
+
55
+ $: fontSourcesList = $editorState.fonts.sources;
56
+ $: fontStacksList = $editorState.fonts.stacks;
57
+
58
+ function commitSources(next: FontSource[]) {
59
+ setFontSources(next);
60
+ applyFontSources(next);
61
+ applyFontStacks(fontStacksList, next);
62
+ }
63
+
64
+ function addGoogleFont(entry: GoogleFontEntry) {
65
+ const weightList = entry.variants.length > 0 ? entry.variants : [400];
66
+ const weightSpec = weightList.length > 1
67
+ ? `wght@${weightList.join(';')}`
68
+ : `wght@${weightList[0]}`;
69
+ const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(entry.family).replace(/%20/g, '+')}:${weightSpec}&display=swap`;
70
+ const source = buildSourceFromUrl(url, [{ name: entry.family, weights: weightList }]);
71
+ commitSources([...fontSourcesList, source]);
72
+ reset();
73
+ }
74
+
75
+ async function discoverUrl() {
76
+ urlError = '';
77
+ urlParsed = null;
78
+ urlNeedsManualFamilies = false;
79
+ const url = urlInput.trim();
80
+ if (!url) return;
81
+ urlDiscovering = true;
82
+ try {
83
+ const found = await discoverFamiliesFromUrl(url);
84
+ if (found && found.length > 0) {
85
+ urlParsed = found;
86
+ urlPickedNames = new Set(found.map((f) => f.name));
87
+ } else {
88
+ urlNeedsManualFamilies = true;
89
+ }
90
+ } catch (e) {
91
+ urlError = 'Discovery failed';
92
+ urlNeedsManualFamilies = true;
93
+ }
94
+ urlDiscovering = false;
95
+ }
96
+
97
+ function addUrlSource() {
98
+ const url = urlInput.trim();
99
+ if (!url) return;
100
+ let families: ParsedFamily[] = [];
101
+ if (urlParsed) {
102
+ families = urlParsed.filter((f) => urlPickedNames.has(f.name));
103
+ } else if (urlNeedsManualFamilies) {
104
+ families = urlManualFamilies
105
+ .split(',')
106
+ .map((s) => s.trim())
107
+ .filter(Boolean)
108
+ .map((name) => ({ name }));
109
+ }
110
+ if (families.length === 0) {
111
+ urlError = 'Pick at least one family';
112
+ return;
113
+ }
114
+ const source = buildSourceFromUrl(url, families);
115
+ commitSources([...fontSourcesList, source]);
116
+ reset();
117
+ }
118
+
119
+ function parseFontFaceTextInput() {
120
+ fontFaceParsed = parseFontFaceText(fontFaceText);
121
+ }
122
+
123
+ function addFontFaceSource() {
124
+ if (!fontFaceText.trim()) return;
125
+ const families = fontFaceParsed.length > 0 ? fontFaceParsed : [];
126
+ if (families.length === 0) {
127
+ fontFaceParsed = parseFontFaceText(fontFaceText);
128
+ if (fontFaceParsed.length === 0) return;
129
+ }
130
+ const source = buildSourceFromFontFaceText(fontFaceText, families.length > 0 ? families : fontFaceParsed);
131
+ commitSources([...fontSourcesList, source]);
132
+ reset();
133
+ }
134
+
135
+ function removeFamily(sourceId: string, familyId: string) {
136
+ const next = fontSourcesList
137
+ .map((s) => (s.id === sourceId ? { ...s, families: s.families.filter((f) => f.id !== familyId) } : s))
138
+ .filter((s) => s.families.length > 0);
139
+ const updatedStacks = fontStacksList.map((stack) => ({
140
+ ...stack,
141
+ slots: stack.slots.filter((slot) => !(slot.kind === 'project' && slot.familyId === familyId)),
142
+ }));
143
+ transaction('remove font family', (s) => {
144
+ s.fonts.sources = next;
145
+ s.fonts.stacks = updatedStacks;
146
+ });
147
+ applyFontSources(next);
148
+ applyFontStacks(updatedStacks, next);
149
+ }
150
+
151
+ function sourceKindLabel(source: FontSource): string {
152
+ if (source.kind === 'google') return 'Google';
153
+ if (source.kind === 'typekit') return 'Typekit';
154
+ if (source.kind === 'font-face') return 'Local';
155
+ return 'CSS URL';
156
+ }
157
+
158
+ function stacksReferencing(familyId: string): string[] {
159
+ return fontStacksList
160
+ .filter((s) => s.slots.some((slot) => slot.kind === 'project' && slot.familyId === familyId))
161
+ .map((s) => s.variable);
162
+ }
163
+
164
+ let expanded = new Set<string>();
165
+ function toggleExpanded(familyId: string) {
166
+ const next = new Set(expanded);
167
+ if (next.has(familyId)) next.delete(familyId); else next.add(familyId);
168
+ expanded = next;
169
+ }
170
+ </script>
171
+
172
+ <section class="project-fonts">
173
+ <header class="pf-header">
174
+ <h3 class="group-title">Project Fonts</h3>
175
+ </header>
176
+
177
+ {#if fontSourcesList.length === 0}
178
+ <p class="pf-empty">No fonts loaded yet. Use the add button below.</p>
179
+ {/if}
180
+
181
+ <div class="pf-sources">
182
+ {#each fontSourcesList as source (source.id)}
183
+ <div class="pf-source">
184
+ <div class="pf-source-head">
185
+ <span class="pf-kind-badge">{sourceKindLabel(source)}</span>
186
+ <span class="pf-source-label">{source.label ?? source.kind}</span>
187
+ {#if source.url}
188
+ <span class="pf-source-url" title={source.url}>{source.url}</span>
189
+ {/if}
190
+ </div>
191
+ <ul class="pf-family-list">
192
+ {#each source.families as fam (fam.id)}
193
+ {@const refs = stacksReferencing(fam.id)}
194
+ {@const isOpen = expanded.has(fam.id)}
195
+ {@const hasMultipleWeights = !!fam.weights && fam.weights.length > 1}
196
+ <li class="pf-family">
197
+ <div class="pf-family-row">
198
+ {#if hasMultipleWeights}
199
+ <button
200
+ type="button"
201
+ class="pf-family-disclosure"
202
+ class:open={isOpen}
203
+ on:click={() => toggleExpanded(fam.id)}
204
+ aria-label={isOpen ? 'Collapse weights' : 'Expand weights'}
205
+ aria-expanded={isOpen}
206
+ ><i class="fas fa-chevron-right" aria-hidden="true"></i></button>
207
+ {:else}
208
+ <span class="pf-family-disclosure-placeholder" aria-hidden="true"></span>
209
+ {/if}
210
+ <span class="pf-family-preview" style="font-family: {fam.cssName}, sans-serif;">Ag</span>
211
+ <span class="pf-family-name">{fam.name}</span>
212
+ <button
213
+ type="button"
214
+ class="pf-family-remove"
215
+ on:click={() => removeFamily(source.id, fam.id)}
216
+ aria-label={`Remove ${fam.name}`}
217
+ title="Remove family"
218
+ ><i class="fas fa-xmark" aria-hidden="true"></i></button>
219
+ </div>
220
+ {#if refs.length > 0}
221
+ <div class="pf-family-meta">
222
+ <span class="pf-meta-label">Used by</span>
223
+ <span class="pf-meta-value">
224
+ {#each refs as r (r)}<span class="pf-meta-pill">{r}</span>{/each}
225
+ </span>
226
+ </div>
227
+ {/if}
228
+ {#if isOpen && hasMultipleWeights && fam.weights}
229
+ <div class="pf-family-meta">
230
+ <span class="pf-meta-label">Weights</span>
231
+ <span class="pf-meta-value pf-meta-weights">{fam.weights.join(', ')}</span>
232
+ </div>
233
+ {/if}
234
+ </li>
235
+ {/each}
236
+ </ul>
237
+ </div>
238
+ {/each}
239
+ </div>
240
+
241
+ {#if addMode === 'closed'}
242
+ <button type="button" class="pf-add-toggle" on:click={() => (addMode = 'google')}>+ add font</button>
243
+ {:else}
244
+ <div class="pf-add-panel">
245
+ <div class="pf-add-tabs">
246
+ <button type="button" class:active={addMode === 'google'} on:click={() => (addMode = 'google')}>Google search</button>
247
+ <button type="button" class:active={addMode === 'url'} on:click={() => (addMode = 'url')}>Paste URL</button>
248
+ <button type="button" class:active={addMode === 'fontface'} on:click={() => (addMode = 'fontface')}>@font-face</button>
249
+ <button type="button" class="pf-add-close" on:click={reset} aria-label="Cancel">×</button>
250
+ </div>
251
+
252
+ {#if addMode === 'google'}
253
+ <input
254
+ type="text"
255
+ class="form-input"
256
+ placeholder="Search Google Fonts (e.g. Inter)"
257
+ bind:value={googleQuery}
258
+ />
259
+ <ul class="pf-results">
260
+ {#each googleMatches as m (m.family)}
261
+ <li class="pf-result">
262
+ <span class="pf-result-preview" style="font-family: '{m.family}', {m.category};">{m.family}</span>
263
+ <span class="pf-result-meta">{m.category} · {m.variants.length}w</span>
264
+ <button type="button" class="pf-result-add" on:click={() => addGoogleFont(m)}>add</button>
265
+ </li>
266
+ {/each}
267
+ {#if googleMatches.length === 0}
268
+ <li class="pf-no-results">No matches</li>
269
+ {/if}
270
+ </ul>
271
+ {:else if addMode === 'url'}
272
+ <input
273
+ type="text"
274
+ class="form-input"
275
+ placeholder="https://fonts.googleapis.com/css2?family=... or Typekit URL"
276
+ bind:value={urlInput}
277
+ />
278
+ <div class="pf-row">
279
+ <button type="button" class="pf-btn" on:click={discoverUrl} disabled={!urlInput.trim() || urlDiscovering}>
280
+ {urlDiscovering ? 'Checking…' : 'Detect families'}
281
+ </button>
282
+ </div>
283
+ {#if urlError}<div class="pf-error">{urlError}</div>{/if}
284
+ {#if urlParsed}
285
+ <div class="pf-detected">Detected families — pick which to add:</div>
286
+ <ul class="pf-checklist">
287
+ {#each urlParsed as f (f.name)}
288
+ <li>
289
+ <label>
290
+ <input
291
+ type="checkbox"
292
+ checked={urlPickedNames.has(f.name)}
293
+ on:change={(e) => {
294
+ const target = e.currentTarget;
295
+ const s = new Set(urlPickedNames);
296
+ if (target.checked) s.add(f.name); else s.delete(f.name);
297
+ urlPickedNames = s;
298
+ }}
299
+ />
300
+ <span class="pf-check-name">{f.name}</span>
301
+ {#if f.weights && f.weights.length > 0}
302
+ <span class="pf-check-meta">{f.weights.length}w</span>
303
+ {/if}
304
+ </label>
305
+ </li>
306
+ {/each}
307
+ </ul>
308
+ <button type="button" class="pf-btn primary" on:click={addUrlSource}>Add {urlPickedNames.size} selected</button>
309
+ {:else if urlNeedsManualFamilies}
310
+ <div class="pf-detected">Couldn't auto-detect families (CORS or no metadata). Name them:</div>
311
+ <input
312
+ type="text"
313
+ class="form-input"
314
+ placeholder="Comma-separated family names"
315
+ bind:value={urlManualFamilies}
316
+ />
317
+ <button type="button" class="pf-btn primary" on:click={addUrlSource} disabled={!urlManualFamilies.trim()}>Add</button>
318
+ {/if}
319
+ {:else if addMode === 'fontface'}
320
+ <textarea
321
+ class="form-input pf-textarea"
322
+ placeholder={'Paste one or more @font-face { ... } rules'}
323
+ rows="6"
324
+ bind:value={fontFaceText}
325
+ on:input={parseFontFaceTextInput}
326
+ ></textarea>
327
+ {#if fontFaceParsed.length > 0}
328
+ <div class="pf-detected">Detected: {fontFaceParsed.map((f) => f.name).join(', ')}</div>
329
+ {/if}
330
+ <button type="button" class="pf-btn primary" on:click={addFontFaceSource} disabled={fontFaceParsed.length === 0}>Add</button>
331
+ {/if}
332
+ </div>
333
+ {/if}
334
+ </section>
335
+
336
+ <style>
337
+ .project-fonts {
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: var(--ui-space-8);
341
+ }
342
+
343
+ .pf-header {
344
+ display: flex;
345
+ align-items: baseline;
346
+ gap: var(--ui-space-12);
347
+ justify-content: space-between;
348
+ }
349
+
350
+ .group-title {
351
+ margin: 0;
352
+ font-size: var(--ui-font-size-lg);
353
+ color: var(--ui-text-primary);
354
+ }
355
+
356
+ .pf-empty {
357
+ margin: 0;
358
+ padding: var(--ui-space-8) 0;
359
+ color: var(--ui-text-muted);
360
+ font-size: var(--ui-font-size-sm);
361
+ }
362
+
363
+ .pf-sources {
364
+ display: flex;
365
+ flex-direction: column;
366
+ gap: var(--ui-space-8);
367
+ }
368
+
369
+ .pf-source {
370
+ border: 1px solid var(--ui-border-faint);
371
+ border-radius: var(--ui-radius-md);
372
+ display: flex;
373
+ flex-direction: column;
374
+ }
375
+
376
+ .pf-source-head {
377
+ display: flex;
378
+ align-items: center;
379
+ gap: var(--ui-space-8);
380
+ padding: var(--ui-space-8) var(--ui-space-12);
381
+ border-bottom: 1px solid var(--ui-border-faint);
382
+ background: var(--ui-surface-subtle, rgba(255,255,255,0.02));
383
+ border-radius: var(--ui-radius-md) var(--ui-radius-md) 0 0;
384
+ }
385
+
386
+ .pf-kind-badge {
387
+ font-size: var(--ui-font-size-xs);
388
+ color: var(--ui-text-tertiary);
389
+ border: 1px solid var(--ui-border-faint);
390
+ padding: 0 var(--ui-space-4);
391
+ border-radius: var(--ui-radius-sm);
392
+ font-family: var(--ui-font-mono);
393
+ }
394
+
395
+ .pf-source-label {
396
+ font-size: var(--ui-font-size-lg);
397
+ color: var(--ui-text-primary);
398
+ font-weight: 500;
399
+ }
400
+
401
+ .pf-source-url {
402
+ font-family: var(--ui-font-mono);
403
+ font-size: var(--ui-font-size-xs);
404
+ color: var(--ui-text-tertiary);
405
+ overflow: hidden;
406
+ text-overflow: ellipsis;
407
+ white-space: nowrap;
408
+ flex: 1;
409
+ min-width: 0;
410
+ }
411
+
412
+ .pf-family-list {
413
+ list-style: none;
414
+ margin: 0;
415
+ padding: var(--ui-space-8);
416
+ display: grid;
417
+ grid-template-columns: repeat(2, minmax(0, 1fr));
418
+ gap: var(--ui-space-2) var(--ui-space-8);
419
+ }
420
+
421
+ .pf-family {
422
+ display: flex;
423
+ flex-direction: column;
424
+ min-width: 0;
425
+ }
426
+
427
+ .pf-family-row {
428
+ display: grid;
429
+ grid-template-columns: 24px 1.75rem 1fr 24px;
430
+ align-items: center;
431
+ gap: var(--ui-space-6);
432
+ padding: 0 var(--ui-space-4);
433
+ border-radius: var(--ui-radius-sm);
434
+ min-height: 28px;
435
+ }
436
+ .pf-family-row:hover {
437
+ background: var(--ui-surface-subtle, rgba(255,255,255,0.02));
438
+ }
439
+
440
+ .pf-family-disclosure,
441
+ .pf-family-remove {
442
+ display: inline-flex;
443
+ align-items: center;
444
+ justify-content: center;
445
+ width: 24px;
446
+ height: 24px;
447
+ padding: 0;
448
+ background: none;
449
+ border: none;
450
+ border-radius: var(--ui-radius-sm);
451
+ color: var(--ui-text-muted);
452
+ cursor: pointer;
453
+ font-size: var(--ui-font-size-sm);
454
+ line-height: 1;
455
+ }
456
+ .pf-family-disclosure:hover,
457
+ .pf-family-remove:hover {
458
+ color: var(--ui-text-primary);
459
+ background: var(--ui-surface-hover, rgba(255,255,255,0.06));
460
+ }
461
+ .pf-family-disclosure i {
462
+ transition: transform 0.12s ease;
463
+ }
464
+ .pf-family-disclosure.open i { transform: rotate(90deg); }
465
+ .pf-family-disclosure-placeholder {
466
+ width: 24px;
467
+ height: 24px;
468
+ display: inline-block;
469
+ }
470
+
471
+ .pf-family-preview {
472
+ font-size: var(--ui-font-size-md);
473
+ color: var(--ui-text-primary);
474
+ min-width: 1.75rem;
475
+ text-align: center;
476
+ line-height: 1.2;
477
+ }
478
+
479
+ .pf-family-name {
480
+ font-size: var(--ui-font-size-sm);
481
+ color: var(--ui-text-primary);
482
+ min-width: 0;
483
+ overflow: hidden;
484
+ text-overflow: ellipsis;
485
+ white-space: nowrap;
486
+ }
487
+
488
+ .pf-family-meta {
489
+ display: grid;
490
+ grid-template-columns: 4rem 1fr;
491
+ align-items: center;
492
+ gap: var(--ui-space-6);
493
+ padding: var(--ui-space-2) var(--ui-space-4) var(--ui-space-2) calc(24px + var(--ui-space-6) + 1.75rem + var(--ui-space-6));
494
+ font-size: var(--ui-font-size-xs);
495
+ font-family: var(--ui-font-mono);
496
+ }
497
+ .pf-meta-label { color: var(--ui-text-muted); }
498
+ .pf-meta-value {
499
+ display: flex;
500
+ flex-wrap: wrap;
501
+ gap: var(--ui-space-2);
502
+ color: var(--ui-text-tertiary);
503
+ min-width: 0;
504
+ }
505
+ .pf-meta-pill {
506
+ padding: 0 var(--ui-space-4);
507
+ border-radius: var(--ui-radius-sm);
508
+ background: var(--ui-surface-hover, rgba(255,255,255,0.06));
509
+ color: var(--ui-text-secondary, var(--ui-text-primary));
510
+ }
511
+ .pf-meta-weights { word-break: break-word; }
512
+
513
+ .pf-add-toggle {
514
+ align-self: flex-start;
515
+ background: none;
516
+ border: 1px dashed var(--ui-border-faint);
517
+ color: var(--ui-text-muted);
518
+ font-size: var(--ui-font-size-sm);
519
+ padding: var(--ui-space-4) var(--ui-space-8);
520
+ border-radius: var(--ui-radius-sm);
521
+ cursor: pointer;
522
+ }
523
+ .pf-add-toggle:hover {
524
+ color: var(--ui-text-primary);
525
+ border-color: var(--ui-border);
526
+ }
527
+
528
+ .pf-add-panel {
529
+ display: flex;
530
+ flex-direction: column;
531
+ gap: var(--ui-space-8);
532
+ padding: var(--ui-space-8);
533
+ border: 1px solid var(--ui-border-faint);
534
+ border-radius: var(--ui-radius-sm);
535
+ }
536
+
537
+ .pf-add-tabs {
538
+ display: flex;
539
+ gap: var(--ui-space-4);
540
+ }
541
+ .pf-add-tabs button {
542
+ background: none;
543
+ border: 1px solid var(--ui-border-faint);
544
+ color: var(--ui-text-muted);
545
+ font-size: var(--ui-font-size-sm);
546
+ padding: var(--ui-space-4) var(--ui-space-8);
547
+ border-radius: var(--ui-radius-sm);
548
+ cursor: pointer;
549
+ }
550
+ .pf-add-tabs button.active {
551
+ color: var(--ui-text-primary);
552
+ border-color: var(--ui-border);
553
+ }
554
+ .pf-add-tabs .pf-add-close {
555
+ margin-left: auto;
556
+ }
557
+
558
+ .pf-row {
559
+ display: flex;
560
+ gap: var(--ui-space-8);
561
+ align-items: center;
562
+ }
563
+
564
+ .pf-btn {
565
+ background: none;
566
+ border: 1px solid var(--ui-border-faint);
567
+ color: var(--ui-text-primary);
568
+ font-size: var(--ui-font-size-sm);
569
+ padding: var(--ui-space-4) var(--ui-space-8);
570
+ border-radius: var(--ui-radius-sm);
571
+ cursor: pointer;
572
+ }
573
+ .pf-btn:hover:not(:disabled) { border-color: var(--ui-border); }
574
+ .pf-btn:disabled { opacity: 0.4; cursor: not-allowed; }
575
+ .pf-btn.primary {
576
+ background: var(--ui-focus, #5eb2ff);
577
+ border-color: var(--ui-focus, #5eb2ff);
578
+ color: #000;
579
+ }
580
+
581
+ .pf-error { color: var(--ui-text-danger, #ff6b6b); font-size: var(--ui-font-size-sm); }
582
+
583
+ .pf-detected { color: var(--ui-text-tertiary); font-size: var(--ui-font-size-sm); }
584
+
585
+ .pf-checklist {
586
+ list-style: none;
587
+ margin: 0;
588
+ padding: 0;
589
+ display: flex;
590
+ flex-direction: column;
591
+ gap: var(--ui-space-2);
592
+ }
593
+ .pf-checklist label {
594
+ display: flex;
595
+ align-items: center;
596
+ gap: var(--ui-space-6);
597
+ font-size: var(--ui-font-size-sm);
598
+ cursor: pointer;
599
+ }
600
+ .pf-check-name { color: var(--ui-text-primary); }
601
+ .pf-check-meta { color: var(--ui-text-tertiary); font-family: var(--ui-font-mono); font-size: var(--ui-font-size-xs); }
602
+
603
+ .pf-results {
604
+ list-style: none;
605
+ margin: 0;
606
+ padding: 0;
607
+ display: flex;
608
+ flex-direction: column;
609
+ gap: var(--ui-space-2);
610
+ max-height: 18rem;
611
+ overflow-y: auto;
612
+ }
613
+
614
+ .pf-result {
615
+ display: grid;
616
+ grid-template-columns: 1fr auto auto;
617
+ align-items: center;
618
+ gap: var(--ui-space-8);
619
+ padding: var(--ui-space-4);
620
+ border: 1px solid transparent;
621
+ border-radius: var(--ui-radius-sm);
622
+ }
623
+ .pf-result:hover { border-color: var(--ui-border-faint); }
624
+
625
+ .pf-result-preview { font-size: var(--ui-font-size-lg); color: var(--ui-text-primary); }
626
+ .pf-result-meta { font-size: var(--ui-font-size-xs); color: var(--ui-text-tertiary); font-family: var(--ui-font-mono); }
627
+ .pf-result-add {
628
+ background: none;
629
+ border: 1px solid var(--ui-border-faint);
630
+ color: var(--ui-text-muted);
631
+ font-size: var(--ui-font-size-sm);
632
+ padding: 2px var(--ui-space-6);
633
+ border-radius: var(--ui-radius-sm);
634
+ cursor: pointer;
635
+ }
636
+ .pf-result-add:hover { color: var(--ui-text-primary); border-color: var(--ui-border); }
637
+
638
+ .pf-no-results { color: var(--ui-text-muted); font-size: var(--ui-font-size-sm); padding: var(--ui-space-4); }
639
+
640
+ .pf-textarea {
641
+ font-family: var(--ui-font-mono);
642
+ font-size: var(--ui-font-size-xs);
643
+ resize: vertical;
644
+ }
645
+ </style>