@tayacrystals/lore 0.1.2 → 1.0.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.
Files changed (49) hide show
  1. package/README.md +7 -0
  2. package/package.json +48 -36
  3. package/src/build.ts +148 -0
  4. package/src/config.ts +26 -0
  5. package/src/dev.ts +112 -0
  6. package/src/files.ts +193 -0
  7. package/src/i18n.ts +49 -0
  8. package/src/icons.ts +59 -0
  9. package/src/index.ts +28 -0
  10. package/src/mdx.ts +281 -0
  11. package/src/parse.ts +53 -0
  12. package/src/routing.ts +46 -0
  13. package/src/serve.ts +72 -0
  14. package/src/template.ts +747 -0
  15. package/src/types.ts +51 -0
  16. package/src/version.ts +33 -0
  17. package/components/docs/Breadcrumbs.astro +0 -41
  18. package/components/docs/PrevNext.astro +0 -50
  19. package/components/docs/Sidebar.astro +0 -28
  20. package/components/docs/SidebarGroup.astro +0 -55
  21. package/components/docs/SidebarItem.astro +0 -26
  22. package/components/docs/TableOfContents.astro +0 -82
  23. package/components/global/SearchModal.astro +0 -159
  24. package/components/mdx/Accordion.astro +0 -20
  25. package/components/mdx/Callout.astro +0 -53
  26. package/components/mdx/Card.astro +0 -26
  27. package/components/mdx/CardGrid.astro +0 -16
  28. package/components/mdx/CodeTabs.astro +0 -129
  29. package/components/mdx/FileTree.astro +0 -117
  30. package/components/mdx/Step.astro +0 -18
  31. package/components/mdx/Steps.astro +0 -6
  32. package/components/mdx/Tab.astro +0 -11
  33. package/components/mdx/Tabs.astro +0 -73
  34. package/components.ts +0 -11
  35. package/config.ts +0 -42
  36. package/index.ts +0 -2
  37. package/integration.ts +0 -68
  38. package/layouts/DocsLayout.astro +0 -277
  39. package/loaders.ts +0 -5
  40. package/routes/docs.astro +0 -201
  41. package/schema.ts +0 -13
  42. package/styles/global.css +0 -78
  43. package/styles/prose.css +0 -148
  44. package/utils/navigation.ts +0 -32
  45. package/utils/rehype-file-tree.ts +0 -229
  46. package/utils/sidebar.ts +0 -97
  47. package/utils/toc.ts +0 -28
  48. package/virtual.d.ts +0 -9
  49. package/vite-plugin.ts +0 -28
@@ -0,0 +1,747 @@
1
+ import type { Config } from "./types.ts";
2
+ import type { SidebarItem } from "./types.ts";
3
+ import type { VersionInfo, LocaleInfo } from "./types.ts";
4
+ import { resolveColor } from "./config.ts";
5
+ import { linkIcon, linkHref } from "./icons.ts";
6
+ import { buildUrl } from "./routing.ts";
7
+ import { getLocaleLabel } from "./i18n.ts";
8
+
9
+ const CHEVRON = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`;
10
+
11
+ function renderVersionSwitcher(
12
+ versions: VersionInfo[],
13
+ currentVersion: string | undefined,
14
+ currentLocale: string | undefined,
15
+ currentPath: string
16
+ ): string {
17
+ if (!versions.length) return "";
18
+
19
+ const items = versions
20
+ .map((v) => {
21
+ const isActive = v.name === currentVersion;
22
+ const url = buildUrl(currentPath, { locale: currentLocale, version: v.name });
23
+ return `<option value="${url}"${isActive ? " selected" : ""}>${escHtml(v.label ?? v.name)}</option>`;
24
+ })
25
+ .join("");
26
+
27
+ return `<select class="version-switcher" onchange="location.href = this.value;">
28
+ ${items}
29
+ </select>`;
30
+ }
31
+
32
+ function renderLanguageSwitcher(
33
+ locales: LocaleInfo[],
34
+ currentLocale: string | undefined,
35
+ currentVersion: string | undefined,
36
+ currentPath: string
37
+ ): string {
38
+ if (!locales.length) return "";
39
+
40
+ const items = locales
41
+ .map((l) => {
42
+ const isActive = l.code === currentLocale;
43
+ const url = buildUrl(currentPath, { locale: l.code, version: currentVersion });
44
+ return `<option value="${url}"${isActive ? " selected" : ""}>${escHtml(l.label ?? getLocaleLabel(l.code))}</option>`;
45
+ })
46
+ .join("");
47
+
48
+ return `<select class="locale-switcher" onchange="location.href = this.value;">
49
+ ${items}
50
+ </select>`;
51
+ }
52
+
53
+ function sectionContainsUrl(
54
+ items: SidebarItem[],
55
+ url: string,
56
+ locale?: string,
57
+ version?: string
58
+ ): boolean {
59
+ const prefix = buildUrl("", { locale, version });
60
+ for (const item of items) {
61
+ if (item.type === "page" && prefix + item.url === url) return true;
62
+ if (item.type === "section") {
63
+ if (prefix + item.url === url || sectionContainsUrl(item.items, url, locale, version)) return true;
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+
69
+ function renderSidebarItems(
70
+ items: SidebarItem[],
71
+ currentUrl: string,
72
+ locale?: string,
73
+ version?: string
74
+ ): string {
75
+ const prefix = buildUrl("", { locale, version });
76
+ const hasPrefix = prefix !== "/";
77
+
78
+ return items
79
+ .map((item) => {
80
+ let url: string;
81
+ if (item.url?.startsWith("/")) {
82
+ if (hasPrefix) {
83
+ url = item.url === "/" ? prefix : prefix + item.url;
84
+ } else {
85
+ url = item.url;
86
+ }
87
+ } else {
88
+ url = item.url ?? "#";
89
+ }
90
+
91
+ if (item.type === "page") {
92
+ const active = item.url === currentUrl;
93
+ return `<li><a href="${url}" class="sidebar-row${active ? " active" : ""}">${escHtml(item.title)}</a></li>`;
94
+ } else {
95
+ const open = sectionContainsUrl(item.items, currentUrl, locale, version) || item.url === currentUrl;
96
+ const active = item.url === currentUrl;
97
+ const titleEl = item.url
98
+ ? `<a href="${url}" class="section-link">${escHtml(item.title)}</a>`
99
+ : `<span class="section-link">${escHtml(item.title)}</span>`;
100
+ const inner = item.items.length > 0
101
+ ? `<ul class="section-items">${renderSidebarItems(item.items, currentUrl, locale, version)}</ul>`
102
+ : "";
103
+ return `<li class="section${open ? " open" : ""}" data-section="${escHtml(item.title)}">
104
+ <div class="sidebar-row${active ? " active" : ""}">
105
+ ${titleEl}
106
+ <button class="section-toggle" aria-label="Toggle section">${CHEVRON}</button>
107
+ </div>
108
+ ${inner}
109
+ </li>`;
110
+ }
111
+ })
112
+ .join("\n");
113
+ }
114
+
115
+ function renderHeaderLinks(links: Config["links"]): string {
116
+ if (!links || links.length === 0) return "";
117
+ return links
118
+ .map((link) => {
119
+ const url = escHtml(linkHref(link));
120
+ const icon = linkIcon(link);
121
+ return `<a href="${url}" class="header-icon-link" target="_blank" rel="noopener" aria-label="${url}">${icon}</a>`;
122
+ })
123
+ .join("");
124
+ }
125
+
126
+ function escHtml(str: string): string {
127
+ return str
128
+ .replace(/&/g, "&amp;")
129
+ .replace(/</g, "&lt;")
130
+ .replace(/>/g, "&gt;")
131
+ .replace(/"/g, "&quot;");
132
+ }
133
+
134
+ const SUN_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>`;
135
+ const MOON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>`;
136
+
137
+ function minifyCss(css: string): string {
138
+ return css
139
+ .replace(/\/\*[\s\S]*?\*\//g, "") // strip comments
140
+ .replace(/\s+/g, " ") // collapse whitespace
141
+ .replace(/ *([{}:;,]) */g, "$1") // remove spaces around punctuation
142
+ .replace(/;}/g, "}") // remove trailing semicolons
143
+ .trim();
144
+ }
145
+
146
+ export interface PageTemplateOptions {
147
+ config: Config;
148
+ title: string;
149
+ description?: string;
150
+ contentHtml: string;
151
+ sidebar: SidebarItem[];
152
+ currentUrl: string;
153
+ logoSrc?: string;
154
+ devMode?: boolean;
155
+ versions?: VersionInfo[];
156
+ locales?: LocaleInfo[];
157
+ currentVersion?: string;
158
+ currentLocale?: string;
159
+ translationOf?: string;
160
+ }
161
+
162
+ export function renderPage(opts: PageTemplateOptions): string {
163
+ const {
164
+ config,
165
+ title,
166
+ description,
167
+ contentHtml,
168
+ sidebar,
169
+ currentUrl,
170
+ logoSrc,
171
+ devMode,
172
+ versions,
173
+ locales,
174
+ currentVersion,
175
+ currentLocale,
176
+ translationOf,
177
+ } = opts;
178
+
179
+ const accent = resolveColor(config.color);
180
+ const homeUrl = buildUrl("", { locale: currentLocale, version: currentVersion });
181
+ const siteTitle = config.title ?? "Docs";
182
+ const pageTitle = title ? `${title} — ${siteTitle}` : siteTitle;
183
+ const metaDesc = description ?? config.description ?? "";
184
+
185
+ return `<!DOCTYPE html>
186
+ <html lang="en">
187
+ <head>
188
+ <meta charset="UTF-8">
189
+ <meta name="viewport" content="width=device-width, initial-scale=1">
190
+ <title>${escHtml(pageTitle)}</title>
191
+ ${logoSrc ? `<link rel="icon" href="${escHtml(logoSrc)}">` : ""}
192
+ ${metaDesc ? `<meta name="description" content="${escHtml(metaDesc)}">` : ""}
193
+ <script>
194
+ // Apply saved theme before render to prevent flash
195
+ (function() {
196
+ var t = localStorage.getItem('lore-theme');
197
+ if (t) document.documentElement.classList.add(t);
198
+ })();
199
+ </script>
200
+ <style>${CSS_MIN.replace("__ACCENT__", accent)}${COMPONENT_CSS_MIN}</style>
201
+ <noscript><style>
202
+ .section-items { display: block !important; }
203
+ .section-toggle { display: none !important; }
204
+ .tab-panel { display: block !important; }
205
+ .tab-list { display: none !important; }
206
+ @media (max-width: 768px) {
207
+ .sidebar { transform: none !important; position: static; border-right: none; border-bottom: 1px solid var(--border); }
208
+ .menu-btn { display: none !important; }
209
+ .layout { flex-direction: column; }
210
+ .content { margin-left: 0; }
211
+ }
212
+ </style></noscript>
213
+ </head>
214
+ <body>
215
+ <header>
216
+ <div class="header-inner">
217
+ <div class="header-left">
218
+ <button class="menu-btn" aria-label="Toggle menu" onclick="toggleSidebar()">
219
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round">
220
+ <line x1="2" y1="4.5" x2="16" y2="4.5"/>
221
+ <line x1="2" y1="9" x2="16" y2="9"/>
222
+ <line x1="2" y1="13.5" x2="16" y2="13.5"/>
223
+ </svg>
224
+ </button>
225
+ <a href="${escHtml(homeUrl)}" class="logo">${logoSrc ? `<img src="${escHtml(logoSrc)}" alt="" width="24" height="24" class="logo-img">` : ""}${escHtml(siteTitle)}</a>
226
+ </div>
227
+ <div class="header-right">
228
+ ${renderVersionSwitcher(versions ?? [], currentVersion, currentLocale, currentUrl)}
229
+ ${renderLanguageSwitcher(locales ?? [], currentLocale, currentVersion, currentUrl)}
230
+ <nav class="header-links">
231
+ ${renderHeaderLinks(config.links)}
232
+ </nav>
233
+ <button class="header-icon-link theme-btn" onclick="toggleTheme()" aria-label="Toggle theme">
234
+ <span class="icon-sun">${SUN_SVG}</span>
235
+ <span class="icon-moon">${MOON_SVG}</span>
236
+ </button>
237
+ </div>
238
+ </div>
239
+ </header>
240
+
241
+ <div class="overlay" onclick="toggleSidebar()"></div>
242
+
243
+ <div class="layout">
244
+ <aside class="sidebar">
245
+ <nav>
246
+ <ul class="sidebar-nav">
247
+ ${renderSidebarItems(sidebar, currentUrl, currentLocale, currentVersion)}
248
+ </ul>
249
+ </nav>
250
+ </aside>
251
+
252
+ <main class="content">
253
+ <article>
254
+ ${contentHtml}
255
+ </article>
256
+ </main>
257
+ </div>
258
+ <script>
259
+ function toggleSidebar() {
260
+ document.body.classList.toggle('sidebar-open');
261
+ }
262
+ document.querySelectorAll('.sidebar-row').forEach(function(link) {
263
+ link.addEventListener('click', function() {
264
+ document.body.classList.remove('sidebar-open');
265
+ });
266
+ });
267
+ document.querySelectorAll('.section[data-section]').forEach(function(section) {
268
+ var key = 'sidebar:' + section.dataset.section;
269
+ if (section.classList.contains('open')) {
270
+ localStorage.setItem(key, '1');
271
+ } else {
272
+ if (localStorage.getItem(key) === '1') section.classList.add('open');
273
+ }
274
+ });
275
+ document.querySelectorAll('.section-toggle').forEach(function(btn) {
276
+ btn.addEventListener('click', function() {
277
+ var section = btn.closest('.section');
278
+ section.classList.toggle('open');
279
+ var key = section.dataset.section;
280
+ if (key) localStorage.setItem('sidebar:' + key, section.classList.contains('open') ? '1' : '0');
281
+ });
282
+ });
283
+ // Tabs
284
+ function activateTab(tabs, index) {
285
+ tabs.querySelectorAll('.tab-btn').forEach(function(b, i) { b.classList.toggle('active', i === index); });
286
+ tabs.querySelectorAll('.tab-panel').forEach(function(p, i) { p.classList.toggle('active', i === index); });
287
+ }
288
+ document.querySelectorAll('.tabs').forEach(function(tabs) {
289
+ var group = tabs.dataset.group;
290
+ // Restore saved tab for this group
291
+ if (group) {
292
+ var saved = localStorage.getItem('tab:' + group);
293
+ if (saved) {
294
+ tabs.querySelectorAll('.tab-btn').forEach(function(btn, i) {
295
+ if (btn.textContent === saved) activateTab(tabs, i);
296
+ });
297
+ }
298
+ }
299
+ tabs.querySelectorAll('.tab-btn').forEach(function(btn, i) {
300
+ btn.addEventListener('click', function() {
301
+ activateTab(tabs, i);
302
+ if (group) {
303
+ var label = btn.textContent;
304
+ localStorage.setItem('tab:' + group, label);
305
+ // Sync other tab groups with the same group name
306
+ document.querySelectorAll('.tabs[data-group="' + group + '"]').forEach(function(other) {
307
+ if (other !== tabs) {
308
+ other.querySelectorAll('.tab-btn').forEach(function(ob, oi) {
309
+ if (ob.textContent === label) activateTab(other, oi);
310
+ });
311
+ }
312
+ });
313
+ }
314
+ });
315
+ });
316
+ });
317
+
318
+ function toggleTheme() {
319
+ var html = document.documentElement;
320
+ var isDark = html.classList.contains('dark') ||
321
+ (!html.classList.contains('light') && window.matchMedia('(prefers-color-scheme: dark)').matches);
322
+ if (isDark) {
323
+ html.classList.replace('dark', 'light') || html.classList.add('light');
324
+ localStorage.setItem('lore-theme', 'light');
325
+ } else {
326
+ html.classList.replace('light', 'dark') || html.classList.add('dark');
327
+ localStorage.setItem('lore-theme', 'dark');
328
+ }
329
+ }
330
+ </script>
331
+ ${devMode ? `<script>new EventSource("/_lore/reload").onmessage = () => location.reload();</script>` : ""}
332
+ </body>
333
+ </html>`;
334
+ }
335
+
336
+ const CSS = `
337
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
338
+
339
+ :root {
340
+ --bg: #ffffff;
341
+ --bg-sidebar: #f6f8fa;
342
+ --bg-hover: #eef0f2;
343
+ --text: #1c1c1e;
344
+ --text-muted: #636366;
345
+ --border: #e5e5ea;
346
+ --accent: __ACCENT__;
347
+ --sidebar-w: 272px;
348
+ --header-h: 56px;
349
+ }
350
+
351
+ /* Dark theme via explicit class (user override) */
352
+ :root.dark {
353
+ --bg: #111113;
354
+ --bg-sidebar: #1c1c1e;
355
+ --bg-hover: #2c2c2e;
356
+ --text: #f5f5f7;
357
+ --text-muted: #98989f;
358
+ --border: #2c2c2e;
359
+ }
360
+
361
+ /* Dark theme via system preference (when no explicit override) */
362
+ @media (prefers-color-scheme: dark) {
363
+ :root:not(.light) {
364
+ --bg: #111113;
365
+ --bg-sidebar: #1c1c1e;
366
+ --bg-hover: #2c2c2e;
367
+ --text: #f5f5f7;
368
+ --text-muted: #98989f;
369
+ --border: #2c2c2e;
370
+ }
371
+ }
372
+
373
+ body {
374
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
375
+ font-size: 16px;
376
+ line-height: 1.6;
377
+ color: var(--text);
378
+ background: var(--bg);
379
+ }
380
+
381
+ /* Header */
382
+ header {
383
+ position: fixed;
384
+ top: 0; left: 0; right: 0;
385
+ height: var(--header-h);
386
+ background: var(--bg);
387
+ border-bottom: 1px solid var(--border);
388
+ z-index: 100;
389
+ display: flex;
390
+ align-items: center;
391
+ }
392
+ .header-inner {
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: space-between;
396
+ width: 100%;
397
+ padding: 0 20px;
398
+ }
399
+ .header-left { display: flex; align-items: center; gap: 8px; }
400
+ .header-right { display: flex; align-items: center; gap: 2px; }
401
+ .logo-img { height: 24px; width: auto; }
402
+ .logo {
403
+ display: flex; align-items: center; gap: 8px;
404
+ font-size: 18px;
405
+ font-weight: 700;
406
+ color: var(--text);
407
+ text-decoration: none;
408
+ letter-spacing: -0.01em;
409
+ }
410
+ .menu-btn {
411
+ display: none;
412
+ align-items: center;
413
+ justify-content: center;
414
+ width: 36px;
415
+ height: 36px;
416
+ background: none;
417
+ border: none;
418
+ border-radius: 6px;
419
+ color: var(--text);
420
+ cursor: pointer;
421
+ flex-shrink: 0;
422
+ }
423
+ .menu-btn:hover { background: var(--bg-hover); }
424
+ .header-links { display: flex; gap: 2px; align-items: center; }
425
+ .header-icon-link {
426
+ display: flex;
427
+ align-items: center;
428
+ justify-content: center;
429
+ width: 34px;
430
+ height: 34px;
431
+ color: var(--text-muted);
432
+ background: none;
433
+ border: none;
434
+ border-radius: 6px;
435
+ cursor: pointer;
436
+ text-decoration: none;
437
+ transition: color 0.15s, background 0.15s;
438
+ }
439
+ .header-icon-link:hover { color: var(--text); background: var(--bg-hover); }
440
+
441
+ /* Theme toggle icons */
442
+ .theme-btn .icon-sun { display: none; }
443
+ .theme-btn .icon-moon { display: flex; }
444
+ :root.dark .theme-btn .icon-sun { display: flex; }
445
+ :root.dark .theme-btn .icon-moon { display: none; }
446
+ @media (prefers-color-scheme: dark) {
447
+ :root:not(.light) .theme-btn .icon-sun { display: flex; }
448
+ :root:not(.light) .theme-btn .icon-moon { display: none; }
449
+ }
450
+
451
+ /* Overlay (mobile) */
452
+ .overlay {
453
+ display: none;
454
+ position: fixed;
455
+ inset: 0;
456
+ background: rgba(0, 0, 0, 0.4);
457
+ z-index: 49;
458
+ }
459
+ body.sidebar-open .overlay { display: block; }
460
+
461
+ /* Layout */
462
+ .layout {
463
+ display: flex;
464
+ margin-top: var(--header-h);
465
+ min-height: calc(100vh - var(--header-h));
466
+ }
467
+
468
+ /* Sidebar */
469
+ .sidebar {
470
+ position: fixed;
471
+ top: var(--header-h);
472
+ bottom: 0;
473
+ left: 0;
474
+ width: var(--sidebar-w);
475
+ background: var(--bg-sidebar);
476
+ border-right: 1px solid var(--border);
477
+ overflow-y: auto;
478
+ padding: 16px 0;
479
+ z-index: 50;
480
+ }
481
+ .sidebar-nav { list-style: none; }
482
+ .sidebar-nav li { list-style: none; }
483
+ /* Unified sidebar row — used by both page links and section headers */
484
+ .sidebar-row {
485
+ display: flex;
486
+ align-items: center;
487
+ padding: 5px 8px 5px 16px;
488
+ margin: 1px 8px;
489
+ border-radius: 6px;
490
+ font-size: 14px;
491
+ color: var(--text-muted);
492
+ text-decoration: none;
493
+ transition: color 0.15s, background 0.15s;
494
+ }
495
+ .sidebar-row:hover { color: var(--text); background: var(--bg-hover); }
496
+ .sidebar-row.active {
497
+ color: var(--accent);
498
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
499
+ font-weight: 500;
500
+ }
501
+ /* Section-specific: title link inherits row style, toggle sits at the end */
502
+ .section-link {
503
+ flex: 1;
504
+ color: inherit;
505
+ text-decoration: none;
506
+ white-space: nowrap;
507
+ overflow: hidden;
508
+ text-overflow: ellipsis;
509
+ }
510
+ .section-toggle {
511
+ flex-shrink: 0;
512
+ display: flex;
513
+ align-items: center;
514
+ justify-content: center;
515
+ width: 24px;
516
+ height: 24px;
517
+ background: none;
518
+ border: none;
519
+ color: inherit;
520
+ cursor: pointer;
521
+ }
522
+ .section-toggle svg { transition: transform 0.2s ease; }
523
+ .section.open .section-toggle svg { transform: rotate(90deg); }
524
+ .section-items {
525
+ display: none;
526
+ list-style: none;
527
+ padding-left: 12px;
528
+ }
529
+ .section.open .section-items { display: block; }
530
+
531
+ /* Content */
532
+ .content {
533
+ margin-left: var(--sidebar-w);
534
+ padding: 48px 64px;
535
+ flex: 1;
536
+ min-width: 0;
537
+ }
538
+ article { max-width: 780px; }
539
+
540
+ /* Typography */
541
+ article h1 { font-size: 2rem; font-weight: 700; margin-bottom: 16px; line-height: 1.2; }
542
+ article h2 { font-size: 1.4rem; font-weight: 600; margin: 40px 0 12px; padding-top: 8px; border-top: 1px solid var(--border); }
543
+ article h3 { font-size: 1.15rem; font-weight: 600; margin: 28px 0 8px; }
544
+ article h4 { font-size: 1rem; font-weight: 600; margin: 20px 0 8px; }
545
+ article h1, article h2, article h3, article h4 { position: relative; }
546
+ .heading-anchor { margin-left: 0.4em; color: var(--text-muted); opacity: 0; text-decoration: none; font-weight: 400; transition: opacity 0.15s; }
547
+ :is(h1, h2, h3, h4):hover .heading-anchor { opacity: 1; }
548
+ .heading-anchor:hover { color: var(--accent); }
549
+ article p { margin-bottom: 16px; }
550
+ article ul, article ol { margin-bottom: 16px; padding-left: 24px; }
551
+ article li { margin-bottom: 4px; }
552
+ article a { color: var(--accent); text-decoration: none; }
553
+ article a:hover { text-decoration: underline; }
554
+ article strong { font-weight: 600; }
555
+ article code {
556
+ font-family: 'SF Mono', ui-monospace, Consolas, monospace;
557
+ font-size: 0.85em;
558
+ background: var(--bg-hover);
559
+ border: 1px solid var(--border);
560
+ border-radius: 4px;
561
+ padding: 1px 5px;
562
+ }
563
+ article pre {
564
+ background: var(--bg-sidebar);
565
+ border: 1px solid var(--border);
566
+ border-radius: 8px;
567
+ padding: 16px 20px;
568
+ overflow-x: auto;
569
+ margin-bottom: 16px;
570
+ }
571
+ article pre code {
572
+ background: none;
573
+ border: none;
574
+ padding: 0;
575
+ font-size: 0.875rem;
576
+ line-height: 1.7;
577
+ }
578
+ article blockquote {
579
+ border-left: 3px solid var(--accent);
580
+ padding-left: 16px;
581
+ margin: 0 0 16px;
582
+ color: var(--text-muted);
583
+ }
584
+ article hr { border: none; border-top: 1px solid var(--border); margin: 32px 0; }
585
+ article table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
586
+ article th, article td { padding: 8px 12px; text-align: left; border: 1px solid var(--border); }
587
+ article th { background: var(--bg-sidebar); font-weight: 600; }
588
+
589
+ /* Mobile */
590
+ @media (max-width: 768px) {
591
+ .menu-btn { display: flex; }
592
+ .sidebar {
593
+ transform: translateX(-100%);
594
+ transition: transform 0.25s ease;
595
+ }
596
+ body.sidebar-open .sidebar { transform: translateX(0); }
597
+ .content {
598
+ margin-left: 0;
599
+ padding: 32px 20px;
600
+ }
601
+ article h1 { font-size: 1.6rem; }
602
+ article h2 { font-size: 1.25rem; }
603
+ }
604
+ `;
605
+
606
+ const COMPONENT_CSS = `
607
+ /* ── Version/Locale Switchers ─────────────────────────────────── */
608
+ .version-switcher, .locale-switcher {
609
+ appearance: none;
610
+ -webkit-appearance: none;
611
+ background: var(--bg-hover);
612
+ border: 1px solid var(--border);
613
+ border-radius: 6px;
614
+ padding: 4px 28px 4px 10px;
615
+ font-size: 13px;
616
+ font-weight: 500;
617
+ color: var(--text);
618
+ cursor: pointer;
619
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23636366' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
620
+ background-repeat: no-repeat;
621
+ background-position: right 8px center;
622
+ background-size: 12px;
623
+ margin-right: 4px;
624
+ }
625
+ .version-switcher:hover, .locale-switcher:hover {
626
+ border-color: var(--text-muted);
627
+ }
628
+
629
+ /* ── Shiki dual-theme ───────────────────────────────────── */
630
+ .shiki, .shiki span { color: var(--shiki-light) !important; background-color: var(--shiki-light-bg) !important; }
631
+ :root.dark .shiki, :root.dark .shiki span { color: var(--shiki-dark) !important; background-color: var(--shiki-dark-bg) !important; }
632
+ @media (prefers-color-scheme: dark) {
633
+ :root:not(.light) .shiki, :root:not(.light) .shiki span { color: var(--shiki-dark) !important; background-color: var(--shiki-dark-bg) !important; }
634
+ }
635
+
636
+ /* ── Code blocks ────────────────────────────────────────── */
637
+ .code-block { margin-bottom: 16px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
638
+ .code-block .shiki { margin: 0; border: none; border-radius: 0; padding: 16px 20px; }
639
+ .code-frame {
640
+ display: flex; align-items: center;
641
+ padding: 0 16px;
642
+ height: 36px;
643
+ background: var(--bg-hover);
644
+ border-bottom: 1px solid var(--border);
645
+ font-family: 'SF Mono', ui-monospace, Consolas, monospace;
646
+ font-size: 12px;
647
+ color: var(--text-muted);
648
+ gap: 8px;
649
+ }
650
+ .code-frame::before {
651
+ content: '';
652
+ display: block;
653
+ width: 10px; height: 10px;
654
+ border-radius: 50%;
655
+ background: var(--border);
656
+ box-shadow: 16px 0 0 var(--border), 32px 0 0 var(--border);
657
+ flex-shrink: 0;
658
+ }
659
+ .code-frame-terminal::before { background: #ff5f57; box-shadow: 16px 0 0 #febc2e, 32px 0 0 #28c840; }
660
+ .code-frame span { margin-left: 16px; }
661
+
662
+ /* ── Callouts ───────────────────────────────────────────── */
663
+ .callout {
664
+ display: flex; gap: 12px;
665
+ padding: 14px 16px; margin-bottom: 16px;
666
+ border-radius: 8px; border: 1px solid;
667
+ }
668
+ .callout-note { background: color-mix(in srgb, #3b82f6 10%, transparent); border-color: color-mix(in srgb, #3b82f6 30%, transparent); }
669
+ .callout-tip { background: color-mix(in srgb, #22c55e 10%, transparent); border-color: color-mix(in srgb, #22c55e 30%, transparent); }
670
+ .callout-warning { background: color-mix(in srgb, #eab308 10%, transparent); border-color: color-mix(in srgb, #eab308 30%, transparent); }
671
+ .callout-danger { background: color-mix(in srgb, #ef4444 10%, transparent); border-color: color-mix(in srgb, #ef4444 30%, transparent); }
672
+ .callout-icon { flex-shrink: 0; display: flex; align-items: flex-start; padding-top: 2px; }
673
+ .callout-note .callout-icon { color: #3b82f6; }
674
+ .callout-tip .callout-icon { color: #22c55e; }
675
+ .callout-warning .callout-icon { color: #eab308; }
676
+ .callout-danger .callout-icon { color: #ef4444; }
677
+ .callout-body { flex: 1; min-width: 0; }
678
+ .callout-body > *:last-child { margin-bottom: 0; }
679
+
680
+ /* ── File tree ──────────────────────────────────────────── */
681
+ .file-tree {
682
+ font-family: 'SF Mono', ui-monospace, Consolas, monospace;
683
+ font-size: 13px;
684
+ background: var(--bg-sidebar);
685
+ border: 1px solid var(--border);
686
+ border-radius: 8px;
687
+ padding: 12px 16px;
688
+ margin-bottom: 16px;
689
+ }
690
+ .file-tree ul { list-style: none; padding-left: 20px; margin-bottom: 0; }
691
+ .file-tree > ul { padding-left: 0; }
692
+ .file-tree li { padding: 2px 0; }
693
+ .tree-row { display: flex; align-items: center; gap: 6px; }
694
+ .tree-icon { display: flex; flex-shrink: 0; color: var(--text-muted); }
695
+ .tree-dir-icon { color: var(--accent); }
696
+
697
+ /* ── Steps ──────────────────────────────────────────────── */
698
+ .steps { margin-bottom: 16px; }
699
+ .step { display: flex; gap: 16px; margin-bottom: 28px; }
700
+ .step-num {
701
+ flex-shrink: 0;
702
+ width: 26px; height: 26px;
703
+ border-radius: 50%;
704
+ background: var(--accent);
705
+ color: #fff;
706
+ display: flex; align-items: center; justify-content: center;
707
+ font-size: 13px; font-weight: 600;
708
+ margin-top: 1px;
709
+ }
710
+ .step-body { flex: 1; min-width: 0; }
711
+ .step-title { font-weight: 600; margin-bottom: 8px; }
712
+ .step-body > *:last-child { margin-bottom: 0; }
713
+
714
+ /* ── Tabs ───────────────────────────────────────────────── */
715
+ .tabs { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 16px; }
716
+ .tab-list {
717
+ display: flex;
718
+ background: var(--bg-sidebar);
719
+ border-bottom: 1px solid var(--border);
720
+ padding: 8px 8px 0;
721
+ gap: 2px;
722
+ overflow-x: auto;
723
+ scrollbar-width: none;
724
+ }
725
+ .tab-list::-webkit-scrollbar { display: none; }
726
+ .tab-btn {
727
+ padding: 6px 14px;
728
+ border: none; border-radius: 6px 6px 0 0;
729
+ background: none;
730
+ font-size: 13px; font-weight: 500;
731
+ color: var(--text-muted);
732
+ cursor: pointer;
733
+ white-space: nowrap;
734
+ border-bottom: 2px solid transparent;
735
+ margin-bottom: -1px;
736
+ transition: color 0.15s, background 0.15s;
737
+ }
738
+ .tab-btn:hover { color: var(--text); background: var(--bg-hover); }
739
+ .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); background: var(--bg); }
740
+ .tab-panels { }
741
+ .tab-panel { display: none; padding: 16px 20px; }
742
+ .tab-panel.active { display: block; }
743
+ .tab-panel > *:last-child { margin-bottom: 0; }
744
+ `;
745
+
746
+ const CSS_MIN = minifyCss(CSS);
747
+ const COMPONENT_CSS_MIN = minifyCss(COMPONENT_CSS);