@motion-proto/live-tokens 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,7 @@ export default defineConfig({
41
41
 
42
42
  The `themeFileApi` plugin:
43
43
  - Seeds `src/live-tokens/data/themes/` with a default theme on first dev-server start.
44
- - Discovers components at `src/system/components/*.svelte` and seeds `src/live-tokens/data/component-configs/{comp}/default.json` from each component's `:global(:root)` block.
44
+ - Discovers components at `src/components/*.svelte` (and `src/system/components/*.svelte` for back-compat) and seeds `src/live-tokens/data/component-configs/{comp}/default.json` from each component's `:global(:root)` block.
45
45
  - Hosts the `/api/live-tokens/*` routes the editor uses to save and load themes + per-component configs.
46
46
  - Auto-injects `__PROJECT_ROOT__` for the overlay's "Page Source" link and `__LIVE_TOKENS_API_BASE__` so the client uses whatever `apiBase` you configured.
47
47
 
@@ -73,66 +73,79 @@ Resolution order, per folder: explicit `themeFileApi(opts)` argument > matching
73
73
  ### Bootstrap in `main.ts`
74
74
 
75
75
  ```ts
76
- import './styles/tokens.css';
77
- import {
78
- configureEditor,
79
- initializeTheme,
80
- initCssVarSync,
81
- initRouter,
82
- initColumnsOverlay,
83
- initEditorStore,
84
- } from '@motion-proto/live-tokens';
76
+ // main.ts
77
+ import '@motion-proto/live-tokens/app/tokens.css';
78
+ import './live-tokens/data/tokens.generated.css';
79
+ import '@motion-proto/live-tokens/app/fonts.css';
80
+ import { bootLiveTokens } from '@motion-proto/live-tokens';
85
81
  import App from './App.svelte';
86
- import { mount } from 'svelte';
87
82
 
88
- configureEditor({ storagePrefix: 'my-app-' });
89
-
90
- async function boot() {
91
- initCssVarSync();
92
- initRouter();
93
- initColumnsOverlay();
94
- initEditorStore();
95
- if (import.meta.env.DEV) {
96
- await initializeTheme();
97
- }
98
- mount(App, { target: document.getElementById('app')! });
99
- }
83
+ bootLiveTokens(App, '#app');
84
+ ```
85
+
86
+ `bootLiveTokens` orchestrates the editor's idempotent init hooks, fetches the
87
+ active theme in dev, optionally registers consumer-authored components, and
88
+ mounts the app. FontAwesome is side-effect-imported by the bootstrap (the dev
89
+ overlay always needs icons). The three token-CSS imports stay with the
90
+ consumer because order matters and `tokens.generated.css` is project-local.
91
+
92
+ Pass `components` to register consumer-authored editable components:
100
93
 
101
- boot();
94
+ ```ts
95
+ import MyWidgetEditor, { allTokens as myWidgetTokens } from './components/MyWidgetEditor.svelte';
96
+
97
+ bootLiveTokens(App, '#app', {
98
+ components: [{
99
+ id: 'mywidget',
100
+ label: 'My Widget',
101
+ icon: 'fas fa-magic',
102
+ sourceFile: 'src/components/MyWidget.svelte',
103
+ editorComponent: MyWidgetEditor,
104
+ schema: myWidgetTokens,
105
+ }],
106
+ });
102
107
  ```
103
108
 
104
- ### Mount overlay + editor pages
109
+ ### Mount routes with `<LiveTokensRouter>`
105
110
 
106
111
  ```svelte
107
112
  <!-- App.svelte -->
108
113
  <script lang="ts">
109
- import { LiveEditorOverlay, ColumnsOverlay } from '@motion-proto/live-tokens';
110
- import Editor from '@motion-proto/live-tokens/editor';
111
- import ComponentEditorPage from '@motion-proto/live-tokens/component-editor-page';
112
- import { route } from './router';
114
+ import { LiveTokensRouter } from '@motion-proto/live-tokens';
113
115
  </script>
114
116
 
115
- <LiveEditorOverlay
116
- navLinks={[
117
- { path: '/', label: 'Home', icon: 'fa-home' },
118
- { path: '/components', label: 'Components', icon: 'fa-puzzle-piece' },
119
- ]}
120
- pageSources={{
121
- '/': 'src/app/Home.svelte',
122
- }}
123
- />
124
- <ColumnsOverlay />
125
-
126
- {#if $route === '/editor'}
127
- <Editor />
128
- {:else if $route === '/components'}
129
- <ComponentEditorPage />
130
- {:else}
131
- <!-- your routes -->
132
- {/if}
117
+ <LiveTokensRouter pages={{
118
+ '/': { lazy: () => import('./Home.svelte'), label: 'Home', icon: 'fa-home', source: 'src/Home.svelte' },
119
+ }} />
133
120
  ```
134
121
 
135
- `<LiveEditorOverlay />` self-gates: it only renders in dev, never inside an iframe, and never on the editor route. No need to wrap in `{#if import.meta.env.DEV}` guards.
122
+ `<LiveTokensRouter>` owns the dev overlay (`<LiveEditorOverlay>` +
123
+ `<ColumnsOverlay>`), the editor routes (`/editor`, `/components`), the
124
+ in-app link-click interception, and the nav-rail/page-source plumbing the
125
+ overlay needs. Each entry in `pages` is one of your routes; entries with a
126
+ `label` appear in the overlay's nav rail. Pass pages as `lazy: () => import('./Page.svelte')`
127
+ so each page's stylesheet side-effects only evaluate when that route is
128
+ visited; pass `component: PageComponent` instead for an eagerly-imported
129
+ page. The editor routes are dispatched internally, so you don't have to
130
+ dynamic-import the library's editor pages yourself.
131
+
132
+ You can also relocate or disable a default editor route via the
133
+ `editorRoutes` prop: `<LiveTokensRouter pages={…} editorRoutes={{ editor: '/admin/editor', components: false }} />`.
134
+ Pass a string to move a route; pass `false` to remove the route entirely
135
+ (no dispatch and, for `components`, no auto-injected nav-rail entry).
136
+
137
+ The whole overlay surface is dev-only and tree-shakes out of production
138
+ builds — no `{#if import.meta.env.DEV}` guards needed.
139
+
140
+ ### Lower-level API (when you need it)
141
+
142
+ `bootLiveTokens` and `<LiveTokensRouter>` are convenience wrappers. The
143
+ individual init functions (`initCssVarSync`, `initRouter`,
144
+ `initColumnsOverlay`, `initEditorStore`, `initializeTheme`),
145
+ `<LiveEditorOverlay>`, `<ColumnsOverlay>`, and the editor page exports
146
+ (`@motion-proto/live-tokens/editor`,
147
+ `@motion-proto/live-tokens/component-editor-page`) all stay exported. Use
148
+ them directly if you need a custom shell or non-standard route dispatch.
136
149
 
137
150
  ### Use components
138
151
 
@@ -199,7 +212,7 @@ export default defineConfig({
199
212
  });
200
213
  ```
201
214
 
202
- No `css: 'injected'` workaround, no `optimizeDeps` excludes — `vite build` works as-is. (You'll want the full `themeFileApi` plugin from the Quick install section above when you're ready to persist edits to disk.)
215
+ No `css: 'injected'` workaround, no `optimizeDeps` excludes — `vite build` works as-is. (You'll want the full `themeFileApi` plugin and `bootLiveTokens` / `<LiveTokensRouter>` from the Quick install section above when you're ready to persist edits to disk and ship a real app.)
203
216
 
204
217
  ## Greenfield? Use the starter
205
218
 
@@ -216,25 +229,28 @@ Open http://localhost:5173 and replace `src/app/Home.svelte` with your content.
216
229
 
217
230
  ## Consumer-authored components
218
231
 
219
- The shipped components are first-party by default, but you can author your own and get the same real-time editing experience via `registerComponent()`. Co-locate runtime and editor files in `src/system/components/` and register the pair before mounting:
232
+ The shipped components are first-party by default, but you can author your own and get the same real-time editing experience. Co-locate runtime and editor files in `src/components/` (or `src/system/components/`, both are scanned by default) and pass them to `bootLiveTokens`:
220
233
 
221
234
  ```ts
222
235
  // src/main.ts
223
- import { registerComponent } from '@motion-proto/live-tokens';
224
- import MyWidgetEditor, { allTokens as myWidgetTokens } from './system/components/MyWidgetEditor.svelte';
225
-
226
- registerComponent({
227
- id: 'mywidget',
228
- label: 'My Widget',
229
- icon: 'fas fa-magic',
230
- sourceFile: 'src/system/components/MyWidget.svelte',
231
- editorComponent: MyWidgetEditor,
232
- schema: myWidgetTokens,
236
+ import { bootLiveTokens } from '@motion-proto/live-tokens';
237
+ import App from './App.svelte';
238
+ import MyWidgetEditor, { allTokens as myWidgetTokens } from './components/MyWidgetEditor.svelte';
239
+
240
+ bootLiveTokens(App, '#app', {
241
+ components: [{
242
+ id: 'mywidget',
243
+ label: 'My Widget',
244
+ icon: 'fas fa-magic',
245
+ sourceFile: 'src/components/MyWidget.svelte',
246
+ editorComponent: MyWidgetEditor,
247
+ schema: myWidgetTokens,
248
+ }],
233
249
  });
234
-
235
- // then mount(App, ...)
236
250
  ```
237
251
 
252
+ (`bootLiveTokens` calls `registerComponent` internally for each entry, gated on `import.meta.env.DEV` so the registration tree-shakes out of production builds. Call `registerComponent` directly if you need finer control over timing.)
253
+
238
254
  The component appears in the `/components` page under a **CUSTOM** group in the nav rail. Token rows, linked-block sharing, per-component config persistence, and reset-to-default work identically to the built-in set. All imports must come from `@motion-proto/live-tokens` or `@motion-proto/live-tokens/component-editor`; never deep-import from `src/`.
239
255
 
240
256
  ## Claude Code skills
@@ -635,7 +635,7 @@ function themeFileApi(opts) {
635
635
  const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? import_path3.default.resolve(opts.tokensGeneratedCssPath) : import_path3.default.join(dataDirs.dataDir, "tokens.generated.css");
636
636
  const FONTS_CSS_PATH = opts.fontsCssPath ? import_path3.default.resolve(opts.fontsCssPath) : import_path3.default.join(import_path3.default.dirname(CSS_PATH), "fonts.css");
637
637
  const API_BASE = opts.apiBase ?? "/api/live-tokens";
638
- const consumerComponentsDir = opts.componentsSrcDir ? import_path3.default.resolve(opts.componentsSrcDir) : import_path3.default.resolve("src/system/components");
638
+ const consumerComponentDirs = opts.componentsSrcDir ? [import_path3.default.resolve(opts.componentsSrcDir)] : [import_path3.default.resolve("src/components"), import_path3.default.resolve("src/system/components")];
639
639
  const packageComponentsDir = import_path3.default.resolve(
640
640
  import_path3.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
641
641
  "..",
@@ -643,8 +643,8 @@ function themeFileApi(opts) {
643
643
  "system",
644
644
  "components"
645
645
  );
646
- const COMPONENTS_SCAN_DIRS = [consumerComponentsDir];
647
- if (packageComponentsDir !== consumerComponentsDir && import_fs3.default.existsSync(packageComponentsDir)) {
646
+ const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
647
+ if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && import_fs3.default.existsSync(packageComponentsDir)) {
648
648
  COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
649
649
  }
650
650
  const LEGACY_PRESETS_DIR = import_path3.default.resolve("presets");
@@ -846,14 +846,24 @@ function themeFileApi(opts) {
846
846
  function componentNameFromFile(filePath) {
847
847
  return import_path3.default.basename(filePath, ".svelte").toLowerCase();
848
848
  }
849
+ function isThemeAwareComponent(filePath) {
850
+ try {
851
+ const source = import_fs3.default.readFileSync(filePath, "utf-8");
852
+ return /:global\(:root\)\s*\{/.test(source);
853
+ } catch {
854
+ return false;
855
+ }
856
+ }
849
857
  function listComponentSourcePaths() {
850
858
  const byName = /* @__PURE__ */ new Map();
851
859
  for (const dir of COMPONENTS_SCAN_DIRS) {
852
860
  if (!import_fs3.default.existsSync(dir)) continue;
853
861
  for (const f of import_fs3.default.readdirSync(dir)) {
854
862
  if (!f.endsWith(".svelte")) continue;
863
+ const full = import_path3.default.join(dir, f);
864
+ if (!isThemeAwareComponent(full)) continue;
855
865
  const name = componentNameFromFile(f);
856
- if (!byName.has(name)) byName.set(name, import_path3.default.join(dir, f));
866
+ if (!byName.has(name)) byName.set(name, full);
857
867
  }
858
868
  }
859
869
  return Array.from(byName.values());
@@ -1695,6 +1705,7 @@ function themeFileApi(opts) {
1695
1705
  const normalized = import_path3.default.resolve(ctx.file);
1696
1706
  if (!COMPONENTS_SCAN_DIRS.some((d) => normalized.startsWith(d))) return;
1697
1707
  if (!normalized.endsWith(".svelte")) return;
1708
+ if (!isThemeAwareComponent(normalized)) return;
1698
1709
  const comp = componentNameFromFile(normalized);
1699
1710
  generateDefaultConfig(comp, normalized);
1700
1711
  }
@@ -598,7 +598,7 @@ function themeFileApi(opts) {
598
598
  const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? path3.resolve(opts.tokensGeneratedCssPath) : path3.join(dataDirs.dataDir, "tokens.generated.css");
599
599
  const FONTS_CSS_PATH = opts.fontsCssPath ? path3.resolve(opts.fontsCssPath) : path3.join(path3.dirname(CSS_PATH), "fonts.css");
600
600
  const API_BASE = opts.apiBase ?? "/api/live-tokens";
601
- const consumerComponentsDir = opts.componentsSrcDir ? path3.resolve(opts.componentsSrcDir) : path3.resolve("src/system/components");
601
+ const consumerComponentDirs = opts.componentsSrcDir ? [path3.resolve(opts.componentsSrcDir)] : [path3.resolve("src/components"), path3.resolve("src/system/components")];
602
602
  const packageComponentsDir = path3.resolve(
603
603
  path3.dirname(fileURLToPath(import.meta.url)),
604
604
  "..",
@@ -606,8 +606,8 @@ function themeFileApi(opts) {
606
606
  "system",
607
607
  "components"
608
608
  );
609
- const COMPONENTS_SCAN_DIRS = [consumerComponentsDir];
610
- if (packageComponentsDir !== consumerComponentsDir && fs3.existsSync(packageComponentsDir)) {
609
+ const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
610
+ if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && fs3.existsSync(packageComponentsDir)) {
611
611
  COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
612
612
  }
613
613
  const LEGACY_PRESETS_DIR = path3.resolve("presets");
@@ -809,14 +809,24 @@ function themeFileApi(opts) {
809
809
  function componentNameFromFile(filePath) {
810
810
  return path3.basename(filePath, ".svelte").toLowerCase();
811
811
  }
812
+ function isThemeAwareComponent(filePath) {
813
+ try {
814
+ const source = fs3.readFileSync(filePath, "utf-8");
815
+ return /:global\(:root\)\s*\{/.test(source);
816
+ } catch {
817
+ return false;
818
+ }
819
+ }
812
820
  function listComponentSourcePaths() {
813
821
  const byName = /* @__PURE__ */ new Map();
814
822
  for (const dir of COMPONENTS_SCAN_DIRS) {
815
823
  if (!fs3.existsSync(dir)) continue;
816
824
  for (const f of fs3.readdirSync(dir)) {
817
825
  if (!f.endsWith(".svelte")) continue;
826
+ const full = path3.join(dir, f);
827
+ if (!isThemeAwareComponent(full)) continue;
818
828
  const name = componentNameFromFile(f);
819
- if (!byName.has(name)) byName.set(name, path3.join(dir, f));
829
+ if (!byName.has(name)) byName.set(name, full);
820
830
  }
821
831
  }
822
832
  return Array.from(byName.values());
@@ -1658,6 +1668,7 @@ function themeFileApi(opts) {
1658
1668
  const normalized = path3.resolve(ctx.file);
1659
1669
  if (!COMPONENTS_SCAN_DIRS.some((d) => normalized.startsWith(d))) return;
1660
1670
  if (!normalized.endsWith(".svelte")) return;
1671
+ if (!isThemeAwareComponent(normalized)) return;
1661
1672
  const comp = componentNameFromFile(normalized);
1662
1673
  generateDefaultConfig(comp, normalized);
1663
1674
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 6/7.",
6
6
  "keywords": [
@@ -0,0 +1,55 @@
1
+ /**
2
+ * One-call boot for a live-tokens consumer app.
3
+ *
4
+ * Bundles the five idempotent `init*` hooks that previously had to be
5
+ * orchestrated by every consumer (see README "Bootstrap in main.ts"), runs
6
+ * `initializeTheme` in dev, optionally registers custom components, and
7
+ * mounts the consumer's App at the target.
8
+ *
9
+ * CSS imports stay with the consumer — token CSS is order-sensitive
10
+ * (defaults → editor overrides → fonts) and the consumer's
11
+ * `tokens.generated.css` is per-project. FA icons are side-effect-imported
12
+ * here because the dev overlay always needs them and a consumer must not
13
+ * have to remember to import an icon font.
14
+ */
15
+ import '@fortawesome/fontawesome-free/css/all.min.css';
16
+
17
+ import { mount, type Component } from 'svelte';
18
+ import * as cssVarSync from './core/cssVarSync';
19
+ import * as router from './core/routing/router';
20
+ import * as columnsOverlay from './overlay/columnsOverlay';
21
+ import * as editorStore from './core/store/editorStore';
22
+ import { initializeTheme } from './core/themes/themeInit';
23
+ import { registerComponent, type RegisterComponentEntry } from './component-editor/registry';
24
+
25
+ export interface BootLiveTokensOptions {
26
+ /** Consumer-authored components to register with the editor before mount. Dev-only. */
27
+ components?: RegisterComponentEntry[];
28
+ }
29
+
30
+ export async function bootLiveTokens(
31
+ App: Component<any, any, any>,
32
+ target: string | Element,
33
+ opts: BootLiveTokensOptions = {},
34
+ ): Promise<ReturnType<typeof mount>> {
35
+ cssVarSync.init();
36
+ router.init();
37
+ columnsOverlay.init();
38
+ editorStore.init();
39
+
40
+ if (import.meta.env.DEV) {
41
+ if (opts.components) {
42
+ for (const entry of opts.components) {
43
+ registerComponent(entry);
44
+ }
45
+ }
46
+ await initializeTheme();
47
+ }
48
+
49
+ const targetEl =
50
+ typeof target === 'string' ? document.querySelector(target) : target;
51
+ if (!targetEl) {
52
+ throw new Error(`bootLiveTokens: target ${JSON.stringify(target)} not found`);
53
+ }
54
+ return mount(App, { target: targetEl });
55
+ }
@@ -1,6 +1,10 @@
1
1
  export { default as LiveEditorOverlay } from './overlay/LiveEditorOverlay.svelte';
2
2
  export type { NavLink } from './core/routing/navLinkTypes';
3
3
  export { default as ColumnsOverlay } from './overlay/ColumnsOverlay.svelte';
4
+ export { default as LiveTokensRouter } from './overlay/LiveTokensRouter.svelte';
5
+ export type { RouteEntry, EditorRouteOverrides } from './overlay/LiveTokensRouter.svelte';
6
+ export { bootLiveTokens } from './bootstrap';
7
+ export type { BootLiveTokensOptions } from './bootstrap';
4
8
 
5
9
  export { columnsVisible, toggleColumns, init as initColumnsOverlay } from './overlay/columnsOverlay';
6
10
  export { configureEditor, storageKey } from './core/store/editorConfig';
@@ -0,0 +1,165 @@
1
+ <script module lang="ts">
2
+ import type { Component } from 'svelte';
3
+
4
+ /**
5
+ * One entry per consumer route. `label` + `icon` make it appear in the
6
+ * overlay's nav rail; `source` powers the "Page Source" button. Omit
7
+ * `label` to keep the route reachable by URL but absent from the rail
8
+ * (matches the existing playground/unlisted route pattern).
9
+ *
10
+ * Provide either `component` (eager) or `lazy` (code-split). Use `lazy`
11
+ * for any page that side-effect-imports a stylesheet at the top of its
12
+ * module, so those imports only evaluate when the route is visited and
13
+ * don't leak into unrelated routes (most importantly the editor pages).
14
+ */
15
+ export interface RouteEntry {
16
+ component?: Component<any, any, any>;
17
+ lazy?: () => Promise<{ default: Component<any, any, any> }>;
18
+ label?: string;
19
+ icon?: string;
20
+ source?: string;
21
+ /** Hide the overlay's "Page Source" button on this route. */
22
+ hidePageSource?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Override the default editor routes (`/editor`, `/components`). Pass a
27
+ * string to relocate the route; pass `false` to disable it entirely (no
28
+ * dispatch and, for `components`, no auto-injected nav-rail entry).
29
+ */
30
+ export interface EditorRouteOverrides {
31
+ editor?: string | false;
32
+ components?: string | false;
33
+ }
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ import LiveEditorOverlay from './LiveEditorOverlay.svelte';
38
+ import ColumnsOverlay from './ColumnsOverlay.svelte';
39
+ import { route, navigate } from '../core/routing/router';
40
+
41
+ interface Props {
42
+ pages: Record<string, RouteEntry>;
43
+ editorRoutes?: EditorRouteOverrides;
44
+ }
45
+
46
+ let { pages, editorRoutes = {} }: Props = $props();
47
+
48
+ let editorEnabled = $derived(editorRoutes.editor !== false);
49
+ let componentsEnabled = $derived(editorRoutes.components !== false);
50
+ let editorPath = $derived(typeof editorRoutes.editor === 'string' ? editorRoutes.editor : '/editor');
51
+ let componentsPath = $derived(typeof editorRoutes.components === 'string' ? editorRoutes.components : '/components');
52
+
53
+ const isDev = import.meta.env.DEV;
54
+ let isEditor = $derived(isDev && editorEnabled && $route === editorPath);
55
+ let isComponentEditor = $derived(isDev && componentsEnabled && $route === componentsPath);
56
+
57
+ // Pages with a label show up in the nav rail, in declaration order. In dev,
58
+ // the components-editor route is auto-appended so it's reachable from every
59
+ // page (the convention every existing live-tokens consumer has settled on).
60
+ let navLinks = $derived([
61
+ ...Object.entries(pages)
62
+ .filter(([, e]) => !!e.label)
63
+ .map(([path, e]) => ({ path, label: e.label!, icon: e.icon ?? '' })),
64
+ ...(isDev && componentsEnabled
65
+ ? [{ path: componentsPath, label: 'Components', icon: 'fa-puzzle-piece' }]
66
+ : []),
67
+ ]);
68
+
69
+ let pageSources = $derived(
70
+ Object.fromEntries(
71
+ Object.entries(pages)
72
+ .filter(([, e]) => !!e.source)
73
+ .map(([path, e]) => [path, e.source!]),
74
+ ),
75
+ );
76
+
77
+ // Components-editor route always hides the page-source button (matches the
78
+ // convention from the original consumer pattern).
79
+ let hidePageSourceOn = $derived([
80
+ ...Object.entries(pages)
81
+ .filter(([, e]) => e.hidePageSource)
82
+ .map(([path]) => path),
83
+ ...(componentsEnabled ? [componentsPath] : []),
84
+ ]);
85
+
86
+ // Dispatch the current route. Editor pages are dynamically imported so they
87
+ // don't ship in non-editor route bundles. Consumer pages dispatch via
88
+ // `entry.lazy()` when provided (so each page's CSS side-effect imports stay
89
+ // out of other routes), or via the eagerly-imported `entry.component`.
90
+ let pagePromise = $derived.by(() => {
91
+ if (isEditor) return import('../pages/Editor.svelte');
92
+ if (isComponentEditor) return import('../pages/ComponentEditorPage.svelte');
93
+ const entry = pages[$route] ?? pages['/'];
94
+ if (!entry) return Promise.resolve({ default: null as unknown as Component<any, any, any> });
95
+ if (entry.lazy) return entry.lazy();
96
+ if (entry.component) return Promise.resolve({ default: entry.component });
97
+ return Promise.resolve({ default: null as unknown as Component<any, any, any> });
98
+ });
99
+
100
+ // In-app link interception: turn left-clicks on internal `/...` anchors into
101
+ // navigate() calls so router state updates without a full reload. Modifier
102
+ // keys (cmd/ctrl/shift/alt) pass through to the browser's default handling.
103
+ function handleClick(e: MouseEvent) {
104
+ const anchor = (e.target as HTMLElement).closest('a[href]');
105
+ if (!anchor) return;
106
+ const href = anchor.getAttribute('href');
107
+ if (!href || !href.startsWith('/')) return;
108
+ if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
109
+ e.preventDefault();
110
+ navigate(href);
111
+ }
112
+ </script>
113
+
114
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions, a11y_no_static_element_interactions -->
115
+ <div
116
+ class="lt-app"
117
+ class:is-editor={isEditor}
118
+ class:is-component-editor={isComponentEditor}
119
+ onclick={handleClick}
120
+ >
121
+ <LiveEditorOverlay {navLinks} {pageSources} {hidePageSourceOn} {editorPath} />
122
+ <ColumnsOverlay />
123
+
124
+ {#await pagePromise then m}
125
+ {@const PageComponent = m.default}
126
+ {#if PageComponent}
127
+ <PageComponent />
128
+ {/if}
129
+ {/await}
130
+ </div>
131
+
132
+ <style>
133
+ :global(html) {
134
+ scrollbar-gutter: stable;
135
+ }
136
+
137
+ :global(body) {
138
+ background: var(--page-bg);
139
+ background-attachment: var(--page-bg-attachment, fixed);
140
+ }
141
+
142
+ .lt-app {
143
+ width: 100%;
144
+ min-height: 100vh;
145
+ color: var(--text-primary);
146
+ padding-bottom: 12rem;
147
+ /* Set by LiveEditorOverlay when docked-open. Extends layout past the
148
+ fixed panel so the viewport can scroll to reveal hidden content. */
149
+ padding-right: var(--lt-overlay-scroll-pad, 0px);
150
+ }
151
+
152
+ .lt-app.is-editor {
153
+ padding-bottom: 0;
154
+ background: black;
155
+ }
156
+
157
+ .lt-app.is-component-editor {
158
+ min-height: 0;
159
+ height: 100vh;
160
+ padding-bottom: 0;
161
+ padding-right: 0;
162
+ background: black;
163
+ overflow: hidden;
164
+ }
165
+ </style>