@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,148 @@
1
+ /**
2
+ * Static registry of CSS custom-property declarations.
3
+ *
4
+ * Layer-1 design tokens live in tokens.css (the single source of truth for
5
+ * shared primitives). Layer-2 component tokens live inside each component's
6
+ * `<style>` block as `:global(:root) { ... }` declarations — owned by the
7
+ * component that uses them.
8
+ *
9
+ * Token-picking UIs need to recover the semantic identity of an arbitrary
10
+ * variable — e.g. to display "info text / primary" rather than a raw hex. For
11
+ * alias variables we must follow the `var(--x)` chain from the declaration to
12
+ * find the underlying token whose *name* the selector can parse.
13
+ *
14
+ * This module parses both sources at import time (tokens.css via Vite's
15
+ * ?raw loader, component files via import.meta.glob) and exposes helpers that
16
+ * walk those aliases. The pure `buildTokenRegistry` and `extractGlobalRootBody`
17
+ * helpers are also exported so tests can construct a registry from fs-loaded
18
+ * files (Vitest's default CSS plugin swallows `?raw` imports for .css files).
19
+ */
20
+
21
+ import { derived, type Readable } from 'svelte/store';
22
+ import tokensCss from '../styles/tokens.css?raw';
23
+ import { editorState } from './editorStore';
24
+ import type { EditorState } from './editorTypes';
25
+ import { extractGlobalRootBody } from './parsers/globalRootBlock';
26
+
27
+ // Re-exported for tests and downstream consumers that previously imported it
28
+ // from this module. The canonical implementation lives in `./parsers/globalRootBlock`
29
+ // so the dev-server vite plugin can share it.
30
+ export { extractGlobalRootBody };
31
+
32
+ const componentSources = import.meta.glob('../components/*.svelte', {
33
+ query: '?raw',
34
+ import: 'default',
35
+ eager: true,
36
+ }) as Record<string, string>;
37
+
38
+ export interface TokenRegistry {
39
+ getDeclaredValue(varName: string): string | null;
40
+ resolveAliasChain(varName: string): string[];
41
+ }
42
+
43
+ /**
44
+ * Pure constructor: parses a CSS source string (possibly concatenated from
45
+ * multiple files) and returns a registry bound to that snapshot.
46
+ */
47
+ export function buildTokenRegistry(cssText: string): TokenRegistry {
48
+ const declarations = new Map<string, string>();
49
+ const re = /(--[a-z0-9-]+)\s*:\s*([^;]+);/gi;
50
+ let m: RegExpExecArray | null;
51
+ while ((m = re.exec(cssText)) !== null) {
52
+ declarations.set(m[1], m[2].trim());
53
+ }
54
+
55
+ function resolveAliasChain(varName: string): string[] {
56
+ const chain = [varName];
57
+ const visited = new Set<string>([varName]);
58
+ let current = varName;
59
+ while (true) {
60
+ const decl = declarations.get(current);
61
+ if (!decl) return chain;
62
+ const aliasMatch = decl.match(/var\((--[a-z0-9-]+)\)/i);
63
+ if (!aliasMatch) return chain;
64
+ const next = aliasMatch[1];
65
+ if (visited.has(next)) return chain;
66
+ visited.add(next);
67
+ chain.push(next);
68
+ current = next;
69
+ }
70
+ }
71
+
72
+ return {
73
+ getDeclaredValue: (v) => declarations.get(v) ?? null,
74
+ resolveAliasChain,
75
+ };
76
+ }
77
+
78
+ const componentTokenCss = Object.values(componentSources)
79
+ .map(extractGlobalRootBody)
80
+ .join('\n');
81
+
82
+ const defaultRegistry = buildTokenRegistry(tokensCss + '\n' + componentTokenCss);
83
+
84
+ /** Raw declared value from tokens.css, or null if not declared. */
85
+ export const getDeclaredValue = defaultRegistry.getDeclaredValue;
86
+
87
+ /**
88
+ * Walk the alias chain starting at `varName`, returning each successive
89
+ * variable reference (including the starting one). Stops when a declaration is
90
+ * a literal value (e.g. hex) or when the next link is absent from the
91
+ * registry. Guards against cycles.
92
+ *
93
+ * Example: `--notification-info-title` → `--text-info` → stops (literal hex).
94
+ * Returns `['--notification-info-title', '--text-info']`.
95
+ *
96
+ * This is a static snapshot: it only sees declarations from tokens.css + the
97
+ * baked-in `:global(:root)` blocks at module load. For state-aware resolution
98
+ * that includes live component-alias edits, subscribe to `tokenRegistry$`.
99
+ */
100
+ export const resolveAliasChain = defaultRegistry.resolveAliasChain;
101
+
102
+ /**
103
+ * Build a registry that layers live component-alias overrides on top of the
104
+ * static base. Cheap enough to rebuild per editorState tick — components
105
+ * count is small and the alias walk is lazy.
106
+ */
107
+ function buildOverlayRegistry(
108
+ base: TokenRegistry,
109
+ components: EditorState['components'],
110
+ ): TokenRegistry {
111
+ const overrides = new Map<string, string>();
112
+ for (const slice of Object.values(components)) {
113
+ for (const [varName, ref] of Object.entries(slice.aliases)) {
114
+ overrides.set(varName, ref.kind === 'token' ? `var(${ref.name})` : ref.value);
115
+ }
116
+ }
117
+ const getDeclared = (v: string): string | null =>
118
+ overrides.has(v) ? overrides.get(v)! : base.getDeclaredValue(v);
119
+ return {
120
+ getDeclaredValue: getDeclared,
121
+ resolveAliasChain(varName: string): string[] {
122
+ const chain = [varName];
123
+ const visited = new Set<string>([varName]);
124
+ let current = varName;
125
+ while (true) {
126
+ const decl = getDeclared(current);
127
+ if (!decl) return chain;
128
+ const aliasMatch = decl.match(/var\((--[a-z0-9-]+)\)/i);
129
+ if (!aliasMatch) return chain;
130
+ const next = aliasMatch[1];
131
+ if (visited.has(next)) return chain;
132
+ visited.add(next);
133
+ chain.push(next);
134
+ current = next;
135
+ }
136
+ },
137
+ };
138
+ }
139
+
140
+ /**
141
+ * State-aware token registry: overlays live `state.components[*].aliases`
142
+ * on top of the static base registry. Consume with `$tokenRegistry$` in
143
+ * Svelte components when the UI needs to reflect in-flight alias edits
144
+ * (e.g. selectors displaying the currently-selected semantic token).
145
+ */
146
+ export const tokenRegistry$: Readable<TokenRegistry> = derived(editorState, ($state) =>
147
+ buildOverlayRegistry(defaultRegistry, $state.components),
148
+ );
@@ -0,0 +1,384 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
4
+ import PresetFileManager from '../ui/PresetFileManager.svelte';
5
+ import { navigate } from '../lib/router';
6
+ import { componentRegistryEntries, validateRegistryAgainstServerScan } from '../component-editor/registry';
7
+ import { listComponents } from '../lib/componentConfigService';
8
+ import { selectedComponent } from '../lib/editorViewStore';
9
+
10
+ let drawerOpen = true;
11
+
12
+ // Demo page is statically imported from `./Demo.svelte` in App.svelte; the
13
+ // glob resolves to an empty object if the file has been deleted, in which
14
+ // case we hide the demo option from the page-switcher.
15
+ const demoExists = Object.keys(import.meta.glob('./Demo.svelte')).length > 0;
16
+
17
+ let pageMenuOpen = false;
18
+ let pageMenuRoot: HTMLElement;
19
+
20
+ const HINT_DELAY_MS = 80;
21
+ let hintLabel: string | null = null;
22
+ let hintTop = 0;
23
+ let hintTimer: ReturnType<typeof setTimeout> | null = null;
24
+
25
+ function showHint(label: string, target: HTMLElement) {
26
+ if (drawerOpen) return;
27
+ if (hintTimer) clearTimeout(hintTimer);
28
+ const top = target.getBoundingClientRect().top + target.offsetHeight / 2;
29
+ hintTimer = setTimeout(() => {
30
+ hintLabel = label;
31
+ hintTop = top;
32
+ }, HINT_DELAY_MS);
33
+ }
34
+
35
+ function hideHint() {
36
+ if (hintTimer) {
37
+ clearTimeout(hintTimer);
38
+ hintTimer = null;
39
+ }
40
+ hintLabel = null;
41
+ }
42
+
43
+ $: if (drawerOpen) hideHint();
44
+
45
+ function selectComponent(id: string) {
46
+ selectedComponent.set(id);
47
+ hideHint();
48
+ }
49
+
50
+ function selectPage(path: string) {
51
+ pageMenuOpen = false;
52
+ navigate(path);
53
+ }
54
+
55
+ function handleDocClick(e: MouseEvent) {
56
+ if (!pageMenuOpen) return;
57
+ if (pageMenuRoot && !pageMenuRoot.contains(e.target as Node)) {
58
+ pageMenuOpen = false;
59
+ }
60
+ }
61
+
62
+ function handleKeydown(e: KeyboardEvent) {
63
+ if (e.key === 'Escape') {
64
+ if (pageMenuOpen) pageMenuOpen = false;
65
+ else if (drawerOpen) drawerOpen = false;
66
+ }
67
+ }
68
+
69
+ onMount(async () => {
70
+ document.addEventListener('click', handleDocClick, true);
71
+ window.addEventListener('keydown', handleKeydown);
72
+ try {
73
+ const summaries = await listComponents();
74
+ validateRegistryAgainstServerScan(summaries.map((s) => s.name));
75
+ } catch {
76
+ // Server unreachable — registry's eager schema registration still works
77
+ // for the editor; validation just gets skipped this boot.
78
+ }
79
+ });
80
+
81
+ onDestroy(() => {
82
+ document.removeEventListener('click', handleDocClick, true);
83
+ window.removeEventListener('keydown', handleKeydown);
84
+ });
85
+
86
+ const componentNavItems = componentRegistryEntries.map(({ id, label, icon }) => ({ id, label, icon }));
87
+ </script>
88
+
89
+ <!--
90
+ Site-level component editor page. Wrapped in .editor-page so the
91
+ ComponentsTab scaffolding (labels, section wrappers) can resolve its
92
+ --ui-* custom properties from styles/ui-editor.css. The actual components inside
93
+ still read the user's design tokens, so live edits in the overlay
94
+ editor flow straight through to this page.
95
+ -->
96
+ <div class="editor-page components-shell" class:rail-expanded={drawerOpen}>
97
+ <nav class="sidebar rail" class:expanded={drawerOpen}>
98
+ <div class="rail-header" bind:this={pageMenuRoot}>
99
+ <button
100
+ type="button"
101
+ class="rail-toggle"
102
+ aria-label={drawerOpen ? 'Collapse components menu' : 'Expand components menu'}
103
+ aria-expanded={drawerOpen}
104
+ on:click={() => (drawerOpen = !drawerOpen)}
105
+ >
106
+ <i class="fas {drawerOpen ? 'fa-arrow-left' : 'fa-arrow-right'}"></i>
107
+ </button>
108
+ <button
109
+ type="button"
110
+ class="page-menu-trigger"
111
+ class:open={pageMenuOpen}
112
+ aria-haspopup="menu"
113
+ aria-expanded={pageMenuOpen}
114
+ tabindex={drawerOpen ? 0 : -1}
115
+ on:click={() => drawerOpen && (pageMenuOpen = !pageMenuOpen)}
116
+ >
117
+ <span class="rail-label">Components</span>
118
+ <i class="fas fa-chevron-down rail-chevron" class:open={pageMenuOpen}></i>
119
+ </button>
120
+ {#if pageMenuOpen && drawerOpen}
121
+ <div class="page-menu" role="menu">
122
+ <button class="page-menu-item" role="menuitem" on:click={() => selectPage('/')}>
123
+ <i class="fas fa-home"></i>
124
+ <span>Main site</span>
125
+ </button>
126
+ {#if demoExists}
127
+ <button class="page-menu-item" role="menuitem" on:click={() => selectPage('/demo')}>
128
+ <i class="fas fa-box-open"></i>
129
+ <span>Demo page</span>
130
+ </button>
131
+ {/if}
132
+ </div>
133
+ {/if}
134
+ </div>
135
+ <div class="nav-items">
136
+ {#each componentNavItems as item}
137
+ <button
138
+ class="nav-item"
139
+ class:active={$selectedComponent === item.id}
140
+ on:mouseenter={(e) => showHint(item.label, e.currentTarget)}
141
+ on:mouseleave={hideHint}
142
+ on:click={() => selectComponent(item.id)}
143
+ >
144
+ <i class={item.icon}></i>
145
+ <span class="rail-label">{item.label}</span>
146
+ </button>
147
+ {/each}
148
+ </div>
149
+ {#if drawerOpen}
150
+ <div class="sidebar-footer">
151
+ <PresetFileManager />
152
+ </div>
153
+ {/if}
154
+ </nav>
155
+
156
+ <main class="content">
157
+ <ComponentsTab selectedComponent={$selectedComponent} />
158
+ </main>
159
+
160
+ {#if hintLabel !== null && !drawerOpen}
161
+ <div class="rail-hint" style="top: {hintTop}px">{hintLabel}</div>
162
+ {/if}
163
+ </div>
164
+
165
+ <style>
166
+ @import '../styles/ui-editor.css';
167
+ .components-shell {
168
+ --rail-w: 48px;
169
+ display: grid;
170
+ grid-template-columns: var(--rail-w) minmax(0, 1fr);
171
+ height: 100vh;
172
+ width: 100%;
173
+ background: black;
174
+ overflow: hidden;
175
+ transition: grid-template-columns 220ms ease;
176
+ }
177
+
178
+ .components-shell.rail-expanded {
179
+ --rail-w: 240px;
180
+ }
181
+
182
+ .sidebar.rail {
183
+ position: relative;
184
+ height: 100vh;
185
+ overflow-y: auto;
186
+ overflow-x: hidden;
187
+ background: black;
188
+ border-right: 1px solid var(--ui-border-faint);
189
+ display: flex;
190
+ flex-direction: column;
191
+ min-width: 0;
192
+ }
193
+
194
+ .rail-header {
195
+ position: relative;
196
+ display: grid;
197
+ grid-template-columns: 48px 1fr;
198
+ align-items: center;
199
+ padding: var(--ui-space-12) 0 var(--ui-space-12) 0;
200
+ border-bottom: 1px solid var(--ui-border-faint);
201
+ }
202
+
203
+ .rail-toggle {
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ width: 48px;
208
+ height: 36px;
209
+ padding: 0;
210
+ background: none;
211
+ border: none;
212
+ color: var(--ui-text-primary);
213
+ cursor: pointer;
214
+ transition: background var(--ui-transition-fast);
215
+ }
216
+
217
+ .rail-toggle:hover {
218
+ background: var(--ui-hover);
219
+ }
220
+
221
+ .page-menu-trigger {
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: space-between;
225
+ gap: var(--ui-space-8);
226
+ height: 36px;
227
+ padding: 0 var(--ui-space-10) 0 0;
228
+ background: none;
229
+ border: none;
230
+ color: var(--ui-text-primary);
231
+ font-family: inherit;
232
+ font-size: var(--ui-font-size-lg);
233
+ font-weight: var(--ui-font-weight-bold);
234
+ text-align: left;
235
+ cursor: pointer;
236
+ transition: opacity 180ms ease;
237
+ opacity: 0;
238
+ pointer-events: none;
239
+ }
240
+
241
+ .components-shell.rail-expanded .page-menu-trigger {
242
+ opacity: 1;
243
+ pointer-events: auto;
244
+ }
245
+
246
+ .rail-chevron {
247
+ font-size: 0.7em;
248
+ color: var(--ui-text-tertiary);
249
+ transition: transform var(--ui-transition-fast);
250
+ }
251
+
252
+ .rail-chevron.open {
253
+ transform: rotate(180deg);
254
+ }
255
+
256
+ .rail-label {
257
+ white-space: nowrap;
258
+ overflow: hidden;
259
+ opacity: 0;
260
+ transition: opacity 180ms ease;
261
+ }
262
+
263
+ .components-shell.rail-expanded .rail-label {
264
+ opacity: 1;
265
+ }
266
+
267
+ .page-menu {
268
+ position: absolute;
269
+ top: calc(100% - var(--ui-space-2));
270
+ left: 48px;
271
+ right: var(--ui-space-8);
272
+ background: var(--ui-surface-low);
273
+ border: 1px solid var(--ui-border-default);
274
+ border-radius: var(--ui-radius-md);
275
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
276
+ padding: var(--ui-space-4);
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 2px;
280
+ z-index: 10;
281
+ }
282
+
283
+ .page-menu-item {
284
+ display: flex;
285
+ align-items: center;
286
+ gap: var(--ui-space-8);
287
+ padding: var(--ui-space-6) var(--ui-space-10);
288
+ background: none;
289
+ border: none;
290
+ border-radius: var(--ui-radius-sm);
291
+ color: var(--ui-text-secondary);
292
+ font-family: inherit;
293
+ font-size: var(--ui-font-size-md);
294
+ text-align: left;
295
+ cursor: pointer;
296
+ transition: background var(--ui-transition-fast), color var(--ui-transition-fast);
297
+ }
298
+
299
+ .page-menu-item i {
300
+ width: 1rem;
301
+ text-align: center;
302
+ opacity: 0.7;
303
+ }
304
+
305
+ .page-menu-item:hover {
306
+ background: var(--ui-hover);
307
+ color: var(--ui-text-primary);
308
+ }
309
+
310
+ .nav-items {
311
+ display: flex;
312
+ flex-direction: column;
313
+ gap: var(--ui-space-2);
314
+ padding: 0 0 var(--ui-space-16);
315
+ background: black;
316
+ }
317
+
318
+ .sidebar-footer {
319
+ flex-shrink: 0;
320
+ margin-top: auto;
321
+ padding: var(--ui-space-12) var(--ui-space-8) var(--ui-space-16);
322
+ border-top: 1px solid var(--ui-border-faint);
323
+ }
324
+
325
+ .nav-item {
326
+ display: grid;
327
+ grid-template-columns: 48px 1fr;
328
+ align-items: center;
329
+ width: 100%;
330
+ height: 36px;
331
+ padding: 0;
332
+ background: none;
333
+ border: none;
334
+ border-radius: 0;
335
+ color: var(--ui-text-tertiary);
336
+ font-size: var(--ui-font-size-md);
337
+ cursor: pointer;
338
+ text-align: left;
339
+ overflow: hidden;
340
+ transition: color 60ms ease, background 60ms ease;
341
+ }
342
+
343
+ .nav-item:hover {
344
+ color: var(--ui-text-secondary);
345
+ background: var(--ui-hover);
346
+ }
347
+
348
+ .nav-item.active {
349
+ color: var(--ui-text-primary);
350
+ background: var(--ui-surface-high);
351
+ }
352
+
353
+ .nav-item i {
354
+ justify-self: center;
355
+ width: 1.25rem;
356
+ text-align: center;
357
+ font-size: var(--ui-font-size-md);
358
+ opacity: 0.85;
359
+ }
360
+
361
+ .content {
362
+ padding: 0 var(--ui-space-32);
363
+ background: black;
364
+ min-width: 0;
365
+ height: 100vh;
366
+ overflow-y: auto;
367
+ }
368
+
369
+ .rail-hint {
370
+ position: fixed;
371
+ left: calc(var(--rail-w) + var(--ui-space-6));
372
+ transform: translateY(-50%);
373
+ z-index: 50;
374
+ padding: var(--ui-space-4) var(--ui-space-8);
375
+ background: var(--ui-surface-low);
376
+ border: 1px solid var(--ui-border-default);
377
+ border-radius: var(--ui-radius-sm);
378
+ color: var(--ui-text-primary);
379
+ font-size: var(--ui-font-size-sm);
380
+ white-space: nowrap;
381
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
382
+ pointer-events: none;
383
+ }
384
+ </style>
@@ -0,0 +1,2 @@
1
+ import { SvelteComponent } from 'svelte';
2
+ export default class ComponentEditorPage extends SvelteComponent<Record<string, never>> {}
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import EditorShell from './EditorShell.svelte';
4
+ import UICopyPopover from '../ui/UICopyPopover.svelte';
5
+ import { installEditorKeybindings } from '../lib/editorKeybindings';
6
+ import { initializeEditorStore } from '../lib/editorStore';
7
+ import { storageKey } from '../lib/editorConfig';
8
+
9
+ const inOverlay = typeof window !== 'undefined' && window.parent !== window;
10
+
11
+ // Where "Back to site" sends the user. Prefer the previous non-editor entry
12
+ // from session history; fall back to /demo and finally /.
13
+ const backHref = pickBackHref();
14
+
15
+ function pickBackHref(): string {
16
+ try {
17
+ const prev = sessionStorage.getItem(storageKey('prev-route'));
18
+ if (prev && prev !== '/editor') return prev;
19
+ } catch {
20
+ // ignore
21
+ }
22
+ return '/demo';
23
+ }
24
+
25
+ onMount(() => {
26
+ initializeEditorStore();
27
+ return installEditorKeybindings();
28
+ });
29
+ </script>
30
+
31
+ <div class="editor-page">
32
+ {#if !inOverlay}
33
+ <div class="editor-bar">
34
+ <div class="bar-left">
35
+ <a href={backHref} class="back-link">
36
+ <i class="fas fa-arrow-left"></i>
37
+ <span>Back to site</span>
38
+ </a>
39
+ <span class="editor-label">Editor</span>
40
+ </div>
41
+ </div>
42
+ {/if}
43
+
44
+ <EditorShell />
45
+ <UICopyPopover />
46
+ </div>
47
+
48
+ <style>
49
+ @import '../styles/ui-editor.css';
50
+
51
+ .editor-page {
52
+ min-height: 100vh;
53
+ background: black;
54
+ }
55
+
56
+ .editor-bar {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: var(--ui-space-16);
60
+ padding: var(--ui-space-10) var(--ui-space-16);
61
+ background: black;
62
+ border-bottom: 1px solid var(--ui-border-faint);
63
+ min-height: 52px;
64
+ }
65
+
66
+ .bar-left {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: var(--ui-space-16);
70
+ min-width: 0;
71
+ }
72
+
73
+ .back-link {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: var(--ui-space-6);
77
+ color: var(--ui-text-tertiary);
78
+ text-decoration: none;
79
+ font-size: var(--ui-font-size-md);
80
+ transition: color var(--ui-transition-fast);
81
+ }
82
+
83
+ .back-link:hover {
84
+ color: var(--ui-text-primary);
85
+ }
86
+
87
+ .editor-label {
88
+ font-size: var(--ui-font-size-md);
89
+ font-weight: var(--ui-font-weight-semibold);
90
+ color: var(--ui-text-secondary);
91
+ }
92
+
93
+ @media (max-width: 1100px) {
94
+ .editor-label {
95
+ display: none;
96
+ }
97
+ }
98
+ </style>
@@ -0,0 +1,2 @@
1
+ import { SvelteComponent } from 'svelte';
2
+ export default class Editor extends SvelteComponent<Record<string, never>> {}