@motion-proto/live-tokens 0.34.0 → 0.36.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.
@@ -8,7 +8,7 @@ description: Apply the @motion-proto/live-tokens project conventions when buildi
8
8
  Two rules above all else:
9
9
 
10
10
  1. **Use a shipped component if one fits.** Import from `@motion-proto/live-tokens/components/<Name>.svelte`. See [[live-tokens-pick-component]] for the catalogue and the confusing-pair decisions. Author custom markup only when nothing fits, and then consider [[live-tokens-create-component]] so the new piece is editable too.
11
- 2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`. No hex literals. No pixel literals. A change in `/editor` should repaint your page.
11
+ 2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`. No hex literals. No pixel literals. A change in `/live-tokens/editor` should repaint your page.
12
12
 
13
13
  ## Layout
14
14
 
@@ -28,10 +28,10 @@ To place children at specific page-column positions, span the parent grid (`grid
28
28
 
29
29
  - Hex or pixel literals in page CSS.
30
30
  - Hardcoded column counts (`repeat(10, 1fr)`). Use `repeat(var(--columns-count), 1fr)`.
31
- - Utility classes overriding shipped components. Extend via the `/components` editor instead.
31
+ - Utility classes overriding shipped components. Extend via the `/live-tokens/components` editor instead.
32
32
  - Deep imports from `node_modules/@motion-proto/live-tokens/src/...`. Use public entry points only.
33
33
  - Mounting `Editor` or `ComponentEditorPage` outside their dedicated routes.
34
34
 
35
35
  ## Verify
36
36
 
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`.
37
+ In dev: change a colour in `/live-tokens/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`.
@@ -5,7 +5,7 @@ description: Author a brand-new editable component for a @motion-proto/live-toke
5
5
 
6
6
  # Authoring a component for a live-tokens project
7
7
 
8
- This skill teaches you how to add a new editable component to a project that consumes `@motion-proto/live-tokens`. The end state: a runtime Svelte file, an editor Svelte file, one `registerComponent()` call, and a `/components` page entry under the **CUSTOM** group with full token editing, linked-block sharing, and persistence.
8
+ This skill teaches you how to add a new editable component to a project that consumes `@motion-proto/live-tokens`. The end state: a runtime Svelte file, an editor Svelte file, one `registerComponent()` call, and a `/live-tokens/components` page entry under the **CUSTOM** group with full token editing, linked-block sharing, and persistence.
9
9
 
10
10
  ## Worked examples ship inside the package
11
11
 
@@ -44,7 +44,7 @@ For pattern reference, read any shipped component's source directly from the con
44
44
  ```
45
45
  The schema side-effect happens inside `registerComponent` (which `bootLiveTokens` calls for you), so you don't call `registerComponentSchema` separately. **Do not place a standalone `registerComponent(...)` *before* `bootLiveTokens`** — that registers before the editor's init hooks run, which is the wrong window and can leave editor changes disconnected from the live page. Only call `registerComponent` directly if your app mounts manually (no `bootLiveTokens`), in which case call it before `mount(App, ...)`.
46
46
  4. **Tell the picker** — open `.claude/skills/live-tokens-pick-component/SKILL.md` and add your new component to the **Catalogue** line under the family it belongs to (Action / Input / Selection / Containers / Messaging / Display). If it's confusable with an existing component (a second selection control, a competing container), add a row to that family's decision table explaining the use-case it owns. Without this step, the component exists but [[live-tokens-pick-component]] can't recommend it when a user asks "which component should I use?" — the same rule applies whether the component is first-party (update the picker shipped in this package) or consumer-authored (update the local copy at `.claude/skills/live-tokens-pick-component/SKILL.md` that `setup-claude` placed in your project).
47
- 5. **Verify** — open `/components` and run the verification checklist at the bottom of this file.
47
+ 5. **Verify** — open `/live-tokens/components` and run the verification checklist at the bottom of this file.
48
48
 
49
49
  ## Token discipline
50
50
 
@@ -559,7 +559,7 @@ A new first-party component is auto-covered the moment it lands in `builtInRegis
559
559
 
560
560
  **If your component declares `intrinsics`, the intrinsics contract test covers it too.** `src/editor/component-editor/intrinsicsContract.test.ts` iterates every entry with an `intrinsics` array and asserts, per (intrinsic, variant), that the runtime `:global(:root)` declares a default, the default is one of the spec's `values`, and the editor's `default` equals the runtime default. This is what would have caught a getter defaulting to `center` while `:global(:root)` says `start`. Same auto-coverage rule: declare `intrinsics` on the registry entry and the test picks it up.
561
561
 
562
- Finally navigate to `/components` and confirm the runtime behaviours no static check can see:
562
+ Finally navigate to `/live-tokens/components` and confirm the runtime behaviours no static check can see:
563
563
 
564
564
  - [ ] The new component appears in the nav rail under the **CUSTOM** group (system entries above, custom below the labeled divider).
565
565
  - [ ] Token rows render. Color pickers, radius selectors, font selectors all work.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.36.0 — ImageLightbox `capNatural`: stop upscaling small sources
4
+
5
+ ### Added
6
+
7
+ - **`ImageLightbox` gains a `capNatural` prop.** By default the modal opens
8
+ fitted to the viewport, which upscales a small source (e.g. a low-res GIF or
9
+ screenshot) until it looks soft. Set `capNatural` and the open fit is clamped
10
+ to the source's natural resolution (100%, 1 source px = 1 screen px), so small
11
+ images stay crisp while larger ones fit the viewport exactly as before. It only
12
+ bounds the initial open; pair it with `maxZoom={1}` to also stop the `extended`
13
+ zoom controls from magnifying past 100%. Needs the natural pixel size (from
14
+ `width`/`height` or the loaded image) — until that's known the image fits the
15
+ viewport, then snaps to the cap once measured.
16
+
17
+ ## 0.35.0 — Dev routes moved to a reserved `/live-tokens/*` namespace
18
+
19
+ ### Changed (breaking)
20
+
21
+ - **The package's dev-only routes moved under a reserved `/live-tokens/*`
22
+ namespace:** `/editor` → `/live-tokens/editor`, `/components` →
23
+ `/live-tokens/components`, `/docs` → `/live-tokens/docs`. These routes are
24
+ `import.meta.env.DEV`-only and never appear in production, so the longer paths
25
+ cost nothing where users actually see URLs. Reserving a namespace means a
26
+ consumer's own `/docs` or `/components` page no longer collides with a package
27
+ route, and any owned route added in a future release stays inside the namespace
28
+ (no surprise collisions on a version bump). Relocate or disable any of them via
29
+ the `editorRoutes` prop exactly as before.
30
+
31
+ ### Fixed
32
+
33
+ - **A consumer page at `/docs` or `/components` no longer crashes the app.** The
34
+ package auto-injected nav entries at those paths; a consumer page at the same
35
+ path produced a duplicate key in the overlay's keyed nav list and threw an
36
+ uncaught error, and silently shadowed the consumer's page at dispatch. With the
37
+ reserved namespace the collision cannot occur, so `editorRoutes.docs = false`
38
+ is no longer needed to dodge it.
39
+ - **`editorRoutes.components` relocation now also moves the overlay's
40
+ components-view pairing**, which previously compared a hardcoded `/components`.
41
+
42
+ ### Migrating
43
+
44
+ - **`npx live-tokens migrate` now flags hardcoded route references.** If your
45
+ source navigates to the old paths (e.g. `navigate('/editor')` from the old
46
+ scaffold, or `<a href="/components">`), `migrate` reports each one with its
47
+ file, line, and suggested `/live-tokens/*` replacement. Add `--write` to
48
+ rewrite the unambiguous ones automatically. `/docs` is never auto-rewritten
49
+ (you likely own that route), and any path you declare in `pages` or relocate
50
+ via `editorRoutes` is left for manual review. The editor stays reachable via
51
+ the dev overlay regardless, so this only affects hardcoded shortcut links.
52
+
3
53
  ## 0.34.0 — Token-as-API contract guardrail; opt-in autoMigrate
4
54
 
5
55
  ### Added
package/README.md CHANGED
@@ -8,8 +8,8 @@ A foundational design system for quickly styling and building Svelte + Vite micr
8
8
 
9
9
  - **Real-time token editing.** Pick a color, drag a hue slider, retype a font size — the page repaints on every input event via CSS-variable writes. No reload, no save-and-refresh, no build step. Works across colors, typography, spacing, radii, shadows, motion, palettes, and gradients.
10
10
  - **Real-time component editing.** Each of ~24 shipped Svelte components (Button, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, CodeSnippet, SideNavigation, and more) declares its own design-token aliases in a `:global(:root)` block. Rewire any alias from a per-component picker and see that component update everywhere it's used — live, on your real pages, not in a Storybook sandbox.
11
- - **Theme editor** (`/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a static `tokens.css` for the build.
12
- - **Per-component editor** (`/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
11
+ - **Theme editor** (`/live-tokens/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a static `tokens.css` for the build.
12
+ - **Per-component editor** (`/live-tokens/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
13
13
  - **Live editor overlay** — pins to the top-right of every dev page. Opens the editor in a side panel or floating window so you edit *on the page you're styling*, not in a separate tab. Includes a "Page Source" button that opens the current page's `.svelte` file in VS Code.
14
14
  - **Manifests** — a manifest captures a whole site configuration as one portable artifact: the theme in one slot, every component in its own slot, each holding either the shipped default or a custom file. Export it as a bundle and import it into another project to restore the full styling in one step.
15
15
  - **Vite plugin** — hosts the `/api/live-tokens/{themes,component-configs,manifests}/*` routes that persist your edits to disk as you make them. The single namespace keeps live-tokens' routes from colliding with anything your app serves under `/api`.
@@ -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`, `/docs`), the
125
+ `<ColumnsOverlay>`), the editor routes (`/live-tokens/editor`, `/live-tokens/components`, `/live-tokens/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')`
@@ -307,7 +307,7 @@ bootLiveTokens(App, '#app', {
307
307
 
308
308
  (`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.)
309
309
 
310
- 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/`.
310
+ The component appears in the `/live-tokens/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/`.
311
311
 
312
312
  ## Claude Code skills
313
313
 
@@ -345,7 +345,7 @@ It enforces the file layout, `:global(:root)` block, token-suffix vocabulary, th
345
345
 
346
346
  ## How the editor ships changes to prod
347
347
 
348
- 1. Edit in `/editor` or `/components`. Saves write to `<dataDir>/themes/{name}.json` and `<dataDir>/component-configs/{comp}/{name}.json`.
348
+ 1. Edit in `/live-tokens/editor` or `/live-tokens/components`. Saves write to `<dataDir>/themes/{name}.json` and `<dataDir>/component-configs/{comp}/{name}.json`.
349
349
  2. Promote a theme to "production." Its variables are written into `tokens.generated.css` next to your authored `tokens.css`.
350
350
  3. `npm run build` bundles both as plain CSS. No editor code, no JSON lookups, no dev surfaces ship to prod.
351
351
 
package/bin/cli.mjs CHANGED
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import process from 'node:process';
12
12
  import { checkComponent, formatReport } from './check-component.mjs';
13
13
  import { runMigrate, formatMigrateResult } from './migrate.mjs';
14
+ import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
14
15
  import { runCreate, formatCreateResult } from './create.mjs';
15
16
 
16
17
  const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
@@ -21,10 +22,15 @@ Commands:
21
22
  setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
22
23
  check-component <id> Validate <id>'s runtime, editor, and registration
23
24
  against the live-tokens-create-component contract
24
- migrate [--check] [--tokens <path>]
25
- Reconcile your tokens.css with the installed
26
- package's Layer-1 token vocabulary. --check reports
27
- without writing (exit 1 if changes are needed).
25
+ migrate [--check] [--write] [--tokens <path>]
26
+ Reconcile your project with the installed package:
27
+ applies additive tokens.css migrations (unless
28
+ --check), and reports source references to the
29
+ editor/components/docs routes that moved to
30
+ /live-tokens/* in 0.35.0. --write also rewrites the
31
+ unambiguous route references (never /docs). --check
32
+ reports without writing (exit 1 if token migrations
33
+ are pending; route findings are advisory).
28
34
  `;
29
35
 
30
36
  function fail(message, code = 1) {
@@ -67,13 +73,21 @@ if (command === 'check-component') {
67
73
 
68
74
  if (command === 'migrate') {
69
75
  const check = rest.includes('--check');
76
+ const write = rest.includes('--write');
70
77
  const tokensIdx = rest.indexOf('--tokens');
71
78
  const tokensArg = tokensIdx !== -1 ? rest[tokensIdx + 1] : undefined;
72
79
  if (tokensIdx !== -1 && !tokensArg) fail(`--tokens requires a path`);
73
80
  try {
74
81
  const result = await runMigrate({ tokensArg, check });
75
82
  console.log(formatMigrateResult(result, { check }));
76
- // Exit 1 when --check finds pending changes (CI-friendly) or on no-path.
83
+
84
+ // Route-reference pass: advisory by default, rewrites the unambiguous hits
85
+ // only with --write (and never under --check).
86
+ const routes = runMigrateRoutes({ root: process.cwd(), apply: write && !check });
87
+ const routeOut = formatRouteResult(routes, { check });
88
+ if (routeOut) console.log('\n' + routeOut);
89
+
90
+ // Route findings are advisory; only token migrations gate the exit code.
77
91
  if (result.status === 'no-path') process.exit(1);
78
92
  if (check && result.status === 'would-change') process.exit(1);
79
93
  process.exit(0);
@@ -0,0 +1,179 @@
1
+ // `live-tokens migrate` route-reference pass (0.35.0 namespace move).
2
+ //
3
+ // The package's dev-only routes moved to a reserved `/live-tokens/*` namespace,
4
+ // so consumer source that hardcoded `/editor`, `/components`, or `/docs` (e.g.
5
+ // `navigate('/editor')` from the old scaffold) now 404s. This scans the
6
+ // consumer's source and reports those references; with `apply`, it rewrites the
7
+ // unambiguous ones.
8
+ //
9
+ // Safety: `/docs` is never auto-rewritten (consumers commonly own a docs page),
10
+ // and any path the project declares as its own (a `pages` key) or manages via
11
+ // `editorRoutes` is left as an advisory. A reference is rewritten only when the
12
+ // package definitely owns that path.
13
+
14
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { extname, join, relative } from 'node:path';
16
+
17
+ const MOVED = [
18
+ { old: '/editor', next: '/live-tokens/editor' },
19
+ { old: '/components', next: '/live-tokens/components' },
20
+ { old: '/docs', next: '/live-tokens/docs' },
21
+ ];
22
+
23
+ // Never auto-rewritten: a consumer's own docs page is the common case, and we
24
+ // can't tell their `/docs` from the package's without guessing.
25
+ const NEVER_AUTOWRITE = new Set(['/docs']);
26
+
27
+ const SCAN_EXTS = new Set(['.svelte', '.ts', '.js', '.mjs', '.tsx', '.jsx']);
28
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'dist-plugin', '.git', '.svelte-kit', 'build']);
29
+
30
+ function walk(dir, out = []) {
31
+ let entries;
32
+ try {
33
+ entries = readdirSync(dir, { withFileTypes: true });
34
+ } catch {
35
+ return out;
36
+ }
37
+ for (const e of entries) {
38
+ if (e.isDirectory()) {
39
+ if (!SKIP_DIRS.has(e.name)) walk(join(dir, e.name), out);
40
+ } else if (SCAN_EXTS.has(extname(e.name))) {
41
+ out.push(join(dir, e.name));
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+
47
+ // `navigate('/p')` / `navigate("/p")` / `navigate(`/p`)` and the href forms
48
+ // `href="/p"` / `href='/p'` / `href={'/p'}`. The closing backreference quote
49
+ // pins the path to the exact string, so `/live-tokens/editor` never matches
50
+ // `/editor` (the char before `/editor` there is `s`, not a quote).
51
+ function refRegex(oldPath) {
52
+ return new RegExp(`(navigate\\(\\s*|href\\s*=\\s*\\{?\\s*)(['"\`])${oldPath}\\2`, 'g');
53
+ }
54
+
55
+ function kindOf(prefix) {
56
+ return prefix.trimStart().startsWith('href') ? 'href' : 'navigate';
57
+ }
58
+
59
+ /**
60
+ * A path is the consumer's, not the package's, if they declare a page at it
61
+ * (`'/x':` in a pages map) or name it in an `editorRoutes` override. Conservative
62
+ * by design: when unsure we mark it owned, which only ever downgrades a rewrite
63
+ * to an advisory.
64
+ */
65
+ function detectConsumerOwned(files) {
66
+ const owned = new Set();
67
+ for (const file of files) {
68
+ const c = readFileSync(file, 'utf8');
69
+ const hasEditorRoutes = /editorRoutes/.test(c);
70
+ for (const { old } of MOVED) {
71
+ const key = old.slice(1);
72
+ if (new RegExp(`['"]\\/${key}['"]\\s*:`).test(c)) owned.add(old);
73
+ if (hasEditorRoutes && new RegExp(`\\b${key}\\s*:`).test(c)) owned.add(old);
74
+ }
75
+ }
76
+ return owned;
77
+ }
78
+
79
+ function scanContent(content) {
80
+ const lines = content.split('\n');
81
+ const found = [];
82
+ for (const { old, next } of MOVED) {
83
+ const re = refRegex(old);
84
+ lines.forEach((text, i) => {
85
+ re.lastIndex = 0;
86
+ let m;
87
+ while ((m = re.exec(text))) {
88
+ found.push({ line: i + 1, kind: kindOf(m[1]), old, next });
89
+ }
90
+ });
91
+ }
92
+ return found;
93
+ }
94
+
95
+ function applyRewrites(content, paths) {
96
+ let out = content;
97
+ for (const { old, next } of paths) {
98
+ out = out.replace(refRegex(old), (_full, prefix, quote) => `${prefix}${quote}${next}${quote}`);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * Scan (and with `apply`, rewrite) source under `root`. Returns
105
+ * `{ scannedFiles, rewritten, pendingWrite, advisory, owned }` — `rewritten` is
106
+ * populated only when `apply` is true; otherwise auto-writable hits land in
107
+ * `pendingWrite`. `advisory` always holds the hits we won't touch automatically.
108
+ */
109
+ export function runMigrateRoutes({ root = process.cwd(), apply = false } = {}) {
110
+ const srcDir = join(root, 'src');
111
+ const base = existsSync(srcDir) ? srcDir : root;
112
+ const files = walk(base);
113
+ const owned = detectConsumerOwned(files);
114
+
115
+ const rewritten = [];
116
+ const pendingWrite = [];
117
+ const advisory = [];
118
+
119
+ for (const file of files) {
120
+ const content = readFileSync(file, 'utf8');
121
+ const rel = relative(root, file);
122
+ const hits = scanContent(content).map((h) => ({
123
+ ...h,
124
+ file: rel,
125
+ autoWrite: !NEVER_AUTOWRITE.has(h.old) && !owned.has(h.old),
126
+ reason: NEVER_AUTOWRITE.has(h.old) ? 'docs-never' : owned.has(h.old) ? 'consumer-owned' : null,
127
+ }));
128
+ if (hits.length === 0) continue;
129
+
130
+ const auto = hits.filter((h) => h.autoWrite);
131
+ advisory.push(...hits.filter((h) => !h.autoWrite));
132
+
133
+ if (apply && auto.length) {
134
+ const paths = MOVED.filter((m) => auto.some((h) => h.old === m.old));
135
+ const next = applyRewrites(content, paths);
136
+ if (next !== content) {
137
+ writeFileSync(file, next);
138
+ rewritten.push(...auto);
139
+ }
140
+ } else {
141
+ pendingWrite.push(...auto);
142
+ }
143
+ }
144
+
145
+ return { scannedFiles: files.length, rewritten, pendingWrite, advisory, owned: [...owned] };
146
+ }
147
+
148
+ export function formatRouteResult(result, { check = false } = {}) {
149
+ const { rewritten, pendingWrite, advisory } = result;
150
+ if (!rewritten.length && !pendingWrite.length && !advisory.length) return '';
151
+
152
+ const ref = (h) => ` ${h.file}:${h.line} ${h.kind} '${h.old}' → '${h.next}'`;
153
+ const lines = ['Route references — /editor, /components, /docs moved to /live-tokens/* in 0.35.0:'];
154
+
155
+ if (rewritten.length) {
156
+ lines.push(` ✓ Rewrote ${rewritten.length} reference(s):`);
157
+ rewritten.forEach((h) => lines.push(ref(h)));
158
+ }
159
+ if (pendingWrite.length) {
160
+ lines.push(
161
+ check
162
+ ? ` Would rewrite ${pendingWrite.length} reference(s) with --write:`
163
+ : ` ${pendingWrite.length} reference(s) can be rewritten — re-run with --write to apply:`,
164
+ );
165
+ pendingWrite.forEach((h) => lines.push(ref(h)));
166
+ }
167
+ if (advisory.length) {
168
+ lines.push(` ⚠ ${advisory.length} reference(s) need manual review:`);
169
+ advisory.forEach((h) => {
170
+ const why =
171
+ h.reason === 'docs-never'
172
+ ? `/docs is never auto-rewritten — update to '${h.next}' only if it points at the package guide`
173
+ : `you declare or relocate '${h.old}' yourself — leave it if it's your route`;
174
+ lines.push(` ${h.file}:${h.line} ${h.kind} '${h.old}' (${why})`);
175
+ });
176
+ }
177
+ if (rewritten.length) lines.push('\n Review the diff in git before committing.');
178
+ return lines.join('\n');
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -4,10 +4,10 @@
4
4
  * Writes to document.documentElement and — when running inside a same-origin
5
5
  * iframe (the live-preview overlay) — also writes to
6
6
  * window.parent.document.documentElement. This lets the overlay editor at
7
- * /editor drive the host site's :root in real time without any message-passing
8
- * infrastructure.
7
+ * /live-tokens/editor drive the host site's :root in real time without any
8
+ * message-passing infrastructure.
9
9
  *
10
- * When the editor runs standalone at /editor (not inside the overlay iframe),
10
+ * When the editor runs standalone at /live-tokens/editor (not inside the overlay iframe),
11
11
  * parentRoot is null and every call is a plain single-root write.
12
12
  *
13
13
  * Roots are resolved lazily — `init()` (or any setter call) populates them on
@@ -0,0 +1,11 @@
1
+ // The package's dev-only editor surfaces live under a reserved `/live-tokens/*`
2
+ // namespace so they can never collide with a consumer's own routes — the
3
+ // consumer owns the rest of the URL space. Keeping the defaults in one place
4
+ // is the guard against the nav rail and the dispatcher drifting onto different
5
+ // paths (the collision/shadow class these constants exist to prevent).
6
+ //
7
+ // A consumer relocates or disables each surface via the `editorRoutes` prop on
8
+ // <LiveTokensRouter>; these are only the defaults.
9
+ export const DEFAULT_EDITOR_PATH = '/live-tokens/editor';
10
+ export const DEFAULT_COMPONENTS_PATH = '/live-tokens/components';
11
+ export const DEFAULT_DOCS_PATH = '/live-tokens/docs';
@@ -1,12 +1,13 @@
1
1
  import { writable } from 'svelte/store';
2
2
  import { storageKey } from '../store/editorConfig';
3
+ import { DEFAULT_EDITOR_PATH } from './ownedRoutes';
3
4
 
4
5
  function prevKey(): string {
5
6
  return storageKey('prev-route');
6
7
  }
7
8
 
8
9
  function rememberPrev(current: string) {
9
- if (current === '/editor') return;
10
+ if (current === DEFAULT_EDITOR_PATH) return;
10
11
  try { sessionStorage.setItem(prevKey(), current); } catch { /* ignore */ }
11
12
  }
12
13
 
@@ -39,6 +39,7 @@
39
39
  <script lang="ts">
40
40
  import { onMount, tick } from 'svelte';
41
41
  import { marked, type Tokens } from 'marked';
42
+ import { DEFAULT_COMPONENTS_PATH } from '../core/routing/ownedRoutes';
42
43
 
43
44
  import Button from '../../system/components/Button.svelte';
44
45
  import CodeSnippet from '../../system/components/CodeSnippet.svelte';
@@ -68,7 +69,7 @@
68
69
  let error = $state<string | null>(null);
69
70
  let chapterMeta = $state<Chapter | null>(null);
70
71
 
71
- /* Parse `/docs#editing-tokens~palettes` → chapter `editing-tokens`, anchor `palettes`.
72
+ /* Parse `/live-tokens/docs#editing-tokens~palettes` → chapter `editing-tokens`, anchor `palettes`.
72
73
  Using `~` as the delimiter keeps each part valid for an HTML id. */
73
74
  let parsedHash = $derived.by(() => {
74
75
  const raw = hash.replace(/^#/, '');
@@ -269,7 +270,7 @@
269
270
  Demo Site
270
271
  </Button>
271
272
  </a>
272
- <a href="/components" class="rail-action">
273
+ <a href={DEFAULT_COMPONENTS_PATH} class="rail-action">
273
274
  <Button variant="outline" size="small" icon="fas fa-puzzle-piece" fullWidth>
274
275
  Components
275
276
  </Button>
@@ -26,13 +26,13 @@ Describe what you want in plain English. Phrases like these trigger the skill:
26
26
 
27
27
  Claude asks any clarifying questions it needs (which variants, which states,
28
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
29
+ its verification checklist. When it finishes, open `/live-tokens/components` to see your new
30
30
  component in the editor and confirm everything works.
31
31
 
32
32
  ## What you get
33
33
 
34
34
  - A runtime component whose editable properties default to your theme tokens.
35
- - An editor entry that appears under **Custom** in the `/components` view.
35
+ - An editor entry that appears under **Custom** in the `/live-tokens/components` view.
36
36
  - The naming and wiring handled for you, so the component fits the system.
37
37
 
38
38
  Advanced authors who want to write a component by hand can read the naming and
@@ -28,14 +28,14 @@ version upgrades never touch your styles. The package code stays in
28
28
  | Path | What it is |
29
29
  |------|------------|
30
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. |
31
+ | `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |
32
32
  | `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |
33
33
  | `src/styles/site.css` | Themed page typography, yours to edit. |
34
34
 
35
35
  ## Your first edit
36
36
 
37
37
  1. Run `npm run dev` and open the home page.
38
- 2. Click **Open Token Editor**, or visit `/editor`. The editor opens beside
38
+ 2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside
39
39
  the page.
40
40
  3. Open **Palettes**, pick **Brand**, and change the base hex. The page
41
41
  repaints as you type.
@@ -3,8 +3,8 @@
3
3
 
4
4
  export const docContent: Record<string, string> = {
5
5
  "01-overview": "# Overview\n\nLive Tokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the manifest and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button, Card,\n Dialog, Table, and more).You style components by changing the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
6
- "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
6
+ "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
7
7
  "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has two views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Two curves shape how lightness and saturation fall off across the\n ramp. Drag the handles to bias it darker, lighter, or more saturated.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Overlays and gradients\n\n- **Overlays** are translucent tints layered over surfaces, like the subtle\n tint a card gets on hover. Set a colour and opacity per state.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. **Save** is a separate step: it writes a named theme file under\n`src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`) and a file\nmenu for New, Save, Save as, Switch, and Delete. You can keep many themes side\nby side; one is active at a time. See [Themes](themes-workflow.md) for the full\nlifecycle.\n",
8
- "getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds the dev-only `/editor`, `/components`, and `/docs` routes. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. Open the file menu and choose **Save as**. A theme appears as JSON under\n `src/live-tokens/data/themes/`.\n5. Reload. Your saved theme is the active theme, so the page returns as you\n left it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, promote a theme to production in the editor. That bakes the theme's\nvariables into `src/live-tokens/data/tokens.generated.css`, which your build\nbundles alongside `tokens.css`. The editor itself never reaches production.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
8
+ "getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. Open the file menu and choose **Save as**. A theme appears as JSON under\n `src/live-tokens/data/themes/`.\n5. Reload. Your saved theme is the active theme, so the page returns as you\n left it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, promote a theme to production in the editor. That bakes the theme's\nvariables into `src/live-tokens/data/tokens.generated.css`, which your build\nbundles alongside `tokens.css`. The editor itself never reaches production.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
9
9
  "themes-workflow": "# Themes\n\nSave your work, switch between themes, and ship one to production.\n\n## How themes work\n\n- **Your live edits** are what the page shows right now. They save to your\n browser automatically and survive a reload, but they are not yet a file.\n- **A saved theme** is a named JSON file in `src/live-tokens/data/themes/`. You\n create one with **Save as**.\n- **The active theme** is the saved theme the page loads at startup. Exactly one\n at a time.\n- **The production theme** is the one that ships. Promoting sets it.\n\n## Saving\n\nIn the editor header:\n\n- **Save** updates the current theme.\n- **Save as** names a new theme. Use it for your first save and for forking.\n\nNames are tidied to lowercase with underscores, so \"My Brand!\" becomes\n`my_brand`. There is a built-in `default` theme you can always return to; the\neditor never overwrites it.\n\n## Switching\n\nThe file menu lists every saved theme. Pick one to make it active; the page\nreloads with it applied. Your current edits are saved to the previous theme\nfirst, so you don't lose work.\n\n## Shipping\n\n**Promote to production** is the \"ship it\" step. It bakes the theme's variables\ninto `src/live-tokens/data/tokens.generated.css`, which your build bundles\nalongside `tokens.css`. Fonts regenerate to match.\n\nProduction builds (`npm run build`) ship only that plain CSS and your\ncomponents. No editor, no JSON loading, no runtime indirection. If you save\nwhile the production theme is active, the generated CSS updates immediately,\nwith no separate promote step.\n\n## Manifests\n\nA **manifest** bundles one theme plus a config for each component into a single\nnamed set. Useful when you run several brands and want each to apply its theme\nand component tweaks in one move. There is a protected default and an active\nmanifest; applying one swaps everything at once.\n\n## Keeping your work safe\n\nEverything under `src/live-tokens/data/` is plain JSON, so commit it. Themes\nshow up as readable diffs you can review per branch. There are no automatic\nbackups: git is your safety net. To experiment freely, **Save as** a new name\nfirst, then edit.\n\n## Where to go next\n\n- **[Creating components](creating-components.md)**: make your own components\n editable in the same editor.\n",
10
10
  };
@@ -15,6 +15,7 @@
15
15
  import { fade } from 'svelte/transition';
16
16
  import { cubicInOut } from 'svelte/easing';
17
17
  import { route, navigate } from '../core/routing/router';
18
+ import { DEFAULT_EDITOR_PATH, DEFAULT_COMPONENTS_PATH } from '../core/routing/ownedRoutes';
18
19
  import { editorView } from '../core/store/editorViewStore';
19
20
  import { columnsVisible, toggleColumns } from './columnsOverlay';
20
21
  import { storageKey } from '../core/store/editorConfig';
@@ -27,6 +28,7 @@
27
28
  interface Props {
28
29
  open?: boolean | undefined;
29
30
  editorPath?: string;
31
+ componentsPath?: string;
30
32
  navLinks?: NavLink[];
31
33
  pageSources?: Record<string, string>;
32
34
  hidePageSourceOn?: string[];
@@ -35,7 +37,8 @@
35
37
 
36
38
  let {
37
39
  open = $bindable(undefined),
38
- editorPath = '/editor',
40
+ editorPath = DEFAULT_EDITOR_PATH,
41
+ componentsPath = DEFAULT_COMPONENTS_PATH,
39
42
  navLinks = [],
40
43
  pageSources = {},
41
44
  hidePageSourceOn = [],
@@ -62,9 +65,9 @@
62
65
  overlayOpen.set(!!open);
63
66
  });
64
67
 
65
- // The /components route renders the same component-editor surface as the
66
- // overlay's components view. Pair them: on entering /components, flip the
67
- // overlay to tokens so the two surfaces don't stack. Fires only on route
68
+ // The components route renders the same component-editor surface as the
69
+ // overlay's components view. Pair them: on entering it, flip the overlay to
70
+ // tokens so the two surfaces don't stack. Fires only on route
68
71
  // change, not on every editorView change — otherwise cross-window storage
69
72
  // sync re-triggers the rule, which writes editorView, which fires another
70
73
  // storage event, which fires the rule again. The result is heavy re-render
@@ -75,7 +78,7 @@
75
78
  const r = $route;
76
79
  if (r === prevRoute) return;
77
80
  prevRoute = r;
78
- if (r === '/components') {
81
+ if (r === componentsPath) {
79
82
  editorView.update((v) => (v === 'components' ? 'tokens' : v));
80
83
  }
81
84
  });
@@ -28,9 +28,11 @@
28
28
  }
29
29
 
30
30
  /**
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).
31
+ * Override the package's dev-only routes, which default to the reserved
32
+ * `/live-tokens/*` namespace (`/live-tokens/editor`, `/live-tokens/components`,
33
+ * `/live-tokens/docs`) so they never collide with consumer pages. Pass a
34
+ * string to relocate a route; pass `false` to disable it entirely (no
35
+ * dispatch and, for `components`/`docs`, no auto-injected nav-rail entry).
34
36
  */
35
37
  export interface EditorRouteOverrides {
36
38
  editor?: string | false;
@@ -40,8 +42,8 @@
40
42
 
41
43
  /**
42
44
  * 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
+ * package-owned `/live-tokens/*` routes (editor, components, docs) have
46
+ * already been matched by the component:
45
47
  *
46
48
  * 1. `pages[route]` — exact static match.
47
49
  * 2. `resolve(route)` — consumer code, where params / prefixes / gating
@@ -66,6 +68,7 @@
66
68
  import LiveEditorOverlay from './LiveEditorOverlay.svelte';
67
69
  import ColumnsOverlay from './ColumnsOverlay.svelte';
68
70
  import { route, navigate } from '../core/routing/router';
71
+ import { DEFAULT_EDITOR_PATH, DEFAULT_COMPONENTS_PATH, DEFAULT_DOCS_PATH } from '../core/routing/ownedRoutes';
69
72
 
70
73
  interface Props {
71
74
  pages: Record<string, RouteEntry>;
@@ -84,9 +87,9 @@
84
87
  let editorEnabled = $derived(editorRoutes.editor !== false);
85
88
  let componentsEnabled = $derived(editorRoutes.components !== false);
86
89
  let docsEnabled = $derived(editorRoutes.docs !== false);
87
- let editorPath = $derived(typeof editorRoutes.editor === 'string' ? editorRoutes.editor : '/editor');
88
- let componentsPath = $derived(typeof editorRoutes.components === 'string' ? editorRoutes.components : '/components');
89
- let docsPath = $derived(typeof editorRoutes.docs === 'string' ? editorRoutes.docs : '/docs');
90
+ let editorPath = $derived(typeof editorRoutes.editor === 'string' ? editorRoutes.editor : DEFAULT_EDITOR_PATH);
91
+ let componentsPath = $derived(typeof editorRoutes.components === 'string' ? editorRoutes.components : DEFAULT_COMPONENTS_PATH);
92
+ let docsPath = $derived(typeof editorRoutes.docs === 'string' ? editorRoutes.docs : DEFAULT_DOCS_PATH);
90
93
 
91
94
  const isDev = import.meta.env.DEV;
92
95
  let isEditor = $derived(isDev && editorEnabled && $route === editorPath);
@@ -176,7 +179,7 @@
176
179
  class:is-component-editor={isComponentEditor}
177
180
  onclick={handleClick}
178
181
  >
179
- <LiveEditorOverlay {navLinks} {pageSources} {hidePageSourceOn} {editorPath} />
182
+ <LiveEditorOverlay {navLinks} {pageSources} {hidePageSourceOn} {editorPath} {componentsPath} />
180
183
  <ColumnsOverlay />
181
184
 
182
185
  {#await pagePromise then m}
@@ -5,6 +5,7 @@
5
5
  import { installEditorKeybindings } from '../core/store/editorKeybindings';
6
6
  import { initializeEditorStore } from '../core/store/editorStore';
7
7
  import { storageKey } from '../core/store/editorConfig';
8
+ import { DEFAULT_EDITOR_PATH } from '../core/routing/ownedRoutes';
8
9
  // Editor chrome + form controls + icon font must be JS imports (not @import
9
10
  // inside the style block) so Vite resolves them via the module graph
10
11
  // regardless of how the consumer compiles Svelte CSS (external ?lang.css vs
@@ -23,7 +24,7 @@
23
24
  function pickBackHref(): string {
24
25
  try {
25
26
  const prev = sessionStorage.getItem(storageKey('prev-route'));
26
- if (prev && prev !== '/editor') return prev;
27
+ if (prev && prev !== DEFAULT_EDITOR_PATH) return prev;
27
28
  } catch {
28
29
  // ignore
29
30
  }
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { editorView } from '../core/store/editorViewStore';
3
3
  import { parentRoute } from '../core/routing/parentRouteStore';
4
+ import { DEFAULT_COMPONENTS_PATH } from '../core/routing/ownedRoutes';
4
5
 
5
6
  interface Props {
6
7
  condensed?: boolean;
@@ -8,11 +9,13 @@
8
9
 
9
10
  let { condensed = false }: Props = $props();
10
11
 
11
- // On /components the host page is already the components editor — the
12
- // overlay's components view would just stack on top of it, so disable the
12
+ // On the components route the host page is already the components editor —
13
+ // the overlay's components view would just stack on top, so disable the
13
14
  // switch. The switcher renders inside the editor iframe, so we read the
14
- // *parent* route, not this iframe's own route.
15
- let componentsDisabled = $derived($parentRoute === '/components');
15
+ // *parent* route, not this iframe's own. Compares the default path:
16
+ // editorRoutes.components relocation isn't plumbed across the iframe, so a
17
+ // relocated route won't disable the switch.
18
+ let componentsDisabled = $derived($parentRoute === DEFAULT_COMPONENTS_PATH);
16
19
 
17
20
  function set(v: 'tokens' | 'components') {
18
21
  editorView.set(v);
@@ -28,12 +28,22 @@
28
28
  extended?: boolean;
29
29
  /** Maximum zoom, as a multiple of the image's natural resolution: `1` = 100%
30
30
  of the source's real pixels (1 source px = 1 screen px), `2` = 200%. The
31
- modal always opens fitted to the viewport; this only caps how far the
32
- `extended` zoom controls can magnify. Unset = the default 5x-the-fit cap.
33
- An image whose fitted size already exceeds the cap simply can't be zoomed
34
- in. Needs the natural pixel size (from `width`/`height`, or the loaded
35
- image); until that's known the default cap applies. */
31
+ modal opens fitted to the viewport (or to natural size when `capNatural`
32
+ is set); this only caps how far the `extended` zoom controls can magnify.
33
+ Unset = the default 5x-the-fit cap. An image whose fitted size already
34
+ exceeds the cap simply can't be zoomed in. Needs the natural pixel size
35
+ (from `width`/`height`, or the loaded image); until that's known the
36
+ default cap applies. */
36
37
  maxZoom?: number | undefined;
38
+ /** Cap the opened image at its natural resolution (100%): the modal still
39
+ opens centered, but never scales the source above 1:1, so a small source
40
+ (e.g. a low-res GIF) stays crisp instead of being upscaled to fill the
41
+ viewport. Larger images are unaffected — they fit the viewport as usual.
42
+ Only bounds the initial open fit; pair with `maxZoom={1}` to also stop the
43
+ `extended` controls zooming past 100%. Needs the natural pixel size (from
44
+ `width`/`height`, or the loaded image); until that's known the image fits
45
+ the viewport, then snaps to the cap once measured. */
46
+ capNatural?: boolean;
37
47
  }
38
48
 
39
49
  let {
@@ -46,6 +56,7 @@
46
56
  fit = 'contain',
47
57
  extended = false,
48
58
  maxZoom = undefined,
59
+ capNatural = false,
49
60
  }: Props = $props();
50
61
 
51
62
  const items = $derived(
@@ -151,7 +162,11 @@
151
162
  if (!aspect) {
152
163
  return { top: vh * 0.04, left: vw * 0.03, width: capW, height: capH };
153
164
  }
154
- const tileW = Math.min(capW, capH * aspect);
165
+ let tileW = Math.min(capW, capH * aspect);
166
+ if (capNatural) {
167
+ const nw = naturalWidthOf(current);
168
+ if (nw) tileW = Math.min(tileW, nw);
169
+ }
155
170
  const tileH = tileW / aspect;
156
171
  return {
157
172
  top: (vh - tileH) / 2,
@@ -14,9 +14,10 @@ Open http://localhost:5173.
14
14
  This project follows the package's [recommended layout](https://github.com/motionproto/live-tokens#recommended-project-layout): all editable state lives under `src/` and is committed, so `npm install` and upgrades never touch your styles.
15
15
 
16
16
  - `src/pages/Home.svelte` — the starter page. Replace it with your own content.
17
- - `src/App.svelte` — your routes. `<LiveTokensRouter>` adds the dev-only
18
- `/editor` (theme tokens), `/components` (per-component aliases), and `/docs`
19
- (the user guide) routes.
17
+ - `src/App.svelte` — your routes. `<LiveTokensRouter>` adds dev-only routes under
18
+ a reserved `/live-tokens/*` namespace (so they never collide with your pages):
19
+ `/live-tokens/editor` (theme tokens), `/live-tokens/components` (per-component
20
+ aliases), and `/live-tokens/docs` (the user guide).
20
21
  - `src/system/styles/tokens.css` — your theme token vocabulary. The dev server
21
22
  writes edits here when you use the in-browser editor.
22
23
  - `src/styles/site.css` — themed page typography. Yours to edit.
@@ -24,7 +25,7 @@ This project follows the package's [recommended layout](https://github.com/motio
24
25
  ## Editing live
25
26
 
26
27
  Run `npm run dev`, then click **Open Token Editor** on the home page (or visit
27
- `/editor`). Changes persist to `src/system/styles/tokens.css` and the JSON under
28
+ `/live-tokens/editor`). Changes persist to `src/system/styles/tokens.css` and the JSON under
28
29
  `src/live-tokens/data/`. The editor is dev-only — `npm run build` ships plain
29
30
  CSS variables and the components you used, nothing else.
30
31
 
@@ -1,9 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { LiveTokensRouter } from '@motion-proto/live-tokens';
3
3
 
4
- // <LiveTokensRouter> owns the dev-only /editor and /components routes.
5
- // Declare your own pages here. Lazy imports keep each page's CSS
6
- // side-effects out of the editor routes; omit `label` to keep a route
4
+ // <LiveTokensRouter> owns the dev-only /live-tokens/editor and
5
+ // /live-tokens/components routes (a reserved namespace, so they never collide
6
+ // with your pages). Declare your own pages here. Lazy imports keep each page's
7
+ // CSS side-effects out of the editor routes; omit `label` to keep a route
7
8
  // reachable by URL but hidden from the overlay nav rail.
8
9
  const pages = {
9
10
  '/': {
@@ -22,8 +22,8 @@
22
22
  </p>
23
23
  {#if isDev}
24
24
  <div class="actions">
25
- <Button on:click={() => navigate('/editor')}>Open Token Editor</Button>
26
- <Button variant="secondary" on:click={() => navigate('/components')}>Components</Button>
25
+ <Button on:click={() => navigate('/live-tokens/editor')}>Open Token Editor</Button>
26
+ <Button variant="secondary" on:click={() => navigate('/live-tokens/components')}>Components</Button>
27
27
  </div>
28
28
  {/if}
29
29
  </Card>