@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.
@@ -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, '&quot;')}"` : '';
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>
@@ -0,0 +1,2 @@
1
+ import { SvelteComponent } from 'svelte';
2
+ export default class Docs extends SvelteComponent<Record<string, never>> {}