@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 +78 -62
- package/dist-plugin/index.cjs +15 -4
- package/dist-plugin/index.js +15 -4
- package/package.json +1 -1
- package/src/editor/bootstrap.ts +55 -0
- package/src/editor/index.ts +4 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +165 -0
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
|
-
|
|
77
|
-
import
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
109
|
+
### Mount routes with `<LiveTokensRouter>`
|
|
105
110
|
|
|
106
111
|
```svelte
|
|
107
112
|
<!-- App.svelte -->
|
|
108
113
|
<script lang="ts">
|
|
109
|
-
import {
|
|
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
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
`<
|
|
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
|
|
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 {
|
|
224
|
-
import
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -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
|
|
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 = [
|
|
647
|
-
if (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,
|
|
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
|
}
|
package/dist-plugin/index.js
CHANGED
|
@@ -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
|
|
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 = [
|
|
610
|
-
if (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,
|
|
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
|
@@ -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
|
+
}
|
package/src/editor/index.ts
CHANGED
|
@@ -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>
|