@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.
- package/.claude/skills/live-tokens-build-page/SKILL.md +6 -4
- package/README.md +27 -2
- package/package.json +9 -4
- package/src/editor/core/store/editorPersistence.ts +23 -1
- package/src/editor/docs/CodeBlock.svelte +92 -0
- package/src/editor/docs/Docs.svelte +658 -0
- package/src/editor/docs/Docs.svelte.d.ts +2 -0
- package/src/editor/docs/chapters.ts +44 -0
- package/src/editor/docs/content/01-overview.md +31 -0
- package/src/editor/docs/content/creating-components.md +40 -0
- package/src/editor/docs/content/editing-tokens.md +74 -0
- package/src/editor/docs/content/getting-started.md +67 -0
- package/src/editor/docs/content/themes-workflow.md +60 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +71 -13
- package/src/editor/pages/ComponentEditorPage.svelte +0 -11
- package/template/README.md +2 -1
|
@@ -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
|
|
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
|
-
-
|
|
22
|
-
-
|
|
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 `
|
|
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
|
|
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.
|
|
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, '&')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>');
|
|
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>
|