@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.
- package/README.md +168 -21
- package/dist-plugin/index.cjs +823 -336
- package/dist-plugin/index.d.cts +9 -7
- package/dist-plugin/index.d.ts +9 -7
- package/dist-plugin/index.js +822 -335
- package/package.json +46 -20
- package/src/assets/newspaper.webp +0 -0
- package/src/assets/offering.webp +0 -0
- package/src/component-editor/BadgeEditor.svelte +170 -0
- package/src/component-editor/CalloutEditor.svelte +103 -0
- package/src/component-editor/CardEditor.svelte +184 -0
- package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
- package/src/component-editor/CornerBadgeEditor.svelte +207 -0
- package/src/component-editor/DialogEditor.svelte +172 -0
- package/src/component-editor/ImageEditor.svelte +72 -0
- package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
- package/src/component-editor/NotificationEditor.svelte +160 -0
- package/src/component-editor/ProgressBarEditor.svelte +124 -0
- package/src/component-editor/RadioButtonEditor.svelte +140 -0
- package/src/component-editor/SectionDividerEditor.svelte +263 -0
- package/src/component-editor/SegmentedControlEditor.svelte +154 -0
- package/src/component-editor/StandardButtonsEditor.svelte +178 -0
- package/src/component-editor/TabBarEditor.svelte +137 -0
- package/src/component-editor/TableEditor.svelte +128 -0
- package/src/component-editor/TooltipEditor.svelte +122 -0
- package/src/component-editor/editorTokens.test.ts +93 -0
- package/src/component-editor/groupKeySlots.test.ts +67 -0
- package/src/component-editor/groupKeySnapshot.test.ts +52 -0
- package/src/component-editor/index.ts +5 -0
- package/src/component-editor/registry.ts +246 -0
- package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
- package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
- package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
- package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
- package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
- package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
- package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
- package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
- package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
- package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
- package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
- package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
- package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
- package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
- package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
- package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
- package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
- package/src/component-editor/scaffolding/componentSources.ts +9 -0
- package/src/component-editor/scaffolding/defaultSections.ts +16 -0
- package/src/component-editor/scaffolding/editorContext.ts +44 -0
- package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
- package/src/component-editor/scaffolding/siblings.ts +33 -0
- package/src/component-editor/scaffolding/types.ts +39 -0
- package/src/components/Badge.svelte +231 -42
- package/src/components/Button.svelte +324 -124
- package/src/components/Callout.svelte +145 -0
- package/src/components/Card.svelte +123 -25
- package/src/components/CollapsibleSection.svelte +213 -35
- package/src/components/CornerBadge.svelte +224 -0
- package/src/components/Dialog.svelte +137 -114
- package/src/components/Image.svelte +43 -0
- package/src/components/InlineEditActions.svelte +74 -14
- package/src/components/Notification.svelte +184 -163
- package/src/components/ProgressBar.svelte +216 -22
- package/src/components/RadioButton.svelte +110 -40
- package/src/components/SectionDivider.svelte +428 -74
- package/src/components/SegmentedControl.svelte +203 -0
- package/src/components/TabBar.svelte +146 -21
- package/src/components/Table.svelte +102 -0
- package/src/components/Tooltip.svelte +45 -19
- package/src/components/types.ts +51 -0
- package/src/data/google-fonts.json +75 -0
- package/src/lib/ColumnsOverlay.svelte +20 -7
- package/src/lib/LiveEditorOverlay.svelte +257 -78
- package/src/lib/columnsOverlay.ts +21 -17
- package/src/lib/componentConfig.test.ts +204 -0
- package/src/lib/componentConfigKeys.ts +19 -0
- package/src/lib/componentConfigService.ts +88 -0
- package/src/lib/copyPopover.ts +30 -0
- package/src/lib/cssVarSync.ts +59 -7
- package/src/lib/editorConfigStore.ts +0 -10
- package/src/lib/editorCore.ts +402 -0
- package/src/lib/editorKeybindings.ts +52 -0
- package/src/lib/editorPersistence.ts +106 -0
- package/src/lib/editorRenderer.ts +74 -0
- package/src/lib/editorStore.test.ts +328 -0
- package/src/lib/editorStore.ts +412 -0
- package/src/lib/editorTypes.ts +100 -0
- package/src/lib/editorViewStore.ts +55 -0
- package/src/lib/files/versionedFileResource.ts +140 -0
- package/src/lib/fontLoader.ts +130 -0
- package/src/lib/fontMigration.ts +140 -0
- package/src/lib/fontParse.ts +168 -0
- package/src/lib/index.ts +48 -30
- package/src/lib/lazyConfig.test.ts +54 -0
- package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
- package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
- package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
- package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
- package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
- package/src/lib/migrations/index.ts +93 -0
- package/src/lib/migrations/migrations.test.ts +341 -0
- package/src/lib/navLinkTypes.ts +1 -0
- package/src/lib/overlayState.ts +3 -0
- package/src/lib/paletteDerivation.ts +300 -0
- package/src/lib/parentRouteStore.ts +42 -0
- package/src/lib/parsers/globalRootBlock.ts +32 -0
- package/src/lib/presetService.ts +94 -0
- package/src/lib/router.ts +42 -10
- package/src/lib/scrollSection.ts +45 -0
- package/src/lib/slices/columns.ts +59 -0
- package/src/lib/slices/components.ts +362 -0
- package/src/lib/slices/domainVars.ts +15 -0
- package/src/lib/slices/fonts.ts +30 -0
- package/src/lib/slices/gradients.ts +153 -0
- package/src/lib/slices/overlays.ts +132 -0
- package/src/lib/slices/palettes.ts +26 -0
- package/src/lib/slices/shadows.ts +123 -0
- package/src/lib/storage.ts +88 -0
- package/src/lib/themeInit.ts +74 -0
- package/src/lib/themeService.ts +101 -0
- package/src/lib/themeTypes.ts +146 -0
- package/src/lib/tokenRegistry.ts +148 -0
- package/src/pages/ComponentEditorPage.svelte +384 -0
- package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
- package/src/pages/Editor.svelte +98 -0
- package/src/pages/Editor.svelte.d.ts +2 -0
- package/src/pages/EditorShell.svelte +348 -0
- package/src/styles/_padding.scss +34 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
- package/src/styles/fonts.css +22 -10
- package/src/styles/form-controls.css +14 -16
- package/src/styles/tokens.css +1322 -0
- package/src/styles/ui-editor.css +126 -0
- package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
- package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
- package/src/ui/EditorViewSwitcher.svelte +180 -0
- package/src/ui/FontStackEditor.svelte +360 -0
- package/src/ui/GradientEditor.svelte +461 -0
- package/src/ui/GradientStopPicker.svelte +74 -0
- package/src/ui/PaletteEditor.svelte +1590 -0
- package/src/ui/PaletteEditor.test.ts +108 -0
- package/src/ui/PresetFileManager.svelte +567 -0
- package/src/ui/ProjectFontsSection.svelte +645 -0
- package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
- package/src/{showcase → ui}/TextTab.svelte +27 -27
- package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
- package/src/ui/Toggle.svelte +108 -0
- package/src/ui/UICopyPopover.svelte +78 -0
- package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
- package/src/ui/UIFontFamilySelector.svelte +309 -0
- package/src/ui/UIFontSizeSelector.svelte +165 -0
- package/src/ui/UIFontWeightSelector.svelte +52 -0
- package/src/ui/UILineHeightSelector.svelte +47 -0
- package/src/ui/UILinkToggle.svelte +60 -0
- package/src/ui/UIOptionItem.svelte +74 -0
- package/src/ui/UIOptionList.svelte +27 -0
- package/src/ui/UIPaddingSelector.svelte +661 -0
- package/src/ui/UIPaletteSelector.svelte +1084 -0
- package/src/ui/UIRadio.svelte +72 -0
- package/src/ui/UIRadioGroup.svelte +59 -0
- package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
- package/src/ui/UITokenSelector.svelte +509 -0
- package/src/ui/UIVariantSelector.svelte +145 -0
- package/src/ui/VariablesTab.svelte +252 -0
- package/src/ui/index.ts +31 -0
- package/src/ui/keepInViewport.ts +84 -0
- package/src/ui/palette/GradientStopEditor.svelte +482 -0
- package/src/ui/palette/OverridesPanel.svelte +526 -0
- package/src/ui/palette/PaletteBase.svelte +165 -0
- package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
- package/src/ui/palette/paletteEditorState.ts +89 -0
- package/src/ui/sections/ColumnsSection.svelte +273 -0
- package/src/ui/sections/GradientsSection.svelte +147 -0
- package/src/ui/sections/OverlaysSection.svelte +670 -0
- package/src/ui/sections/ShadowsSection.svelte +1250 -0
- package/src/ui/sections/TokenScaleTable.svelte +332 -0
- package/src/ui/sections/tokenScales.ts +81 -0
- package/src/ui/variantScales.ts +108 -0
- package/src/components/DetailNav.svelte +0 -78
- package/src/components/Toggle.svelte +0 -86
- package/src/lib/tokenInit.ts +0 -29
- package/src/lib/tokenService.ts +0 -144
- package/src/lib/tokenTypes.ts +0 -45
- package/src/pages/Admin.svelte +0 -100
- package/src/pages/ShowcasePage.svelte +0 -144
- package/src/showcase/BackupBrowser.svelte +0 -617
- package/src/showcase/ComponentsTab.svelte +0 -105
- package/src/showcase/PaletteEditor.svelte +0 -2579
- package/src/showcase/PaletteSelector.svelte +0 -627
- package/src/showcase/TokenMap.svelte +0 -54
- package/src/showcase/VariablesTab.svelte +0 -2655
- package/src/showcase/VisualsTab.svelte +0 -231
- package/src/showcase/demos/BadgeDemo.svelte +0 -56
- package/src/showcase/demos/CardDemo.svelte +0 -50
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
- package/src/showcase/demos/DialogDemo.svelte +0 -42
- package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
- package/src/showcase/demos/NotificationDemo.svelte +0 -147
- package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
- package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
- package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
- package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
- package/src/showcase/demos/TabBarDemo.svelte +0 -58
- package/src/showcase/demos/TooltipDemo.svelte +0 -52
- package/src/showcase/editor.css +0 -93
- package/src/showcase/index.ts +0 -17
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +0 -97
- package/src/styles/fonts/Domine/README.txt +0 -66
- /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,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>
|