@motion-proto/live-tokens 0.26.0 → 0.28.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: live-tokens-build-page
3
- description: Apply the @motion-proto/live-tokens project conventions when building a page: use shipped components from the catalogue, reference theme tokens (never hex/pixel literals), mount routes dynamically, register pageSources, and import site.css per-page. Use when the user asks to build / create / lay out a page, route, hero, marketing page, landing page, dashboard, settings screen, or pricing page; add a route; place / drop / use an existing component on a page; or assemble a screen from the live-tokens catalogue. For component-choice decisions, see live-tokens-pick-component. For authoring a brand-new component, see live-tokens-create-component.
3
+ description: Apply the @motion-proto/live-tokens project conventions when building a page: use shipped components from the catalogue, reference theme tokens (never hex/pixel literals), mount routes dynamically, register each route's page source, and import site.css per-page. Use when the user asks to build / create / lay out a page, route, hero, marketing page, landing page, dashboard, settings screen, or pricing page; add a route; place / drop / use an existing component on a page; or assemble a screen from the live-tokens catalogue. For component-choice decisions, see live-tokens-pick-component. For authoring a brand-new component, see live-tokens-create-component.
4
4
  ---
5
5
 
6
6
  # Building pages in a live-tokens project
@@ -18,8 +18,10 @@ To place children at specific page-column positions, span the parent grid (`grid
18
18
 
19
19
  ## Wiring
20
20
 
21
- - Mount routes dynamically in `App.svelte` with `$derived.by(() => import(...))`. Static top-level imports evaluate every page module at boot and leak page CSS into editor routes.
22
- - Register each route in `<LiveEditorOverlay pageSources={...} />` so the "Page Source" button opens the file in VS Code.
21
+ - Add the route the way `App.svelte` already wires routes:
22
+ - **`<LiveTokensRouter pages={...}>`** (the usual case): add a `pages` entry as `lazy: () => import('./YourPage.svelte')` with a `source: 'src/...'` (and a `label`/`icon` to show it in the nav rail). For a route you can't enumerate (a `/:id`, a path prefix, a gated page), add a `resolve(path) => RouteEntry | null` instead of a `pages` key; same entry shape, so `props` and `source` (hence "Page Source") work identically.
23
+ - **Manual `<LiveEditorOverlay>`**: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.
24
+ Either way use `lazy`, not a static top-level import: static imports evaluate every page module at boot and leak page CSS into the editor routes.
23
25
  - Import `site.css` from each page's `<script>` block, never from `main.ts` (would leak into editor routes).
24
26
 
25
27
  ## Avoid
@@ -32,4 +34,4 @@ To place children at specific page-column positions, span the parent grid (`grid
32
34
 
33
35
  ## Verify
34
36
 
35
- In dev: change a colour in `/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the `pageSources` entry). `ColumnsOverlay` (Cmd+G) shows content sitting inside `--columns-max-width`.
37
+ In dev: change a colour in `/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the route's `source`). `ColumnsOverlay` (Cmd+G) shows content sitting inside `--columns-max-width`.
package/README.md CHANGED
@@ -122,7 +122,7 @@ bootLiveTokens(App, '#app', {
122
122
  ```
123
123
 
124
124
  `<LiveTokensRouter>` owns the dev overlay (`<LiveEditorOverlay>` +
125
- `<ColumnsOverlay>`), the editor routes (`/editor`, `/components`), the
125
+ `<ColumnsOverlay>`), the editor routes (`/editor`, `/components`, `/docs`), the
126
126
  in-app link-click interception, and the nav-rail/page-source plumbing the
127
127
  overlay needs. Each entry in `pages` is one of your routes; entries with a
128
128
  `label` appear in the overlay's nav rail. Pass pages as `lazy: () => import('./Page.svelte')`
@@ -131,6 +131,28 @@ visited; pass `component: PageComponent` instead for an eagerly-imported
131
131
  page. The editor routes are dispatched internally, so you don't have to
132
132
  dynamic-import the library's editor pages yourself.
133
133
 
134
+ For routes you can't enumerate ahead of time (a `/:id` or `/:slug`, a path
135
+ prefix, or a page shown only when some condition holds), add a `resolve`
136
+ function from the current path to a `RouteEntry`; return `null` to fall
137
+ through. It's plain code, so params, prefixes, and gating are a regex and an
138
+ `if`, and the package ships no route syntax of its own. Resolution order is
139
+ `pages[path]`, then `resolve(path)`, then the `pages['/']` fallback, so adding
140
+ `resolve` never changes how existing `pages` entries match. A resolved entry
141
+ can carry `props`, letting one page component serve many paths (such as the
142
+ matched id), and its `source` gives the dynamic route a working "Page Source"
143
+ button just like a static one.
144
+
145
+ ```svelte
146
+ <LiveTokensRouter
147
+ pages={{ '/': { lazy: () => import('./Home.svelte'), label: 'Home' } }}
148
+ resolve={(path) => {
149
+ const m = path.match(/^\/module\/(.+)$/);
150
+ if (!m) return null;
151
+ return { lazy: () => import('./ModulePage.svelte'), props: { id: m[1] }, source: 'src/ModulePage.svelte' };
152
+ }}
153
+ />
154
+ ```
155
+
134
156
  You can also relocate or disable a default editor route via the
135
157
  `editorRoutes` prop: `<LiveTokensRouter pages={…} editorRoutes={{ editor: '/admin/editor', components: false }} />`.
136
158
  Pass a string to move a route; pass `false` to remove the route entirely
@@ -147,7 +169,10 @@ individual init functions (`initCssVarSync`, `initRouter`,
147
169
  `<LiveEditorOverlay>`, `<ColumnsOverlay>`, and the editor page exports
148
170
  (`@motion-proto/live-tokens/editor`,
149
171
  `@motion-proto/live-tokens/component-editor-page`) all stay exported. Use
150
- them directly if you need a custom shell or non-standard route dispatch.
172
+ them directly to build a custom shell: render arbitrary markup per route, host
173
+ a foreign matcher, or drive the overlay yourself. You do **not** need this for
174
+ dynamic or gated routes; reach for `resolve` above, which keeps the overlay,
175
+ nav rail, and page-source intact.
151
176
 
152
177
  ### Use components
153
178
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -65,6 +65,11 @@
65
65
  "svelte": "./src/editor/pages/ComponentEditorPage.svelte",
66
66
  "default": "./src/editor/pages/ComponentEditorPage.svelte"
67
67
  },
68
+ "./docs": {
69
+ "types": "./src/editor/docs/Docs.svelte.d.ts",
70
+ "svelte": "./src/editor/docs/Docs.svelte",
71
+ "default": "./src/editor/docs/Docs.svelte"
72
+ },
68
73
  "./components/*": {
69
74
  "svelte": "./src/system/components/*",
70
75
  "default": "./src/system/components/*"
@@ -110,8 +115,6 @@
110
115
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
111
116
  "@types/node": "^25.9.1",
112
117
  "happy-dom": "^20.9.0",
113
- "highlight.js": "^11.11.1",
114
- "marked": "^18.0.4",
115
118
  "sass": "^1.98.0",
116
119
  "svelte": "^5.55.5",
117
120
  "svelte-check": "^4.4.8",
@@ -121,6 +124,8 @@
121
124
  "vitest": "^4.1.4"
122
125
  },
123
126
  "dependencies": {
124
- "@fortawesome/fontawesome-free": "^7.2.0"
127
+ "@fortawesome/fontawesome-free": "^7.2.0",
128
+ "highlight.js": "^11.11.1",
129
+ "marked": "^18.0.4"
125
130
  }
126
131
  }
@@ -65,6 +65,28 @@ function migrateGradients(state: EditorState): EditorState {
65
65
  return { ...state, gradients: { tokens: makeDefaultGradients() } };
66
66
  }
67
67
 
68
+ // `hydrate` shallow-merges the persisted `components` bag over the default, so
69
+ // a slice serialized by an older build can lack fields added since (e.g.
70
+ // `config`, added with the two-field alias/config split). `componentsToVars`
71
+ // calls `Object.entries(slice.config)` unconditionally, so backfill the
72
+ // required fields and drop any non-object slice before the state reaches the
73
+ // renderer. Spread preserves optional fields like `unlinked`.
74
+ export function normalizeComponents(state: EditorState): EditorState {
75
+ const raw = state.components;
76
+ if (!raw || typeof raw !== 'object') return { ...state, components: {} };
77
+ const components: EditorState['components'] = {};
78
+ for (const [name, slice] of Object.entries(raw)) {
79
+ if (!slice || typeof slice !== 'object') continue;
80
+ components[name] = {
81
+ ...slice,
82
+ activeFile: typeof slice.activeFile === 'string' ? slice.activeFile : 'default',
83
+ aliases: slice.aliases && typeof slice.aliases === 'object' ? slice.aliases : {},
84
+ config: slice.config && typeof slice.config === 'object' ? slice.config : {},
85
+ };
86
+ }
87
+ return { ...state, components };
88
+ }
89
+
68
90
  export function hydrate(): void {
69
91
  // Corrupt state, missing key, or unavailable storage all return null;
70
92
  // the editor falls through to the empty default in that case.
@@ -73,7 +95,7 @@ export function hydrate(): void {
73
95
  // Shallow-merge onto default shape so older persisted state missing
74
96
  // newly-added domain fields still loads.
75
97
  const merged = { ...emptyStateFactory(), ...(parsed as object) } as EditorState;
76
- store.set(migrateGradients(merged));
98
+ store.set(normalizeComponents(migrateGradients(merged)));
77
99
  }
78
100
  // m13 fix: seed shadows from the DOM at hydrate time so the editor
79
101
  // captures the tokens.css baseline regardless of whether the user opens
@@ -0,0 +1,92 @@
1
+ <script lang="ts">
2
+ import hljs from 'highlight.js/lib/core';
3
+
4
+ interface Props {
5
+ lang?: string;
6
+ text: string;
7
+ }
8
+ let { lang, text }: Props = $props();
9
+
10
+ function escapeHtml(s: string): string {
11
+ return s
12
+ .replace(/&/g, '&amp;')
13
+ .replace(/</g, '&lt;')
14
+ .replace(/>/g, '&gt;');
15
+ }
16
+
17
+ let resolvedLang = $derived(lang && hljs.getLanguage(lang) ? lang : null);
18
+ let highlighted = $derived.by(() => {
19
+ if (!resolvedLang) return escapeHtml(text);
20
+ try {
21
+ return hljs.highlight(text, { language: resolvedLang, ignoreIllegals: true }).value;
22
+ } catch {
23
+ return escapeHtml(text);
24
+ }
25
+ });
26
+ </script>
27
+
28
+ <div class="code-block">
29
+ <pre><code class="hljs">{@html highlighted}</code></pre>
30
+ {#if lang}
31
+ <span class="lang-tag">{lang}</span>
32
+ {/if}
33
+ </div>
34
+
35
+ <style>
36
+ .code-block {
37
+ position: relative;
38
+ margin: 0 0 var(--space-20, 1.25rem);
39
+ background: var(--surface-neutral-lower, #162027);
40
+ border: var(--border-width-1, 1px) solid var(--border-neutral-subtle, #3a4146);
41
+ border-radius: var(--radius-xl, 0.5rem);
42
+ overflow: hidden;
43
+ }
44
+
45
+ pre {
46
+ margin: 0;
47
+ padding: var(--space-16, 1rem) var(--space-20, 1.25rem);
48
+ overflow-x: auto;
49
+ font-size: var(--font-size-sm, 0.875rem);
50
+ line-height: var(--line-height-md, 1.6);
51
+ }
52
+
53
+ pre code {
54
+ font-family: var(--font-mono, ui-monospace, monospace);
55
+ background: none;
56
+ color: var(--text-primary, #fff);
57
+ padding: 0;
58
+ border: 0;
59
+ white-space: pre;
60
+ }
61
+
62
+ .lang-tag {
63
+ position: absolute;
64
+ top: 0;
65
+ right: 0;
66
+ padding: var(--space-4, 0.25rem) var(--space-12, 0.75rem);
67
+ font-family: var(--font-mono, ui-monospace, monospace);
68
+ font-size: var(--font-size-xs, 0.75rem);
69
+ color: var(--text-tertiary, #7e8285);
70
+ text-transform: uppercase;
71
+ letter-spacing: var(--letter-spacing-wider, 0.06em);
72
+ background: color-mix(in srgb, var(--surface-neutral-lowest, #040c13) 60%, transparent);
73
+ border-left: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
74
+ border-bottom: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
75
+ border-radius: 0 var(--radius-xl, 0.5rem) 0 var(--radius-md, 0.25rem);
76
+ pointer-events: none;
77
+ }
78
+
79
+ /* highlight.js token overrides keyed to design-system tokens */
80
+ pre code :global(.hljs-keyword),
81
+ pre code :global(.hljs-built_in) { color: var(--text-brand, #ff75b1); }
82
+ pre code :global(.hljs-string) { color: var(--text-accent, #009d9a); }
83
+ pre code :global(.hljs-comment) { color: var(--text-tertiary, #7e8285); font-style: italic; }
84
+ pre code :global(.hljs-number) { color: var(--text-brand-secondary, #df2d88); }
85
+ pre code :global(.hljs-title),
86
+ pre code :global(.hljs-title.function_),
87
+ pre code :global(.hljs-attr) { color: var(--text-secondary, #c2cacf); }
88
+ pre code :global(.hljs-name),
89
+ pre code :global(.hljs-variable) { color: var(--text-primary, #fff); }
90
+ pre code :global(.hljs-tag),
91
+ pre code :global(.hljs-meta) { color: var(--text-accent-secondary, #1f7673); }
92
+ </style>