@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
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
/* highlight.js core + the languages used across the docs.
|
|
3
|
+
Registered once at module load so every CodeBlock instance shares the
|
|
4
|
+
same registry. */
|
|
5
|
+
import hljs from 'highlight.js/lib/core';
|
|
6
|
+
import typescript from 'highlight.js/lib/languages/typescript';
|
|
7
|
+
import javascript from 'highlight.js/lib/languages/javascript';
|
|
8
|
+
import bash from 'highlight.js/lib/languages/bash';
|
|
9
|
+
import scss from 'highlight.js/lib/languages/scss';
|
|
10
|
+
import css from 'highlight.js/lib/languages/css';
|
|
11
|
+
import json from 'highlight.js/lib/languages/json';
|
|
12
|
+
import xml from 'highlight.js/lib/languages/xml';
|
|
13
|
+
import plaintext from 'highlight.js/lib/languages/plaintext';
|
|
14
|
+
|
|
15
|
+
hljs.registerLanguage('typescript', typescript);
|
|
16
|
+
hljs.registerLanguage('ts', typescript);
|
|
17
|
+
hljs.registerLanguage('javascript', javascript);
|
|
18
|
+
hljs.registerLanguage('js', javascript);
|
|
19
|
+
hljs.registerLanguage('bash', bash);
|
|
20
|
+
hljs.registerLanguage('sh', bash);
|
|
21
|
+
hljs.registerLanguage('shell', bash);
|
|
22
|
+
hljs.registerLanguage('css', css);
|
|
23
|
+
hljs.registerLanguage('scss', scss);
|
|
24
|
+
hljs.registerLanguage('json', json);
|
|
25
|
+
hljs.registerLanguage('jsonc', json);
|
|
26
|
+
hljs.registerLanguage('xml', xml);
|
|
27
|
+
hljs.registerLanguage('html', xml);
|
|
28
|
+
hljs.registerLanguage('svelte', xml);
|
|
29
|
+
hljs.registerLanguage('plaintext', plaintext);
|
|
30
|
+
|
|
31
|
+
/* Shell command blocks render with the shipped CodeSnippet (copy button);
|
|
32
|
+
other languages keep the syntax-highlighted CodeBlock. */
|
|
33
|
+
const SHELL_LANGS = new Set(['bash', 'sh', 'shell']);
|
|
34
|
+
function isShell(lang: string | undefined): boolean {
|
|
35
|
+
return lang != null && SHELL_LANGS.has(lang);
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<script lang="ts">
|
|
40
|
+
import { onMount, tick } from 'svelte';
|
|
41
|
+
import { marked, type Tokens } from 'marked';
|
|
42
|
+
|
|
43
|
+
import Button from '../../system/components/Button.svelte';
|
|
44
|
+
import CodeSnippet from '../../system/components/CodeSnippet.svelte';
|
|
45
|
+
import Notification from '../../system/components/Notification.svelte';
|
|
46
|
+
import SectionDivider from '../../system/components/SectionDivider.svelte';
|
|
47
|
+
import SideNavigation, { type SideNavSection } from '../../system/components/SideNavigation.svelte';
|
|
48
|
+
import Table from '../../system/components/Table.svelte';
|
|
49
|
+
|
|
50
|
+
import CodeBlock from './CodeBlock.svelte';
|
|
51
|
+
import {
|
|
52
|
+
chapters,
|
|
53
|
+
chapterIds,
|
|
54
|
+
loadChapter,
|
|
55
|
+
chapterNeighbours,
|
|
56
|
+
type Chapter,
|
|
57
|
+
} from './chapters';
|
|
58
|
+
|
|
59
|
+
type Segment =
|
|
60
|
+
| { kind: 'html'; html: string }
|
|
61
|
+
| { kind: 'code'; lang: string | undefined; text: string }
|
|
62
|
+
| { kind: 'table'; html: string };
|
|
63
|
+
|
|
64
|
+
/* ------------------------------------------------------------------ State */
|
|
65
|
+
let hash = $state(typeof window !== 'undefined' ? window.location.hash : '');
|
|
66
|
+
let segments = $state<Segment[]>([]);
|
|
67
|
+
let loading = $state(true);
|
|
68
|
+
let error = $state<string | null>(null);
|
|
69
|
+
let chapterMeta = $state<Chapter | null>(null);
|
|
70
|
+
|
|
71
|
+
/* Parse `/docs#editing-tokens~palettes` → chapter `editing-tokens`, anchor `palettes`.
|
|
72
|
+
Using `~` as the delimiter keeps each part valid for an HTML id. */
|
|
73
|
+
let parsedHash = $derived.by(() => {
|
|
74
|
+
const raw = hash.replace(/^#/, '');
|
|
75
|
+
const [chapter, anchor] = raw.split('~');
|
|
76
|
+
const validChapter = chapterIds.includes(chapter) ? chapter : chapterIds[0];
|
|
77
|
+
return { chapter: validChapter, anchor: anchor ?? null };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let neighbours = $derived(chapterNeighbours(parsedHash.chapter));
|
|
81
|
+
|
|
82
|
+
/* Sidebar open/closed state. The SideNavigation toggle button dispatches
|
|
83
|
+
`ontoggle`; this parent owns the actual boolean so layout reflows beside it. */
|
|
84
|
+
let sidebarOpen = $state(true);
|
|
85
|
+
|
|
86
|
+
/* On desktop the content column is the scroll region (the page is locked to
|
|
87
|
+
the viewport), so navigation resets its scrollTop — see the scroll effect. */
|
|
88
|
+
let scrollPane: HTMLElement | undefined;
|
|
89
|
+
|
|
90
|
+
/* Item paths match the chapter id verbatim so
|
|
91
|
+
`currentPath={parsedHash.chapter}` lights up the active row. */
|
|
92
|
+
const navSections: SideNavSection[] = [
|
|
93
|
+
{
|
|
94
|
+
path: 'guide',
|
|
95
|
+
title: 'Guide',
|
|
96
|
+
items: [
|
|
97
|
+
{ path: '01-overview', title: 'Overview' },
|
|
98
|
+
{ path: 'getting-started', title: 'Getting started' },
|
|
99
|
+
{ path: 'editing-tokens', title: 'Editing tokens' },
|
|
100
|
+
{ path: 'themes-workflow', title: 'Themes' },
|
|
101
|
+
{ path: 'creating-components', title: 'Creating components' },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/* ------------------------------------------------------------------ Marked configuration */
|
|
107
|
+
function slug(text: string): string {
|
|
108
|
+
return text
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.replace(/[^\w\s-]/g, '')
|
|
111
|
+
.replace(/\s+/g, '-')
|
|
112
|
+
.replace(/-{2,}/g, '-')
|
|
113
|
+
.replace(/^-|-$/g, '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* `currentChapterId` is read by the renderer overrides when emitting anchor
|
|
117
|
+
hrefs. Set per render before calling `marked.parser`. */
|
|
118
|
+
let currentChapterId = '';
|
|
119
|
+
|
|
120
|
+
marked.use({
|
|
121
|
+
gfm: true,
|
|
122
|
+
breaks: false,
|
|
123
|
+
renderer: {
|
|
124
|
+
heading(token: Tokens.Heading): string {
|
|
125
|
+
const text = (this as any).parser.parseInline(token.tokens);
|
|
126
|
+
const raw = token.tokens.map((t: any) => ('text' in t ? t.text : '')).join('');
|
|
127
|
+
const id = slug(raw);
|
|
128
|
+
const link = `#${currentChapterId}~${id}`;
|
|
129
|
+
return `<h${token.depth} id="${id}">${text}<a class="heading-anchor" href="${link}" aria-label="permalink">#</a></h${token.depth}>`;
|
|
130
|
+
},
|
|
131
|
+
link(token: Tokens.Link): string {
|
|
132
|
+
const text = (this as any).parser.parseInline(token.tokens);
|
|
133
|
+
let rewritten = token.href ?? '';
|
|
134
|
+
/* Rewrite intra-doc links to the hash router: `chapter.md` → `#chapter`,
|
|
135
|
+
`chapter.md#anchor` → `#chapter~anchor` (the page joins chapter and
|
|
136
|
+
anchor with `~`, not `#`). Chapter ids may be numbered or not. */
|
|
137
|
+
const intra = rewritten.match(/^([a-z0-9-]+)\.md(?:#(.+))?$/i);
|
|
138
|
+
if (intra) {
|
|
139
|
+
rewritten = `#${intra[1]}${intra[2] ? `~${intra[2]}` : ''}`;
|
|
140
|
+
}
|
|
141
|
+
const isExt = /^https?:\/\//.test(rewritten);
|
|
142
|
+
const attrs = isExt ? ' target="_blank" rel="noopener"' : '';
|
|
143
|
+
const titleAttr = token.title ? ` title="${token.title.replace(/"/g, '"')}"` : '';
|
|
144
|
+
return `<a href="${rewritten}"${attrs}${titleAttr}>${text}</a>`;
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/* ------------------------------------------------------------------ Segmenting */
|
|
150
|
+
function segmentMarkdown(md: string): Segment[] {
|
|
151
|
+
/* Drop the first H1 since the page chrome renders the chapter title via
|
|
152
|
+
SectionDivider. */
|
|
153
|
+
const tokens = marked.lexer(md) as Tokens.Generic[];
|
|
154
|
+
let strippedFirstH1 = false;
|
|
155
|
+
const filtered = tokens.filter((t) => {
|
|
156
|
+
if (!strippedFirstH1 && t.type === 'heading' && (t as Tokens.Heading).depth === 1) {
|
|
157
|
+
strippedFirstH1 = true;
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const out: Segment[] = [];
|
|
164
|
+
let buffer: Tokens.Generic[] = [];
|
|
165
|
+
|
|
166
|
+
const flush = () => {
|
|
167
|
+
if (buffer.length === 0) return;
|
|
168
|
+
out.push({ kind: 'html', html: marked.parser(buffer as any) });
|
|
169
|
+
buffer = [];
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
for (const t of filtered) {
|
|
173
|
+
if (t.type === 'code') {
|
|
174
|
+
flush();
|
|
175
|
+
out.push({ kind: 'code', lang: (t as Tokens.Code).lang, text: (t as Tokens.Code).text });
|
|
176
|
+
} else if (t.type === 'table') {
|
|
177
|
+
flush();
|
|
178
|
+
out.push({ kind: 'table', html: marked.parser([t] as any) });
|
|
179
|
+
} else {
|
|
180
|
+
buffer.push(t);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
flush();
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ------------------------------------------------------------------ Loading */
|
|
188
|
+
async function fetchChapter(id: string) {
|
|
189
|
+
loading = true;
|
|
190
|
+
error = null;
|
|
191
|
+
try {
|
|
192
|
+
const md = await loadChapter(id);
|
|
193
|
+
currentChapterId = id;
|
|
194
|
+
segments = segmentMarkdown(md);
|
|
195
|
+
chapterMeta = chapters.find((c) => c.id === id) ?? null;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
error = e instanceof Error ? e.message : String(e);
|
|
198
|
+
segments = [];
|
|
199
|
+
chapterMeta = null;
|
|
200
|
+
} finally {
|
|
201
|
+
loading = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* When the chapter changes (hash change), refetch and reset scroll/anchor. */
|
|
206
|
+
$effect(() => {
|
|
207
|
+
const id = parsedHash.chapter;
|
|
208
|
+
fetchChapter(id);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/* After segments render, scroll to the anchor (deep link) or hard-reset to
|
|
212
|
+
the top. Resetting the pane's own scrollTop is deterministic where the old
|
|
213
|
+
scrollIntoView on <main> was not. Reset both the pane (desktop scroller)
|
|
214
|
+
and the window (mobile, where the page scrolls) — the inactive one is a
|
|
215
|
+
no-op. Every plain link lands on the masthead, the constant "top of page". */
|
|
216
|
+
$effect(() => {
|
|
217
|
+
const anchor = parsedHash.anchor;
|
|
218
|
+
if (segments.length === 0) return;
|
|
219
|
+
tick().then(() => {
|
|
220
|
+
if (anchor) {
|
|
221
|
+
const el = document.getElementById(anchor);
|
|
222
|
+
if (el) {
|
|
223
|
+
el.scrollIntoView({ behavior: 'instant', block: 'start' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (scrollPane) scrollPane.scrollTop = 0;
|
|
228
|
+
window.scrollTo({ top: 0, behavior: 'instant' });
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/* ------------------------------------------------------------------ Lifecycle */
|
|
233
|
+
onMount(() => {
|
|
234
|
+
const handler = () => { hash = window.location.hash; };
|
|
235
|
+
window.addEventListener('hashchange', handler);
|
|
236
|
+
return () => window.removeEventListener('hashchange', handler);
|
|
237
|
+
});
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<svelte:head>
|
|
241
|
+
<!-- Inter is not part of the theme's font set; load it just for the brand wordmark. -->
|
|
242
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
243
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
|
244
|
+
<link
|
|
245
|
+
rel="stylesheet"
|
|
246
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
|
|
247
|
+
/>
|
|
248
|
+
</svelte:head>
|
|
249
|
+
|
|
250
|
+
<div class="docs-page">
|
|
251
|
+
<div class="docs-shell" class:collapsed={!sidebarOpen}>
|
|
252
|
+
<SideNavigation
|
|
253
|
+
class="docs-sidebar"
|
|
254
|
+
sections={navSections}
|
|
255
|
+
titleLabel="Live Tokens"
|
|
256
|
+
titleHref="#01-overview"
|
|
257
|
+
currentPath={parsedHash.chapter}
|
|
258
|
+
open={sidebarOpen}
|
|
259
|
+
ontoggle={() => (sidebarOpen = !sidebarOpen)}
|
|
260
|
+
>
|
|
261
|
+
{#snippet lead()}
|
|
262
|
+
<a class="rail-brand" href="https://motionproto.com/#top">MotionProto</a>
|
|
263
|
+
{/snippet}
|
|
264
|
+
|
|
265
|
+
{#snippet actions()}
|
|
266
|
+
<div class="rail-actions">
|
|
267
|
+
<a href="/demo" class="rail-action">
|
|
268
|
+
<Button variant="outline" size="small" icon="fas fa-box-open" fullWidth>
|
|
269
|
+
Demo Site
|
|
270
|
+
</Button>
|
|
271
|
+
</a>
|
|
272
|
+
<a href="/components" class="rail-action">
|
|
273
|
+
<Button variant="outline" size="small" icon="fas fa-puzzle-piece" fullWidth>
|
|
274
|
+
Components
|
|
275
|
+
</Button>
|
|
276
|
+
</a>
|
|
277
|
+
</div>
|
|
278
|
+
{/snippet}
|
|
279
|
+
</SideNavigation>
|
|
280
|
+
|
|
281
|
+
<div class="docs-main" bind:this={scrollPane}>
|
|
282
|
+
<header class="docs-page-header">
|
|
283
|
+
<div class="title-block">
|
|
284
|
+
<p class="eyebrow">Live Tokens</p>
|
|
285
|
+
<h1>Documentation</h1>
|
|
286
|
+
<p class="lede">
|
|
287
|
+
Set up a project, edit your tokens live, and ship a theme. A short
|
|
288
|
+
guide to styling and building with Live Tokens.
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
</header>
|
|
292
|
+
|
|
293
|
+
<main class="docs-content" id="docs-content-top" tabindex="-1">
|
|
294
|
+
{#if chapterMeta}
|
|
295
|
+
<SectionDivider title={chapterMeta.title} variant="lg" />
|
|
296
|
+
{/if}
|
|
297
|
+
|
|
298
|
+
{#if error}
|
|
299
|
+
<Notification
|
|
300
|
+
title="Could not load chapter"
|
|
301
|
+
description={error}
|
|
302
|
+
variant="warning"
|
|
303
|
+
/>
|
|
304
|
+
{:else if loading && segments.length === 0}
|
|
305
|
+
<p class="loading-row">
|
|
306
|
+
<span class="spinner" aria-hidden="true"></span>
|
|
307
|
+
Loading...
|
|
308
|
+
</p>
|
|
309
|
+
{:else}
|
|
310
|
+
<article class="prose">
|
|
311
|
+
{#each segments as seg, i (i)}
|
|
312
|
+
{#if seg.kind === 'html'}
|
|
313
|
+
<div class="md-html">{@html seg.html}</div>
|
|
314
|
+
{:else if seg.kind === 'code'}
|
|
315
|
+
{#if isShell(seg.lang)}
|
|
316
|
+
<div class="md-snippet">
|
|
317
|
+
<CodeSnippet code={seg.text} />
|
|
318
|
+
</div>
|
|
319
|
+
{:else}
|
|
320
|
+
<CodeBlock lang={seg.lang} text={seg.text} />
|
|
321
|
+
{/if}
|
|
322
|
+
{:else if seg.kind === 'table'}
|
|
323
|
+
<Table>{@html seg.html}</Table>
|
|
324
|
+
{/if}
|
|
325
|
+
{/each}
|
|
326
|
+
</article>
|
|
327
|
+
|
|
328
|
+
<nav class="chapter-footer" aria-label="Chapter navigation">
|
|
329
|
+
<div class="footer-slot prev">
|
|
330
|
+
{#if neighbours.prev}
|
|
331
|
+
<span class="footer-dir">Previous</span>
|
|
332
|
+
<a class="chapter-link" href={`#${neighbours.prev.id}`}>
|
|
333
|
+
<Button variant="outline" size="small" icon="fas fa-arrow-left">
|
|
334
|
+
{neighbours.prev.title}
|
|
335
|
+
</Button>
|
|
336
|
+
</a>
|
|
337
|
+
{/if}
|
|
338
|
+
</div>
|
|
339
|
+
<div class="footer-slot next">
|
|
340
|
+
{#if neighbours.next}
|
|
341
|
+
<span class="footer-dir">Next</span>
|
|
342
|
+
<a class="chapter-link" href={`#${neighbours.next.id}`}>
|
|
343
|
+
<Button variant="outline" size="small" icon="fas fa-arrow-right" iconPosition="right">
|
|
344
|
+
{neighbours.next.title}
|
|
345
|
+
</Button>
|
|
346
|
+
</a>
|
|
347
|
+
{/if}
|
|
348
|
+
</div>
|
|
349
|
+
</nav>
|
|
350
|
+
{/if}
|
|
351
|
+
</main>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<style>
|
|
357
|
+
/* Full-bleed dark surface fills the viewport; the centered shell inside
|
|
358
|
+
matches the demo page's 1440px band so navigating between docs and demo
|
|
359
|
+
doesn't shift the content block left/right. On desktop the docs surface is
|
|
360
|
+
locked to the viewport and only the content column scrolls (see the
|
|
361
|
+
min-width: 961px block); below that it reverts to natural page scroll. */
|
|
362
|
+
.docs-page {
|
|
363
|
+
background: var(--surface-neutral-lowest, #040c13);
|
|
364
|
+
color: var(--text-primary);
|
|
365
|
+
min-height: 100vh;
|
|
366
|
+
}
|
|
367
|
+
.docs-shell {
|
|
368
|
+
display: grid;
|
|
369
|
+
grid-template-columns: var(--sidenavigation-panel-open-width, 16rem) minmax(0, 1fr);
|
|
370
|
+
max-width: var(--columns-max-width, 1440px);
|
|
371
|
+
margin: 0 auto;
|
|
372
|
+
padding: 0 var(--space-32, 2rem);
|
|
373
|
+
transition: grid-template-columns var(--duration-200, 200ms) var(--ease-in-out-sine, ease);
|
|
374
|
+
}
|
|
375
|
+
/* The rail's own width tween (its width tokens) drives the column; keep the
|
|
376
|
+
grid track locked to the closed width so content reflows in step. */
|
|
377
|
+
.docs-shell.collapsed {
|
|
378
|
+
grid-template-columns: var(--sidenavigation-panel-closed-width, 3rem) minmax(0, 1fr);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.docs-main {
|
|
382
|
+
min-width: 0;
|
|
383
|
+
padding-bottom: var(--space-96, 6rem);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* -------------------------------------------------------------- Top header */
|
|
387
|
+
.docs-page-header {
|
|
388
|
+
border-bottom: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
|
|
389
|
+
padding: var(--space-48, 3rem) 0 var(--space-32, 2rem) var(--space-32, 2rem);
|
|
390
|
+
background: linear-gradient(
|
|
391
|
+
180deg,
|
|
392
|
+
color-mix(in srgb, var(--surface-neutral-lower, #162027) 50%, transparent),
|
|
393
|
+
transparent
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
.title-block {
|
|
397
|
+
margin: 0;
|
|
398
|
+
}
|
|
399
|
+
.title-block .eyebrow {
|
|
400
|
+
font-size: var(--font-size-xs, 0.75rem);
|
|
401
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
402
|
+
color: var(--text-accent, #009d9a);
|
|
403
|
+
text-transform: uppercase;
|
|
404
|
+
letter-spacing: var(--letter-spacing-wider, 0.12em);
|
|
405
|
+
margin: 0 0 var(--space-8, 0.5rem);
|
|
406
|
+
}
|
|
407
|
+
.title-block h1 {
|
|
408
|
+
font-family: var(--font-display, var(--font-sans));
|
|
409
|
+
font-size: var(--font-size-5xl, 3rem);
|
|
410
|
+
line-height: var(--line-height-xs, 1.1);
|
|
411
|
+
letter-spacing: var(--letter-spacing-tight, -0.02em);
|
|
412
|
+
margin: 0 0 var(--space-12, 0.75rem);
|
|
413
|
+
color: var(--text-primary);
|
|
414
|
+
}
|
|
415
|
+
.title-block .lede {
|
|
416
|
+
max-width: 60ch;
|
|
417
|
+
color: var(--text-secondary);
|
|
418
|
+
font-size: var(--font-size-md, 1rem);
|
|
419
|
+
margin: 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* -------------------------------------------------------------- Sidebar */
|
|
423
|
+
/* Footer actions injected via SideNavigation's `actions` slot — the component
|
|
424
|
+
owns the panel chrome and placement; this owns the gap below the nav and
|
|
425
|
+
the button stacking. :global because the snippet renders inside the child. */
|
|
426
|
+
:global(.rail-actions) {
|
|
427
|
+
margin-top: var(--space-96, 6rem);
|
|
428
|
+
display: flex;
|
|
429
|
+
flex-direction: column;
|
|
430
|
+
gap: var(--space-8, 0.5rem);
|
|
431
|
+
padding: 0 var(--space-16, 1rem) var(--space-16, 1rem);
|
|
432
|
+
}
|
|
433
|
+
:global(.rail-action) {
|
|
434
|
+
display: block;
|
|
435
|
+
text-decoration: none;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* MotionProto brand wordmark in the lead slot, matching the site's
|
|
439
|
+
`text-lg sm:text-xl font-bold tracking-wide hover:text-primary
|
|
440
|
+
transition-colors`. Inter and the literal Tailwind metrics (line-height,
|
|
441
|
+
tracking) are deliberate brand exceptions; Inter is loaded in <svelte:head>. */
|
|
442
|
+
:global(.rail-brand) {
|
|
443
|
+
display: block;
|
|
444
|
+
padding: var(--space-16, 1rem) var(--space-16, 1rem) var(--space-12, 0.75rem);
|
|
445
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
446
|
+
font-size: var(--font-size-lg, 1.125rem);
|
|
447
|
+
line-height: 1.75rem;
|
|
448
|
+
font-weight: var(--font-weight-bold, 700);
|
|
449
|
+
letter-spacing: 0.025em;
|
|
450
|
+
color: var(--text-primary);
|
|
451
|
+
text-decoration: none;
|
|
452
|
+
cursor: pointer;
|
|
453
|
+
transition: color var(--duration-150, 150ms) cubic-bezier(0.4, 0, 0.2, 1);
|
|
454
|
+
}
|
|
455
|
+
@media (min-width: 640px) {
|
|
456
|
+
:global(.rail-brand) {
|
|
457
|
+
font-size: var(--font-size-xl, 1.25rem);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
:global(.rail-brand:hover) {
|
|
461
|
+
color: var(--text-brand);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* -------------------------------------------------------------- Content */
|
|
465
|
+
.docs-content {
|
|
466
|
+
min-width: 0;
|
|
467
|
+
max-width: 760px;
|
|
468
|
+
outline: none;
|
|
469
|
+
padding: var(--space-32, 2rem) 0 0 var(--space-32, 2rem);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.loading-row {
|
|
473
|
+
display: inline-flex;
|
|
474
|
+
align-items: center;
|
|
475
|
+
gap: var(--space-12, 0.75rem);
|
|
476
|
+
color: var(--text-tertiary);
|
|
477
|
+
padding: var(--space-32, 2rem) 0;
|
|
478
|
+
}
|
|
479
|
+
.spinner {
|
|
480
|
+
display: inline-block;
|
|
481
|
+
width: 14px;
|
|
482
|
+
height: 14px;
|
|
483
|
+
border: var(--border-width-2, 2px) solid var(--border-neutral-subtle, #3a4146);
|
|
484
|
+
border-top-color: var(--text-accent, #009d9a);
|
|
485
|
+
border-radius: var(--radius-full, 9999px);
|
|
486
|
+
animation: spin var(--duration-750, 720ms) var(--ease-linear, linear) infinite;
|
|
487
|
+
}
|
|
488
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
489
|
+
|
|
490
|
+
.prose {
|
|
491
|
+
font-size: var(--font-size-md, 1rem);
|
|
492
|
+
line-height: var(--line-height-lg, 1.65);
|
|
493
|
+
}
|
|
494
|
+
.md-snippet {
|
|
495
|
+
margin: 0 0 var(--space-20, 1.25rem);
|
|
496
|
+
}
|
|
497
|
+
.prose :global(p),
|
|
498
|
+
.prose :global(ul),
|
|
499
|
+
.prose :global(ol) {
|
|
500
|
+
margin: 0 0 var(--space-16, 1rem);
|
|
501
|
+
color: var(--text-secondary, #c2cacf);
|
|
502
|
+
max-width: 68ch;
|
|
503
|
+
}
|
|
504
|
+
.prose :global(p) { max-width: 70ch; }
|
|
505
|
+
.prose :global(ul),
|
|
506
|
+
.prose :global(ol) { padding-left: var(--space-24, 1.5rem); }
|
|
507
|
+
.prose :global(li) { margin-bottom: var(--space-6, 0.375rem); }
|
|
508
|
+
.prose :global(li::marker) { color: var(--text-tertiary); }
|
|
509
|
+
.prose :global(strong) {
|
|
510
|
+
color: var(--text-primary);
|
|
511
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
512
|
+
}
|
|
513
|
+
.prose :global(em) { color: var(--text-primary); font-style: italic; }
|
|
514
|
+
|
|
515
|
+
.prose :global(h1),
|
|
516
|
+
.prose :global(h2),
|
|
517
|
+
.prose :global(h3),
|
|
518
|
+
.prose :global(h4) {
|
|
519
|
+
font-family: var(--font-display, var(--font-sans));
|
|
520
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
521
|
+
letter-spacing: var(--letter-spacing-tight, -0.015em);
|
|
522
|
+
line-height: var(--line-height-sm, 1.2);
|
|
523
|
+
color: var(--text-primary);
|
|
524
|
+
position: relative;
|
|
525
|
+
}
|
|
526
|
+
.prose :global(h2) {
|
|
527
|
+
font-size: var(--font-size-2xl, 1.5rem);
|
|
528
|
+
margin: var(--space-48, 3rem) 0 var(--space-16, 1rem);
|
|
529
|
+
padding-top: var(--space-12, 0.75rem);
|
|
530
|
+
border-top: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
|
|
531
|
+
}
|
|
532
|
+
.prose :global(h3) {
|
|
533
|
+
font-size: var(--font-size-xl, 1.25rem);
|
|
534
|
+
margin: var(--space-32, 2rem) 0 var(--space-12, 0.75rem);
|
|
535
|
+
}
|
|
536
|
+
.prose :global(h4) {
|
|
537
|
+
font-size: var(--font-size-lg, 1.125rem);
|
|
538
|
+
margin: var(--space-24, 1.5rem) 0 var(--space-8, 0.5rem);
|
|
539
|
+
color: var(--text-secondary);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.prose :global(.heading-anchor) {
|
|
543
|
+
margin-left: var(--space-8, 0.5rem);
|
|
544
|
+
color: var(--text-tertiary);
|
|
545
|
+
text-decoration: none;
|
|
546
|
+
font-weight: var(--font-weight-normal);
|
|
547
|
+
font-size: 0.7em;
|
|
548
|
+
opacity: 0;
|
|
549
|
+
transition: opacity var(--duration-150, 120ms) var(--ease-in-out-sine, ease),
|
|
550
|
+
color var(--duration-150, 120ms) var(--ease-in-out-sine, ease);
|
|
551
|
+
}
|
|
552
|
+
.prose :global(h1:hover .heading-anchor),
|
|
553
|
+
.prose :global(h2:hover .heading-anchor),
|
|
554
|
+
.prose :global(h3:hover .heading-anchor),
|
|
555
|
+
.prose :global(h4:hover .heading-anchor) { opacity: 1; }
|
|
556
|
+
.prose :global(.heading-anchor:hover) { color: var(--text-accent); }
|
|
557
|
+
|
|
558
|
+
.prose :global(a:not(.heading-anchor)) {
|
|
559
|
+
color: var(--text-accent, #009d9a);
|
|
560
|
+
text-decoration: none;
|
|
561
|
+
border-bottom: var(--border-width-1, 1px) solid color-mix(in srgb, var(--text-accent, #009d9a) 35%, transparent);
|
|
562
|
+
transition: color var(--duration-150, 120ms) var(--ease-in-out-sine, ease),
|
|
563
|
+
border-color var(--duration-150, 120ms) var(--ease-in-out-sine, ease);
|
|
564
|
+
}
|
|
565
|
+
.prose :global(a:not(.heading-anchor):hover) {
|
|
566
|
+
color: color-mix(in srgb, var(--text-accent, #009d9a) 75%, var(--color-white, #fff));
|
|
567
|
+
border-bottom-color: currentColor;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.prose :global(.md-html :not(pre) > code) {
|
|
571
|
+
font-family: var(--font-mono, monospace);
|
|
572
|
+
font-size: 0.875em;
|
|
573
|
+
background: var(--surface-neutral-lower, #162027);
|
|
574
|
+
color: var(--text-accent, #009d9a);
|
|
575
|
+
padding: 0.15em 0.45em;
|
|
576
|
+
border-radius: var(--radius-md, 0.25rem);
|
|
577
|
+
border: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
|
|
578
|
+
white-space: nowrap;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.prose :global(blockquote) {
|
|
582
|
+
margin: 0 0 var(--space-20, 1.25rem);
|
|
583
|
+
padding: var(--space-12, 0.75rem) var(--space-20, 1.25rem);
|
|
584
|
+
border-left: var(--border-width-3, 3px) solid var(--text-accent, #009d9a);
|
|
585
|
+
background: color-mix(in srgb, var(--surface-neutral-lower, #162027) 60%, transparent);
|
|
586
|
+
border-radius: 0 var(--radius-md, 0.25rem) var(--radius-md, 0.25rem) 0;
|
|
587
|
+
color: var(--text-secondary);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.prose :global(hr) {
|
|
591
|
+
border: 0;
|
|
592
|
+
border-top: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
|
|
593
|
+
margin: var(--space-32, 2rem) 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* GFM task lists (the verification checklists in archived chapters) */
|
|
597
|
+
.prose :global(ul.contains-task-list) { list-style: none; padding-left: var(--space-4); }
|
|
598
|
+
.prose :global(li.task-list-item) {
|
|
599
|
+
display: flex;
|
|
600
|
+
align-items: flex-start;
|
|
601
|
+
gap: var(--space-8, 0.5rem);
|
|
602
|
+
}
|
|
603
|
+
.prose :global(li.task-list-item input[type="checkbox"]) {
|
|
604
|
+
margin-top: 0.32em;
|
|
605
|
+
accent-color: var(--text-accent, #009d9a);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* -------------------------------------------------------------- Footer */
|
|
609
|
+
.chapter-footer {
|
|
610
|
+
margin-top: var(--space-48, 3rem);
|
|
611
|
+
padding-top: var(--space-24, 1.5rem);
|
|
612
|
+
border-top: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
|
|
613
|
+
display: grid;
|
|
614
|
+
grid-template-columns: 1fr 1fr;
|
|
615
|
+
gap: var(--space-16, 1rem);
|
|
616
|
+
align-items: end;
|
|
617
|
+
}
|
|
618
|
+
.footer-slot { display: flex; flex-direction: column; gap: var(--space-6, 0.375rem); }
|
|
619
|
+
.footer-slot.next { align-items: flex-end; }
|
|
620
|
+
.footer-dir {
|
|
621
|
+
font-size: var(--font-size-xs, 0.75rem);
|
|
622
|
+
color: var(--text-tertiary);
|
|
623
|
+
text-transform: uppercase;
|
|
624
|
+
letter-spacing: var(--letter-spacing-wider, 0.08em);
|
|
625
|
+
}
|
|
626
|
+
.chapter-link { text-decoration: none; }
|
|
627
|
+
|
|
628
|
+
/* -------------------------------------------------------------- Responsive */
|
|
629
|
+
/* Desktop: lock the docs surface to the viewport so only the content column
|
|
630
|
+
scrolls — the rail is full-height and immovable, and there is never an
|
|
631
|
+
empty band under the page. The router wrapper (.lt-app) assumes window-
|
|
632
|
+
scroll pages (min-height + a 12rem bottom pad); neutralise that here, for
|
|
633
|
+
the docs route only, via :has. The doubled .lt-app outranks the wrapper's
|
|
634
|
+
own scoped rule on a specificity tie. */
|
|
635
|
+
@media (min-width: 961px) {
|
|
636
|
+
:global(.lt-app.lt-app:has(.docs-page)) {
|
|
637
|
+
height: 100vh;
|
|
638
|
+
min-height: 0;
|
|
639
|
+
padding-bottom: 0;
|
|
640
|
+
overflow: hidden;
|
|
641
|
+
}
|
|
642
|
+
.docs-page { height: 100%; overflow: hidden; }
|
|
643
|
+
/* minmax(0, 1fr) pins the row to the viewport height so the pane below can
|
|
644
|
+
scroll; an auto row would grow to content and break the scroll. */
|
|
645
|
+
.docs-shell { height: 100%; grid-template-rows: minmax(0, 1fr); }
|
|
646
|
+
.docs-main { height: 100%; overflow-y: auto; }
|
|
647
|
+
.docs-shell :global(.docs-sidebar) { height: 100%; }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* Below 960px the rail stacks above the content; the page scrolls naturally. */
|
|
651
|
+
@media (max-width: 960px) {
|
|
652
|
+
.docs-shell,
|
|
653
|
+
.docs-shell.collapsed { grid-template-columns: 1fr; }
|
|
654
|
+
}
|
|
655
|
+
@media (max-width: 600px) {
|
|
656
|
+
.docs-shell { padding: 0 var(--space-16, 1rem); }
|
|
657
|
+
}
|
|
658
|
+
</style>
|