@motion-proto/live-tokens 0.25.1 → 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 +28 -3
- package/dist-plugin/index.cjs +120 -115
- package/dist-plugin/index.js +120 -115
- package/package.json +14 -5
- 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/src/editor/ui/ManifestFileManager.svelte +15 -5
- package/src/editor/ui/ThemeFileManager.svelte +6 -2
- package/src/live-tokens/data/manifests/default.json +35 -0
- package/src/live-tokens/data/themes/default.json +2295 -0
- package/src/live-tokens/data/tokens.generated.css +9 -9
- package/src/system/components/CodeSnippet.svelte +4 -0
- package/src/system/components/SideNavigation.svelte +13 -13
- package/template/README.md +2 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface Chapter {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* The user-facing guide, shipped with the package so consumers reference it in
|
|
7
|
+
the editor while building. Markdown lives in ./content/; the original
|
|
8
|
+
developer-reference chapters stay in the repo's docs/archive/ and don't
|
|
9
|
+
ship. */
|
|
10
|
+
export const chapters: Chapter[] = [
|
|
11
|
+
{ id: '01-overview', title: 'Overview' },
|
|
12
|
+
{ id: 'getting-started', title: 'Getting started' },
|
|
13
|
+
{ id: 'editing-tokens', title: 'Editing tokens' },
|
|
14
|
+
{ id: 'themes-workflow', title: 'Themes' },
|
|
15
|
+
{ id: 'creating-components', title: 'Creating components' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const chapterIds = chapters.map((c) => c.id);
|
|
19
|
+
|
|
20
|
+
/* Vite resolves this glob at build time. The `?raw` query loads each .md
|
|
21
|
+
file as a string, so the markdown lives next to the runtime page module
|
|
22
|
+
and reloads on edit via HMR. */
|
|
23
|
+
const docModules = import.meta.glob<string>(
|
|
24
|
+
'./content/*.md',
|
|
25
|
+
{ query: '?raw', import: 'default' },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export async function loadChapter(id: string): Promise<string> {
|
|
29
|
+
const key = `./content/${id}.md`;
|
|
30
|
+
const loader = docModules[key];
|
|
31
|
+
if (!loader) {
|
|
32
|
+
throw new Error(`Chapter not found: ${id} (expected at ${key}).`);
|
|
33
|
+
}
|
|
34
|
+
return loader();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function chapterNeighbours(id: string): { prev: Chapter | null; next: Chapter | null } {
|
|
38
|
+
const idx = chapterIds.indexOf(id);
|
|
39
|
+
if (idx < 0) return { prev: null, next: null };
|
|
40
|
+
return {
|
|
41
|
+
prev: idx > 0 ? chapters[idx - 1] : null,
|
|
42
|
+
next: idx < chapters.length - 1 ? chapters[idx + 1] : null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
Live Tokens is a design system for building Svelte microsites quickly. You
|
|
4
|
+
style your site by editing tokens and components in a live editor. When it looks right, you save the manifest and ship it.
|
|
5
|
+
|
|
6
|
+
## How it works
|
|
7
|
+
|
|
8
|
+
- The editor runs in your dev server, on top of your real pages. You style in
|
|
9
|
+
context, not in a separate sandbox.
|
|
10
|
+
- Every change updates a CSS variable, so the page repaints instantly. No
|
|
11
|
+
reload, no build step.
|
|
12
|
+
- Saving writes a small JSON file into your project. Shipping bakes your chosen
|
|
13
|
+
theme into a plain CSS file that the build bundles.
|
|
14
|
+
- The editor is dev-only. Production ships plain CSS variables and the
|
|
15
|
+
components you used, nothing else.
|
|
16
|
+
|
|
17
|
+
## What you can edit
|
|
18
|
+
|
|
19
|
+
- **Tokens**: the design-system primitives, colour palettes, type, spacing,
|
|
20
|
+
radius, shadow, and gradients, that apply across your whole site.
|
|
21
|
+
- **Components**: the package ships about 25 editable components (Button, Card,
|
|
22
|
+
Dialog, Table, and more).You style components by changing the tokens assigned to each property.
|
|
23
|
+
|
|
24
|
+
## Where to go next
|
|
25
|
+
|
|
26
|
+
- **[Getting started](getting-started.md)**: scaffold a project and make your
|
|
27
|
+
first edit.
|
|
28
|
+
- **[Editing tokens](editing-tokens.md)**: a tour of the editor.
|
|
29
|
+
- **[Themes](themes-workflow.md)**: save, switch, and ship.
|
|
30
|
+
- **[Creating components](creating-components.md)**: make your own components
|
|
31
|
+
editable.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Creating components
|
|
2
|
+
|
|
3
|
+
The package ships about 25 editable components. When you need one it doesn't
|
|
4
|
+
have, you can make your own Svelte component editable, so anyone using the
|
|
5
|
+
editor can re-point its colours, type, and spacing without touching code.
|
|
6
|
+
|
|
7
|
+
The simplest way is to ask Claude. The package bundles a Claude Code skill that
|
|
8
|
+
knows the conventions, writes the files, and checks the result for you.
|
|
9
|
+
|
|
10
|
+
## Install the skills
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @motion-proto/live-tokens setup-claude
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This copies the bundled skills into your project's `.claude/skills/`. Once
|
|
17
|
+
they're there, Claude Code picks them up automatically.
|
|
18
|
+
|
|
19
|
+
## Ask for a component
|
|
20
|
+
|
|
21
|
+
Describe what you want in plain English. Phrases like these trigger the skill:
|
|
22
|
+
|
|
23
|
+
- "Add a Toggle component to live-tokens"
|
|
24
|
+
- "Make this Svelte component editable in the live-tokens editor"
|
|
25
|
+
- "Create a Stat component with a value and a label"
|
|
26
|
+
|
|
27
|
+
Claude asks any clarifying questions it needs (which variants, which states,
|
|
28
|
+
which parts), then writes the component, registers it with the editor, and runs
|
|
29
|
+
its verification checklist. When it finishes, open `/components` to see your new
|
|
30
|
+
component in the editor and confirm everything works.
|
|
31
|
+
|
|
32
|
+
## What you get
|
|
33
|
+
|
|
34
|
+
- A runtime component whose editable properties default to your theme tokens.
|
|
35
|
+
- An editor entry that appears under **Custom** in the `/components` view.
|
|
36
|
+
- The naming and wiring handled for you, so the component fits the system.
|
|
37
|
+
|
|
38
|
+
Advanced authors who want to write a component by hand can read the naming and
|
|
39
|
+
state-model conventions shipped in the package
|
|
40
|
+
(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Editing tokens
|
|
2
|
+
|
|
3
|
+
A tour of the editor. The page behind it repaints on every change; saving
|
|
4
|
+
writes a theme file you can reload later.
|
|
5
|
+
|
|
6
|
+
The editor has two views:
|
|
7
|
+
|
|
8
|
+
- **Tokens**: the design-system primitives (colour, type, spacing, and so on).
|
|
9
|
+
They apply everywhere your site uses them.
|
|
10
|
+
- **Components**: per-component editors. Re-Assign what tokens a component uses
|
|
11
|
+
without changing the underlying system.
|
|
12
|
+
|
|
13
|
+
This page covers **Tokens**. For components, see
|
|
14
|
+
[Creating components](creating-components.md).
|
|
15
|
+
|
|
16
|
+
## Palettes
|
|
17
|
+
|
|
18
|
+
Most colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,
|
|
19
|
+
Success, Warning, Info, Danger, and a few more) has:
|
|
20
|
+
|
|
21
|
+
- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)
|
|
22
|
+
from it.
|
|
23
|
+
- **Curves.** Two curves shape how lightness and saturation fall off across the
|
|
24
|
+
ramp. Drag the handles to bias it darker, lighter, or more saturated.
|
|
25
|
+
- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't
|
|
26
|
+
land where you want.
|
|
27
|
+
|
|
28
|
+
Editing a palette base ripples through every colour that depends on it, in real
|
|
29
|
+
time. Colours use OKLCH, so the ramp stays perceptually even across hues
|
|
30
|
+
without muddy mid-tones.
|
|
31
|
+
|
|
32
|
+
## Type
|
|
33
|
+
|
|
34
|
+
- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an
|
|
35
|
+
inline `@font-face`. The font loads in the page as soon as you add it.
|
|
36
|
+
- **Stacks.** Named font cascades you reference by token, such as a display
|
|
37
|
+
stack and a body stack.
|
|
38
|
+
- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and
|
|
39
|
+
a numeric scale (100 to 900) for weight.
|
|
40
|
+
|
|
41
|
+
## Spacing, radius, shadow
|
|
42
|
+
|
|
43
|
+
Numeric scales with a slider per step.
|
|
44
|
+
|
|
45
|
+
- **Spacing**: the padding, gap, and margin scale.
|
|
46
|
+
- **Radius**: none through full.
|
|
47
|
+
- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked
|
|
48
|
+
shadows supported.
|
|
49
|
+
|
|
50
|
+
Change a step and every element using it repaints.
|
|
51
|
+
|
|
52
|
+
## Overlays and gradients
|
|
53
|
+
|
|
54
|
+
- **Overlays** are translucent tints layered over surfaces, like the subtle
|
|
55
|
+
tint a card gets on hover. Set a colour and opacity per state.
|
|
56
|
+
- **Gradients** are reusable gradient tokens with a stop list and direction, for
|
|
57
|
+
hero panels and accent backgrounds.
|
|
58
|
+
|
|
59
|
+
## Columns
|
|
60
|
+
|
|
61
|
+
The page-grid overlay. Set column count, gutter, and outer margin, and toggle
|
|
62
|
+
the visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow
|
|
63
|
+
live.
|
|
64
|
+
|
|
65
|
+
## Saving
|
|
66
|
+
|
|
67
|
+
The editor saves to your browser continuously, so work survives a reload
|
|
68
|
+
mid-edit. **Save** is a separate step: it writes a named theme file under
|
|
69
|
+
`src/live-tokens/data/themes/`.
|
|
70
|
+
|
|
71
|
+
The header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`) and a file
|
|
72
|
+
menu for New, Save, Save as, Switch, and Delete. You can keep many themes side
|
|
73
|
+
by side; one is active at a time. See [Themes](themes-workflow.md) for the full
|
|
74
|
+
lifecycle.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
Scaffold a live token site in a moments. You need Node 20 or later, a
|
|
4
|
+
package manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.
|
|
5
|
+
|
|
6
|
+
## Scaffold a new app
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm create @motion-proto/live-tokens@latest my-app
|
|
10
|
+
cd my-app
|
|
11
|
+
npm install
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Open the URL Vite prints (usually `http://localhost:5173`). You get a
|
|
16
|
+
one-page Svelte + Vite app that depends on the published package, with the
|
|
17
|
+
editor wired up and the full component set ready to import.
|
|
18
|
+
|
|
19
|
+
`npx @motion-proto/live-tokens create my-app` runs the same scaffold without
|
|
20
|
+
the initialiser package.
|
|
21
|
+
|
|
22
|
+
### What the scaffold gives you
|
|
23
|
+
|
|
24
|
+
Every editable file lives under `src/` and is committed, so `npm install` and
|
|
25
|
+
version upgrades never touch your styles. The package code stays in
|
|
26
|
+
`node_modules`.
|
|
27
|
+
|
|
28
|
+
| Path | What it is |
|
|
29
|
+
|------|------------|
|
|
30
|
+
| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |
|
|
31
|
+
| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds the dev-only `/editor`, `/components`, and `/docs` routes. |
|
|
32
|
+
| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |
|
|
33
|
+
| `src/styles/site.css` | Themed page typography, yours to edit. |
|
|
34
|
+
|
|
35
|
+
## Your first edit
|
|
36
|
+
|
|
37
|
+
1. Run `npm run dev` and open the home page.
|
|
38
|
+
2. Click **Open Token Editor**, or visit `/editor`. The editor opens beside
|
|
39
|
+
the page.
|
|
40
|
+
3. Open **Palettes**, pick **Brand**, and change the base hex. The page
|
|
41
|
+
repaints as you type.
|
|
42
|
+
4. Open the file menu and choose **Save as**. A theme appears as JSON under
|
|
43
|
+
`src/live-tokens/data/themes/`.
|
|
44
|
+
5. Reload. Your saved theme is the active theme, so the page returns as you
|
|
45
|
+
left it.
|
|
46
|
+
|
|
47
|
+
## What you just changed
|
|
48
|
+
|
|
49
|
+
Every edit sets a CSS custom property on `:root`. Your components read those
|
|
50
|
+
properties through `var(--...)`. There is no token build step and no
|
|
51
|
+
preprocessor rewriting your code: the page renders against plain CSS variables
|
|
52
|
+
the editor swaps live.
|
|
53
|
+
|
|
54
|
+
To ship, promote a theme to production in the editor. That bakes the theme's
|
|
55
|
+
variables into `src/live-tokens/data/tokens.generated.css`, which your build
|
|
56
|
+
bundles alongside `tokens.css`. The editor itself never reaches production.
|
|
57
|
+
|
|
58
|
+
Already have a Svelte 5 + Vite app? The
|
|
59
|
+
[README](https://github.com/motionproto/live-tokens#readme) covers installing
|
|
60
|
+
into an existing project.
|
|
61
|
+
|
|
62
|
+
## Where to go next
|
|
63
|
+
|
|
64
|
+
- **[Editing tokens](editing-tokens.md)**: a tour of the editor.
|
|
65
|
+
- **[Themes](themes-workflow.md)**: save, switch, and ship.
|
|
66
|
+
- **[Creating components](creating-components.md)**: make your own component
|
|
67
|
+
editable.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Themes
|
|
2
|
+
|
|
3
|
+
Save your work, switch between themes, and ship one to production.
|
|
4
|
+
|
|
5
|
+
## How themes work
|
|
6
|
+
|
|
7
|
+
- **Your live edits** are what the page shows right now. They save to your
|
|
8
|
+
browser automatically and survive a reload, but they are not yet a file.
|
|
9
|
+
- **A saved theme** is a named JSON file in `src/live-tokens/data/themes/`. You
|
|
10
|
+
create one with **Save as**.
|
|
11
|
+
- **The active theme** is the saved theme the page loads at startup. Exactly one
|
|
12
|
+
at a time.
|
|
13
|
+
- **The production theme** is the one that ships. Promoting sets it.
|
|
14
|
+
|
|
15
|
+
## Saving
|
|
16
|
+
|
|
17
|
+
In the editor header:
|
|
18
|
+
|
|
19
|
+
- **Save** updates the current theme.
|
|
20
|
+
- **Save as** names a new theme. Use it for your first save and for forking.
|
|
21
|
+
|
|
22
|
+
Names are tidied to lowercase with underscores, so "My Brand!" becomes
|
|
23
|
+
`my_brand`. There is a built-in `default` theme you can always return to; the
|
|
24
|
+
editor never overwrites it.
|
|
25
|
+
|
|
26
|
+
## Switching
|
|
27
|
+
|
|
28
|
+
The file menu lists every saved theme. Pick one to make it active; the page
|
|
29
|
+
reloads with it applied. Your current edits are saved to the previous theme
|
|
30
|
+
first, so you don't lose work.
|
|
31
|
+
|
|
32
|
+
## Shipping
|
|
33
|
+
|
|
34
|
+
**Promote to production** is the "ship it" step. It bakes the theme's variables
|
|
35
|
+
into `src/live-tokens/data/tokens.generated.css`, which your build bundles
|
|
36
|
+
alongside `tokens.css`. Fonts regenerate to match.
|
|
37
|
+
|
|
38
|
+
Production builds (`npm run build`) ship only that plain CSS and your
|
|
39
|
+
components. No editor, no JSON loading, no runtime indirection. If you save
|
|
40
|
+
while the production theme is active, the generated CSS updates immediately,
|
|
41
|
+
with no separate promote step.
|
|
42
|
+
|
|
43
|
+
## Manifests
|
|
44
|
+
|
|
45
|
+
A **manifest** bundles one theme plus a config for each component into a single
|
|
46
|
+
named set. Useful when you run several brands and want each to apply its theme
|
|
47
|
+
and component tweaks in one move. There is a protected default and an active
|
|
48
|
+
manifest; applying one swaps everything at once.
|
|
49
|
+
|
|
50
|
+
## Keeping your work safe
|
|
51
|
+
|
|
52
|
+
Everything under `src/live-tokens/data/` is plain JSON, so commit it. Themes
|
|
53
|
+
show up as readable diffs you can review per branch. There are no automatic
|
|
54
|
+
backups: git is your safety net. To experiment freely, **Save as** a new name
|
|
55
|
+
first, then edit.
|
|
56
|
+
|
|
57
|
+
## Where to go next
|
|
58
|
+
|
|
59
|
+
- **[Creating components](creating-components.md)**: make your own components
|
|
60
|
+
editable in the same editor.
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
* for any page that side-effect-imports a stylesheet at the top of its
|
|
12
12
|
* module, so those imports only evaluate when the route is visited and
|
|
13
13
|
* don't leak into unrelated routes (most importantly the editor pages).
|
|
14
|
+
*
|
|
15
|
+
* `props` lets one page component serve many paths: a `resolve()` match can
|
|
16
|
+
* hand the page the matched segment (an id or slug) so a single component
|
|
17
|
+
* renders each dynamic route.
|
|
14
18
|
*/
|
|
15
19
|
export interface RouteEntry {
|
|
16
20
|
component?: Component<any, any, any>;
|
|
17
21
|
lazy?: () => Promise<{ default: Component<any, any, any> }>;
|
|
22
|
+
props?: Record<string, unknown>;
|
|
18
23
|
label?: string;
|
|
19
24
|
icon?: string;
|
|
20
25
|
source?: string;
|
|
@@ -23,13 +28,37 @@
|
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
|
-
* Override the default editor routes (`/editor`, `/components`).
|
|
27
|
-
* string to relocate the route; pass `false` to disable it entirely
|
|
28
|
-
* dispatch and, for `components`, no auto-injected nav-rail entry).
|
|
31
|
+
* Override the default editor routes (`/editor`, `/components`, `/docs`).
|
|
32
|
+
* Pass a string to relocate the route; pass `false` to disable it entirely
|
|
33
|
+
* (no dispatch and, for `components`/`docs`, no auto-injected nav-rail entry).
|
|
29
34
|
*/
|
|
30
35
|
export interface EditorRouteOverrides {
|
|
31
36
|
editor?: string | false;
|
|
32
37
|
components?: string | false;
|
|
38
|
+
docs?: string | false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a path to the single entry that renders it. Precedence, after the
|
|
43
|
+
* package-owned routes (`/editor`, `/components`, `/docs`) have already been
|
|
44
|
+
* matched by the component:
|
|
45
|
+
*
|
|
46
|
+
* 1. `pages[route]` — exact static match.
|
|
47
|
+
* 2. `resolve(route)` — consumer code, where params / prefixes / gating
|
|
48
|
+
* live; returning `null` means "not mine" and resolution falls through.
|
|
49
|
+
* 3. `pages['/']` — final fallback (return your own entry from `resolve`
|
|
50
|
+
* for a real 404 instead).
|
|
51
|
+
*
|
|
52
|
+
* With only `pages` passed this is exactly today's `pages[route] ?? pages['/']`.
|
|
53
|
+
* One resolved entry drives both dispatch and "Page Source", so a dynamic
|
|
54
|
+
* route's source can never desync from its page.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveRoute(
|
|
57
|
+
pages: Record<string, RouteEntry>,
|
|
58
|
+
resolve: ((route: string) => RouteEntry | null) | undefined,
|
|
59
|
+
route: string,
|
|
60
|
+
): RouteEntry | null {
|
|
61
|
+
return pages[route] ?? resolve?.(route) ?? pages['/'] ?? null;
|
|
33
62
|
}
|
|
34
63
|
</script>
|
|
35
64
|
|
|
@@ -40,23 +69,42 @@
|
|
|
40
69
|
|
|
41
70
|
interface Props {
|
|
42
71
|
pages: Record<string, RouteEntry>;
|
|
72
|
+
/**
|
|
73
|
+
* Compute an entry for paths not in `pages` — this is where params,
|
|
74
|
+
* prefixes, and conditional gating live. See `resolveRoute` for the full
|
|
75
|
+
* precedence: `pages` wins over `resolve`, and `pages['/']` is the final
|
|
76
|
+
* fallback.
|
|
77
|
+
*/
|
|
78
|
+
resolve?: (route: string) => RouteEntry | null;
|
|
43
79
|
editorRoutes?: EditorRouteOverrides;
|
|
44
80
|
}
|
|
45
81
|
|
|
46
|
-
let { pages, editorRoutes = {} }: Props = $props();
|
|
82
|
+
let { pages, resolve, editorRoutes = {} }: Props = $props();
|
|
47
83
|
|
|
48
84
|
let editorEnabled = $derived(editorRoutes.editor !== false);
|
|
49
85
|
let componentsEnabled = $derived(editorRoutes.components !== false);
|
|
86
|
+
let docsEnabled = $derived(editorRoutes.docs !== false);
|
|
50
87
|
let editorPath = $derived(typeof editorRoutes.editor === 'string' ? editorRoutes.editor : '/editor');
|
|
51
88
|
let componentsPath = $derived(typeof editorRoutes.components === 'string' ? editorRoutes.components : '/components');
|
|
89
|
+
let docsPath = $derived(typeof editorRoutes.docs === 'string' ? editorRoutes.docs : '/docs');
|
|
52
90
|
|
|
53
91
|
const isDev = import.meta.env.DEV;
|
|
54
92
|
let isEditor = $derived(isDev && editorEnabled && $route === editorPath);
|
|
55
93
|
let isComponentEditor = $derived(isDev && componentsEnabled && $route === componentsPath);
|
|
94
|
+
let isDocs = $derived(isDev && docsEnabled && $route === docsPath);
|
|
95
|
+
|
|
96
|
+
// The single entry that renders the current route. Owned routes are handled
|
|
97
|
+
// by the isEditor/isComponentEditor/isDocs branches, so they resolve to null
|
|
98
|
+
// here; everything else flows through resolveRoute and drives both dispatch
|
|
99
|
+
// (component + props) and page-source from this one value.
|
|
100
|
+
let resolvedEntry = $derived(
|
|
101
|
+
isEditor || isComponentEditor || isDocs ? null : resolveRoute(pages, resolve, $route),
|
|
102
|
+
);
|
|
56
103
|
|
|
57
104
|
// Pages with a label show up in the nav rail, in declaration order. In dev,
|
|
58
|
-
// the components-editor
|
|
59
|
-
// page
|
|
105
|
+
// the components-editor and docs routes are auto-appended so they're reachable
|
|
106
|
+
// from every page. Docs ship from the package, so consumers reference the
|
|
107
|
+
// guide in the editor while building without vendoring a copy.
|
|
60
108
|
let navLinks = $derived([
|
|
61
109
|
...Object.entries(pages)
|
|
62
110
|
.filter(([, e]) => !!e.label)
|
|
@@ -64,23 +112,32 @@
|
|
|
64
112
|
...(isDev && componentsEnabled
|
|
65
113
|
? [{ path: componentsPath, label: 'Components', icon: 'fa-puzzle-piece' }]
|
|
66
114
|
: []),
|
|
115
|
+
...(isDev && docsEnabled
|
|
116
|
+
? [{ path: docsPath, label: 'Docs', icon: 'fa-book' }]
|
|
117
|
+
: []),
|
|
67
118
|
]);
|
|
68
119
|
|
|
69
|
-
|
|
70
|
-
|
|
120
|
+
// Static-page sources, plus the resolved entry's source for the current
|
|
121
|
+
// route — so a resolve()-matched dynamic route gets "Page Source" too, keyed
|
|
122
|
+
// on the live path ($route) the overlay looks up.
|
|
123
|
+
let pageSources = $derived({
|
|
124
|
+
...Object.fromEntries(
|
|
71
125
|
Object.entries(pages)
|
|
72
126
|
.filter(([, e]) => !!e.source)
|
|
73
127
|
.map(([path, e]) => [path, e.source!]),
|
|
74
128
|
),
|
|
75
|
-
|
|
129
|
+
...(resolvedEntry?.source ? { [$route]: resolvedEntry.source } : {}),
|
|
130
|
+
});
|
|
76
131
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
132
|
+
// The package-owned components and docs routes hide the page-source button
|
|
133
|
+
// (no consumer source file backs them).
|
|
79
134
|
let hidePageSourceOn = $derived([
|
|
80
135
|
...Object.entries(pages)
|
|
81
136
|
.filter(([, e]) => e.hidePageSource)
|
|
82
137
|
.map(([path]) => path),
|
|
138
|
+
...(resolvedEntry?.hidePageSource ? [$route] : []),
|
|
83
139
|
...(componentsEnabled ? [componentsPath] : []),
|
|
140
|
+
...(docsEnabled ? [docsPath] : []),
|
|
84
141
|
]);
|
|
85
142
|
|
|
86
143
|
// Dispatch the current route. Editor pages are dynamically imported so they
|
|
@@ -90,7 +147,8 @@
|
|
|
90
147
|
let pagePromise = $derived.by(() => {
|
|
91
148
|
if (isEditor) return import('../pages/Editor.svelte');
|
|
92
149
|
if (isComponentEditor) return import('../pages/ComponentEditorPage.svelte');
|
|
93
|
-
|
|
150
|
+
if (isDocs) return import('../docs/Docs.svelte');
|
|
151
|
+
const entry = resolvedEntry;
|
|
94
152
|
if (!entry) return Promise.resolve({ default: null as unknown as Component<any, any, any> });
|
|
95
153
|
if (entry.lazy) return entry.lazy();
|
|
96
154
|
if (entry.component) return Promise.resolve({ default: entry.component });
|
|
@@ -124,7 +182,7 @@
|
|
|
124
182
|
{#await pagePromise then m}
|
|
125
183
|
{@const PageComponent = m.default}
|
|
126
184
|
{#if PageComponent}
|
|
127
|
-
<PageComponent />
|
|
185
|
+
<PageComponent {...resolvedEntry?.props ?? {}} />
|
|
128
186
|
{/if}
|
|
129
187
|
{/await}
|
|
130
188
|
</div>
|
|
@@ -20,11 +20,6 @@
|
|
|
20
20
|
|
|
21
21
|
let drawerOpen = $state(true);
|
|
22
22
|
|
|
23
|
-
// Demo page is statically imported from `./Demo.svelte` in App.svelte; the
|
|
24
|
-
// glob resolves to an empty object if the file has been deleted, in which
|
|
25
|
-
// case we hide the demo option from the page-switcher.
|
|
26
|
-
const demoExists = Object.keys(import.meta.glob('./Demo.svelte')).length > 0;
|
|
27
|
-
|
|
28
23
|
let pageMenuOpen = $state(false);
|
|
29
24
|
let pageMenuRoot: HTMLElement | undefined = $state();
|
|
30
25
|
|
|
@@ -138,12 +133,6 @@
|
|
|
138
133
|
<i class="fas fa-home"></i>
|
|
139
134
|
<span>Main site</span>
|
|
140
135
|
</button>
|
|
141
|
-
{#if demoExists}
|
|
142
|
-
<button class="page-menu-item" role="menuitem" onclick={() => selectPage('/demo')}>
|
|
143
|
-
<i class="fas fa-box-open"></i>
|
|
144
|
-
<span>Demo page</span>
|
|
145
|
-
</button>
|
|
146
|
-
{/if}
|
|
147
136
|
</div>
|
|
148
137
|
{/if}
|
|
149
138
|
</div>
|
|
@@ -49,11 +49,7 @@
|
|
|
49
49
|
const active = await getActiveManifest();
|
|
50
50
|
if (active) {
|
|
51
51
|
activeFileName = active._fileName ?? 'default';
|
|
52
|
-
|
|
53
|
-
// name field says (older default.json files may have "Default Preset").
|
|
54
|
-
currentDisplayName = activeFileName === 'default'
|
|
55
|
-
? 'Default'
|
|
56
|
-
: (active.name ?? activeFileName);
|
|
52
|
+
currentDisplayName = active.name ?? activeFileName;
|
|
57
53
|
const meta = (await listManifests()).find((f) => f.fileName === activeFileName) ?? null;
|
|
58
54
|
activeManifest.set(meta);
|
|
59
55
|
}
|
|
@@ -81,8 +77,21 @@
|
|
|
81
77
|
refreshActive();
|
|
82
78
|
});
|
|
83
79
|
|
|
80
|
+
// A manifest snapshots saved file pointers, not the editor's live state. Warn
|
|
81
|
+
// before capturing if there are unsaved theme/component edits, since those
|
|
82
|
+
// won't make it into the manifest until they're saved (and adopted).
|
|
83
|
+
function confirmUnsavedExclusion(): boolean {
|
|
84
|
+
if (!editorDirty) return true;
|
|
85
|
+
return window.confirm(
|
|
86
|
+
'You have unsaved theme or component changes. A manifest captures only saved, ' +
|
|
87
|
+
'adopted files, so these edits will not be included. Save and adopt them first ' +
|
|
88
|
+
'to capture them. Save the manifest anyway?',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
async function handleSave() {
|
|
85
93
|
if (activeIsProtected) return;
|
|
94
|
+
if (!confirmUnsavedExclusion()) return;
|
|
86
95
|
saveStatus = 'saving';
|
|
87
96
|
try {
|
|
88
97
|
await saveActiveManifest(currentDisplayName);
|
|
@@ -94,6 +103,7 @@
|
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
function openSaveAs() {
|
|
106
|
+
if (!confirmUnsavedExclusion()) return;
|
|
97
107
|
showFileList = false;
|
|
98
108
|
saveAsDialog = true;
|
|
99
109
|
}
|
|
@@ -139,8 +139,6 @@
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
async function handleApplyToProduction() {
|
|
142
|
-
if (prodIsInSync) return;
|
|
143
|
-
|
|
144
142
|
// Dirty edits on the protected default theme can't be saved to that file,
|
|
145
143
|
// and adopting the on-disk default would silently strand the user's
|
|
146
144
|
// changes. Open the theme SaveAs dialog and resume Adopt after save.
|
|
@@ -150,8 +148,14 @@
|
|
|
150
148
|
return;
|
|
151
149
|
}
|
|
152
150
|
|
|
151
|
+
// Already adopted with nothing unsaved to push.
|
|
152
|
+
if (prodIsInSync && !$dirty) return;
|
|
153
|
+
|
|
153
154
|
prodApplyStatus = 'applying';
|
|
154
155
|
try {
|
|
156
|
+
// Flush unsaved edits to the active theme file first, so production adopts
|
|
157
|
+
// the current editor state rather than a stale on-disk snapshot.
|
|
158
|
+
if ($dirty) await onsave?.({ fileName: $activeFileName, displayName: currentDisplayName });
|
|
155
159
|
await setProductionFile($activeFileName);
|
|
156
160
|
await refreshProduction();
|
|
157
161
|
bumpProductionRevision();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Default",
|
|
3
|
+
"createdAt": "2026-05-10T18:15:30.815Z",
|
|
4
|
+
"updatedAt": "2026-06-01T09:55:04.250Z",
|
|
5
|
+
"theme": "default",
|
|
6
|
+
"componentConfigs": {
|
|
7
|
+
"badge": "default",
|
|
8
|
+
"button": "default",
|
|
9
|
+
"callout": "default",
|
|
10
|
+
"card": "default",
|
|
11
|
+
"collapsiblesection": "default",
|
|
12
|
+
"cornerbadge": "default",
|
|
13
|
+
"dialog": "default",
|
|
14
|
+
"floatingtokentags": "default",
|
|
15
|
+
"image": "default",
|
|
16
|
+
"inlineeditactions": "default",
|
|
17
|
+
"input": "default",
|
|
18
|
+
"menuselect": "default",
|
|
19
|
+
"notification": "default",
|
|
20
|
+
"panel": "default",
|
|
21
|
+
"progressbar": "default",
|
|
22
|
+
"radiobutton": "default",
|
|
23
|
+
"sectiondivider": "default",
|
|
24
|
+
"segmentedcontrol": "default",
|
|
25
|
+
"sidenavigation": "default",
|
|
26
|
+
"stat": "default",
|
|
27
|
+
"stateditor": "default",
|
|
28
|
+
"tabbar": "default",
|
|
29
|
+
"table": "default",
|
|
30
|
+
"toggle": "default",
|
|
31
|
+
"tooltip": "default",
|
|
32
|
+
"imagelightbox": "default",
|
|
33
|
+
"codesnippet": "default"
|
|
34
|
+
}
|
|
35
|
+
}
|