@tiramisu-docs/kit 0.1.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 (99) hide show
  1. package/README.md +103 -0
  2. package/components.json +14 -0
  3. package/dist/bin/mcp.d.ts +2 -0
  4. package/dist/bin/mcp.js +4 -0
  5. package/dist/config.d.ts +99 -0
  6. package/dist/config.js +36 -0
  7. package/dist/highlight.d.ts +10 -0
  8. package/dist/highlight.js +93 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.js +3 -0
  11. package/dist/lib/components/index.d.ts +16 -0
  12. package/dist/lib/components/index.js +18 -0
  13. package/dist/lib/components/tiramisu/lang-icons.d.ts +4 -0
  14. package/dist/lib/components/tiramisu/lang-icons.js +77 -0
  15. package/dist/lib/components/ui/alert/index.d.ts +5 -0
  16. package/dist/lib/components/ui/alert/index.js +6 -0
  17. package/dist/lib/components/ui/badge/index.d.ts +2 -0
  18. package/dist/lib/components/ui/badge/index.js +1 -0
  19. package/dist/lib/components/ui/button/index.d.ts +4 -0
  20. package/dist/lib/components/ui/button/index.js +2 -0
  21. package/dist/lib/components/ui/card/index.d.ts +8 -0
  22. package/dist/lib/components/ui/card/index.js +10 -0
  23. package/dist/lib/components/ui/collapsible/index.d.ts +1 -0
  24. package/dist/lib/components/ui/collapsible/index.js +1 -0
  25. package/dist/lib/components/ui/dropdown-menu/index.d.ts +18 -0
  26. package/dist/lib/components/ui/dropdown-menu/index.js +18 -0
  27. package/dist/lib/components/ui/scroll-area/index.d.ts +1 -0
  28. package/dist/lib/components/ui/scroll-area/index.js +1 -0
  29. package/dist/lib/components/ui/separator/index.d.ts +1 -0
  30. package/dist/lib/components/ui/separator/index.js +1 -0
  31. package/dist/lib/components/ui/sheet/index.d.ts +3 -0
  32. package/dist/lib/components/ui/sheet/index.js +3 -0
  33. package/dist/lib/components/ui/tabs/index.d.ts +5 -0
  34. package/dist/lib/components/ui/tabs/index.js +7 -0
  35. package/dist/lib/open-links.d.ts +22 -0
  36. package/dist/lib/open-links.js +33 -0
  37. package/dist/lib/routes/docs/[...slug]/+page.d.ts +25 -0
  38. package/dist/lib/routes/docs/[...slug]/+page.js +109 -0
  39. package/dist/lib/utils.d.ts +5 -0
  40. package/dist/lib/utils.js +5 -0
  41. package/dist/mcp.d.ts +24 -0
  42. package/dist/mcp.js +155 -0
  43. package/dist/scan.d.ts +15 -0
  44. package/dist/scan.js +72 -0
  45. package/dist/seo.d.ts +63 -0
  46. package/dist/seo.js +160 -0
  47. package/dist/tiramisu-grammar.d.ts +2 -0
  48. package/dist/tiramisu-grammar.js +77 -0
  49. package/dist/types.d.ts +66 -0
  50. package/dist/types.js +1 -0
  51. package/dist/vite.d.ts +33 -0
  52. package/dist/vite.js +406 -0
  53. package/package.json +74 -0
  54. package/src/config.ts +133 -0
  55. package/src/highlight.ts +110 -0
  56. package/src/index.ts +6 -0
  57. package/src/lib/components/DocPage.svelte +430 -0
  58. package/src/lib/components/DocsLayout.svelte +145 -0
  59. package/src/lib/components/Footer.svelte +26 -0
  60. package/src/lib/components/Navbar.svelte +117 -0
  61. package/src/lib/components/PageFooter.svelte +63 -0
  62. package/src/lib/components/PrevNextNav.svelte +83 -0
  63. package/src/lib/components/SearchDialog.svelte +130 -0
  64. package/src/lib/components/Sidebar.svelte +237 -0
  65. package/src/lib/components/TableOfContents.svelte +50 -0
  66. package/src/lib/components/TopBar.svelte +407 -0
  67. package/src/lib/components/index.ts +19 -0
  68. package/src/lib/components/tiramisu/Accordion.svelte +16 -0
  69. package/src/lib/components/tiramisu/Badge.svelte +16 -0
  70. package/src/lib/components/tiramisu/Callout.svelte +26 -0
  71. package/src/lib/components/tiramisu/CodeBlock.svelte +56 -0
  72. package/src/lib/components/tiramisu/CodeTabs.svelte +123 -0
  73. package/src/lib/components/tiramisu/Demo.svelte +15 -0
  74. package/src/lib/components/tiramisu/FileTree.svelte +67 -0
  75. package/src/lib/components/tiramisu/MathBlock.svelte +26 -0
  76. package/src/lib/components/tiramisu/Mermaid.svelte +30 -0
  77. package/src/lib/components/tiramisu/NavCard.svelte +49 -0
  78. package/src/lib/components/tiramisu/Steps.svelte +60 -0
  79. package/src/lib/components/tiramisu/Tabs.svelte +87 -0
  80. package/src/lib/components/tiramisu/ZoomImage.svelte +114 -0
  81. package/src/lib/components/tiramisu/lang-icons.ts +81 -0
  82. package/src/lib/open-links.ts +50 -0
  83. package/src/lib/routes/docs/[...slug]/+page.svelte +26 -0
  84. package/src/lib/routes/docs/[...slug]/+page.ts +117 -0
  85. package/src/lib/styles/theme.css +222 -0
  86. package/src/lib/utils.ts +10 -0
  87. package/src/mcp.ts +180 -0
  88. package/src/scan.ts +92 -0
  89. package/src/seo.ts +193 -0
  90. package/src/tiramisu-grammar.ts +80 -0
  91. package/src/types.ts +71 -0
  92. package/src/virtual.d.ts +11 -0
  93. package/src/vite.ts +478 -0
  94. package/tests/config.test.ts +60 -0
  95. package/tests/mcp.test.ts +116 -0
  96. package/tests/scan.test.ts +48 -0
  97. package/tests/seo.test.ts +174 -0
  98. package/tests/vite.test.ts +283 -0
  99. package/tsconfig.json +19 -0
@@ -0,0 +1,110 @@
1
+ import { createHighlighter, type Highlighter } from "shiki"
2
+ import { tiramisuGrammar } from "./tiramisu-grammar.js"
3
+
4
+ let highlighter: Highlighter | null = null
5
+
6
+ async function getHighlighter(): Promise<Highlighter> {
7
+ if (!highlighter) {
8
+ highlighter = await createHighlighter({
9
+ themes: ["github-light", "github-dark"],
10
+ langs: [
11
+ "typescript",
12
+ "javascript",
13
+ "bash",
14
+ "html",
15
+ "css",
16
+ "json",
17
+ "svelte",
18
+ "yaml",
19
+ "markdown",
20
+ "tsx",
21
+ "jsx",
22
+ "shell",
23
+ tiramisuGrammar,
24
+ ],
25
+ })
26
+ }
27
+ return highlighter
28
+ }
29
+
30
+ /**
31
+ * Post-process compiled Svelte output to add syntax highlighting.
32
+ *
33
+ * Finds patterns like:
34
+ * const __code_0 = "escaped code"
35
+ * <CodeBlock language="typescript" code={__code_0} />
36
+ *
37
+ * Replaces the code string with Shiki-highlighted HTML.
38
+ */
39
+ export async function highlightCodeBlocks(svelte: string): Promise<string> {
40
+ // Find all CodeBlock usages to map varName -> language
41
+ const langMap = new Map<string, string>()
42
+ const codeBlockRegex =
43
+ /<CodeBlock\s+language="([^"]*)"\s+code=\{(__code_\d+)\}\s*\/>/g
44
+ let match
45
+ while ((match = codeBlockRegex.exec(svelte)) !== null) {
46
+ langMap.set(match[2], match[1])
47
+ }
48
+
49
+ // Find CodeTabs usages to map varName -> language
50
+ const codeTabsRegex =
51
+ /<CodeTabs\s[^>]*codes=\{\[([^\]]*)\]\}\s+langMap=\{\[([^\]]*)\]\}\s*\/>/g
52
+ while ((match = codeTabsRegex.exec(svelte)) !== null) {
53
+ const vars = match[1].split(/,\s*/).map(s => s.trim())
54
+ const langs = match[2].split(/,\s*/).map(s => s.replace(/^"|"$/g, ""))
55
+ for (let i = 0; i < vars.length; i++) {
56
+ if (vars[i] && langs[i]) langMap.set(vars[i], langs[i])
57
+ }
58
+ }
59
+
60
+ if (langMap.size === 0) return svelte
61
+
62
+ const hl = await getHighlighter()
63
+ const loadedLangs = hl.getLoadedLanguages()
64
+
65
+ // Find and replace each __code_N declaration
66
+ const codeVarRegex = /const (__code_\d+) = ("(?:[^"\\]|\\.)*")/g
67
+ let result = svelte
68
+ const replacements: [string, string][] = []
69
+
70
+ while ((match = codeVarRegex.exec(svelte)) !== null) {
71
+ const varName = match[1]
72
+ const jsonStr = match[2]
73
+ const language = langMap.get(varName)
74
+
75
+ if (!language) continue
76
+
77
+ // Decode the JSON string, then unescape HTML entities to get raw code
78
+ const escapedHtml: string = JSON.parse(jsonStr)
79
+ const rawCode = escapedHtml
80
+ .replace(/&amp;/g, "&")
81
+ .replace(/&lt;/g, "<")
82
+ .replace(/&gt;/g, ">")
83
+ .replace(/&quot;/g, '"')
84
+
85
+ const lang = loadedLangs.includes(language) ? language : "text"
86
+
87
+ const highlighted = hl.codeToHtml(rawCode, {
88
+ lang,
89
+ themes: { light: "github-light", dark: "github-dark" },
90
+ defaultColor: false,
91
+ })
92
+
93
+ // Extract just the inner content from shiki's <pre><code>...</code></pre>
94
+ const innerMatch = highlighted.match(
95
+ /<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/
96
+ )
97
+ const inner = innerMatch ? innerMatch[1] : escapedHtml
98
+
99
+ replacements.push([
100
+ `const ${varName} = ${jsonStr}`,
101
+ `const ${varName} = ${JSON.stringify(inner)}`,
102
+ ])
103
+ }
104
+
105
+ for (const [from, to] of replacements) {
106
+ result = result.replace(from, to)
107
+ }
108
+
109
+ return result
110
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { defineConfig, resolveConfig } from "./config.js"
2
+ export type { TiramisuDocsConfig, ResolvedConfig, SectionConfig, LocaleConfig, I18nConfig, GitHubConfig, FooterConfig, FooterSocials, InstantOgConfig } from "./config.js"
3
+ export type { SidebarItem, SidebarSubgroup, SidebarEntry, SidebarGroup, VirtualDoc, SearchIndexEntry, ResolvedSection, LocaleData, VirtualModule } from "./types.js"
4
+ export { generateSitemap, generateLlmsTxt, generateLlmsFullTxt, generateSkillMd, buildCanonicalUrl, buildPageJsonLd, buildInstantOgUrl } from "./seo.js"
5
+ export { getOpenLinks, getGitHubEditUrl, getPageUrl } from "./lib/open-links.js"
6
+ export type { OpenLink, OpenLinkOptions } from "./lib/open-links.js"
@@ -0,0 +1,430 @@
1
+ <script lang="ts">
2
+ import type { DocMeta } from "@tiramisu-docs/core"
3
+ import type { Snippet } from "svelte"
4
+ import type { SidebarGroup, SidebarEntry } from "../../types.js"
5
+ import type { InstantOgConfig } from "../../config.js"
6
+
7
+ let {
8
+ meta,
9
+ children,
10
+ slug = undefined,
11
+ baseUrl = undefined,
12
+ lastEdited = undefined,
13
+ headerActions = undefined,
14
+ sidebar = [],
15
+ siteName = undefined,
16
+ instantOg = undefined,
17
+ }: { meta: DocMeta; children: Snippet; slug?: string; baseUrl?: string; lastEdited?: string; headerActions?: Snippet; sidebar?: SidebarGroup[]; siteName?: string; instantOg?: InstantOgConfig } = $props();
18
+
19
+ function findGroup(sidebar: SidebarGroup[], slug: string): string | null {
20
+ for (const group of sidebar) {
21
+ for (const entry of group.items) {
22
+ if (entry.type === "item" && entry.slug === slug) return group.label;
23
+ if (entry.type === "subgroup") {
24
+ if (entry.slug === slug) return group.label;
25
+ for (const child of entry.items) {
26
+ if (child.type === "item" && child.slug === slug)
27
+ return entry.label;
28
+ }
29
+ }
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ const groupName = $derived(
36
+ slug && sidebar.length ? findGroup(sidebar, slug) : null,
37
+ );
38
+
39
+ function timeAgo(iso: string): string {
40
+ const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
41
+ if (seconds < 60) return "just now";
42
+ const minutes = Math.floor(seconds / 60);
43
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
44
+ const hours = Math.floor(minutes / 60);
45
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
46
+ const days = Math.floor(hours / 24);
47
+ if (days < 30) return `${days} day${days === 1 ? "" : "s"} ago`;
48
+ const months = Math.floor(days / 30);
49
+ if (months < 12) return `${months} month${months === 1 ? "" : "s"} ago`;
50
+ const years = Math.floor(months / 12);
51
+ return `${years} year${years === 1 ? "" : "s"} ago`;
52
+ }
53
+
54
+ function formatDate(iso: string): string {
55
+ return new Date(iso).toLocaleDateString("en-US", {
56
+ year: "numeric",
57
+ month: "long",
58
+ day: "numeric",
59
+ });
60
+ }
61
+
62
+ import { buildPageJsonLd, buildCanonicalUrl, buildInstantOgUrl } from "../../seo.js";
63
+
64
+ const canonicalUrl = $derived(slug && baseUrl ? buildCanonicalUrl(baseUrl, slug) : null);
65
+
66
+ const explicitImage = $derived(
67
+ meta?.image && baseUrl && !meta.image.startsWith("http")
68
+ ? `${baseUrl.replace(/\/+$/, "")}${meta.image.startsWith("/") ? "" : "/"}${meta.image}`
69
+ : meta?.image ?? null
70
+ );
71
+
72
+ const imageUrl = $derived(
73
+ explicitImage
74
+ ?? (instantOg && canonicalUrl
75
+ ? buildInstantOgUrl(canonicalUrl, { siteId: instantOg.siteId, template: instantOg.template, theme: instantOg.theme, accentColor: instantOg.accentColor, gradientBg: instantOg.gradientBg })
76
+ : null)
77
+ );
78
+
79
+ const jsonLd = $derived(
80
+ slug && baseUrl
81
+ ? buildPageJsonLd({
82
+ title: meta?.title ?? slug,
83
+ slug,
84
+ baseUrl,
85
+ description: meta?.description,
86
+ lastEdited,
87
+ siteName,
88
+ image: imageUrl ?? undefined,
89
+ author: meta?.author,
90
+ })
91
+ : null
92
+ );
93
+ </script>
94
+
95
+ <svelte:head>
96
+ {#if meta?.title}
97
+ <title>{siteName ? `${meta.title} | ${siteName}` : meta.title}</title>
98
+ <meta property="og:title" content={meta.title} />
99
+ <meta name="twitter:title" content={meta.title} />
100
+ {/if}
101
+ {#if meta?.description}
102
+ <meta name="description" content={meta.description} />
103
+ <meta property="og:description" content={meta.description} />
104
+ <meta name="twitter:description" content={meta.description} />
105
+ {/if}
106
+ {#if siteName}
107
+ <meta property="og:site_name" content={siteName} />
108
+ {/if}
109
+ <meta property="og:type" content="article" />
110
+ <meta name="twitter:card" content={imageUrl ? "summary_large_image" : "summary"} />
111
+ {#if imageUrl}
112
+ <meta property="og:image" content={imageUrl} />
113
+ <meta name="twitter:image" content={imageUrl} />
114
+ {/if}
115
+ {#if canonicalUrl}
116
+ <link rel="canonical" href={canonicalUrl} />
117
+ <meta property="og:url" content={canonicalUrl} />
118
+ {/if}
119
+ {#if jsonLd}
120
+ {@html `<script type="application/ld+json">${jsonLd}</script>`}
121
+ {/if}
122
+ </svelte:head>
123
+
124
+ <article class="doc-content">
125
+ {#if meta?.title}
126
+ <div class="mb-10 border-b pb-8">
127
+ {#if groupName}
128
+ <p
129
+ class="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70"
130
+ >
131
+ {groupName}
132
+ </p>
133
+ {/if}
134
+ <div class="flex items-start justify-between gap-4">
135
+ <h1
136
+ class="flex items-center gap-3 text-3xl font-bold tracking-tight text-foreground lg:text-4xl"
137
+ >
138
+ {#if meta.icon}
139
+ <iconify-icon icon={meta.icon.includes(":") ? meta.icon : `lucide:${meta.icon}`} width="32" height="32" class="shrink-0 text-muted-foreground"></iconify-icon>
140
+ {/if}
141
+ {meta.title}
142
+ </h1>
143
+ {#if headerActions}
144
+ {@render headerActions()}
145
+ {/if}
146
+ </div>
147
+ {#if meta?.description}
148
+ <p class="mt-3 text-lg text-muted-foreground">
149
+ {meta.description}
150
+ </p>
151
+ {/if}
152
+ </div>
153
+ {/if}
154
+
155
+ {#key meta?.title}
156
+ <div class="doc-body doc-animate">
157
+ {@render children()}
158
+ </div>
159
+ {/key}
160
+
161
+ {#if lastEdited || meta?.author}
162
+ <div class="mt-10 flex items-center gap-2 pt-4 text-sm text-muted-foreground">
163
+ {#if meta?.author}
164
+ <span>{meta.author}</span>
165
+ {/if}
166
+ {#if meta?.author && lastEdited}
167
+ <span class="text-border">·</span>
168
+ {/if}
169
+ {#if lastEdited}
170
+ <time
171
+ datetime={lastEdited}
172
+ title={formatDate(lastEdited)}
173
+ >
174
+ Last edited {timeAgo(lastEdited)}
175
+ </time>
176
+ {/if}
177
+ </div>
178
+ {/if}
179
+ </article>
180
+
181
+ <style>
182
+ .doc-animate {
183
+ animation: doc-fade-in 0.2s ease-out;
184
+ }
185
+
186
+ @keyframes doc-fade-in {
187
+ from {
188
+ opacity: 0;
189
+ transform: translateY(4px);
190
+ }
191
+ to {
192
+ opacity: 1;
193
+ transform: translateY(0);
194
+ }
195
+ }
196
+ .doc-body :global(h1),
197
+ .doc-body :global(h2),
198
+ .doc-body :global(h3),
199
+ .doc-body :global(h4),
200
+ .doc-body :global(h5),
201
+ .doc-body :global(h6) {
202
+ font-weight: 700;
203
+ letter-spacing: -0.025em;
204
+ color: var(--foreground);
205
+ scroll-margin-top: 4rem;
206
+ }
207
+
208
+ .doc-body :global(h1) {
209
+ font-size: 2.25rem;
210
+ line-height: 1.2;
211
+ margin-top: 3rem;
212
+ margin-bottom: 1.25rem;
213
+ }
214
+
215
+ .doc-body :global(h2) {
216
+ font-size: 1.5rem;
217
+ line-height: 1.33;
218
+ margin-top: 3rem;
219
+ margin-bottom: 1rem;
220
+ padding-bottom: 0.5rem;
221
+ border-bottom: 1px solid var(--border);
222
+ }
223
+
224
+ .doc-body :global(h3) {
225
+ font-size: 1.25rem;
226
+ line-height: 1.4;
227
+ margin-top: 2rem;
228
+ margin-bottom: 0.75rem;
229
+ }
230
+
231
+ .doc-body :global(h4) {
232
+ font-size: 1.125rem;
233
+ margin-top: 1.5rem;
234
+ margin-bottom: 0.5rem;
235
+ }
236
+
237
+ .doc-body :global(h2),
238
+ .doc-body :global(h3),
239
+ .doc-body :global(h4) {
240
+ position: relative;
241
+ }
242
+
243
+ .doc-body :global(p) {
244
+ font-size: 1rem;
245
+ line-height: 1.75;
246
+ color: var(--foreground);
247
+ margin-bottom: 1.25rem;
248
+ }
249
+
250
+ .doc-body :global(p a),
251
+ .doc-body :global(li a),
252
+ .doc-body :global(td a),
253
+ .doc-body :global(blockquote a) {
254
+ font-weight: 500;
255
+ text-decoration: underline;
256
+ text-underline-offset: 4px;
257
+ color: var(--foreground);
258
+ transition: opacity 0.2s;
259
+ }
260
+
261
+ .doc-body :global(p a:hover),
262
+ .doc-body :global(li a:hover),
263
+ .doc-body :global(td a:hover),
264
+ .doc-body :global(blockquote a:hover) {
265
+ opacity: 0.8;
266
+ }
267
+
268
+ .doc-body :global(h1 > a),
269
+ .doc-body :global(h2 > a),
270
+ .doc-body :global(h3 > a),
271
+ .doc-body :global(h4 > a) {
272
+ color: inherit;
273
+ font-weight: inherit;
274
+ }
275
+
276
+ .doc-body :global(strong) {
277
+ font-weight: 600;
278
+ color: var(--foreground);
279
+ }
280
+
281
+ .doc-body :global(code) {
282
+ font-family: var(--font-mono);
283
+ font-size: 0.8125rem;
284
+ font-weight: 500;
285
+ background: var(--muted);
286
+ color: var(--foreground);
287
+ padding: 0.125rem 0.375rem;
288
+ border-radius: 0.25rem;
289
+ }
290
+
291
+ .doc-body :global(pre) {
292
+ font-family: var(--font-mono);
293
+ font-size: 0.8125rem;
294
+ line-height: 1.7;
295
+ background: oklch(0.975 0 0);
296
+ color: oklch(0.25 0 0);
297
+ padding: 1rem 1.25rem;
298
+ margin: 0;
299
+ overflow-x: auto;
300
+ }
301
+
302
+ :global(.dark) .doc-body :global(pre) {
303
+ background: oklch(0.105 0 0);
304
+ color: oklch(0.87 0 0);
305
+ }
306
+
307
+ /* Shiki dual-theme: use light colors by default, dark in dark mode */
308
+ .doc-body :global(pre span) {
309
+ color: var(--shiki-light);
310
+ }
311
+
312
+ :global(.dark) .doc-body :global(pre span) {
313
+ color: var(--shiki-dark);
314
+ }
315
+
316
+ .doc-body :global(pre code) {
317
+ background: none;
318
+ border: none;
319
+ padding: 0;
320
+ color: inherit;
321
+ font-size: inherit;
322
+ }
323
+
324
+ .doc-body :global(ul),
325
+ .doc-body :global(ol) {
326
+ padding-left: 1.5rem;
327
+ margin-bottom: 1.25rem;
328
+ }
329
+
330
+ .doc-body :global(ul) {
331
+ list-style: disc;
332
+ }
333
+
334
+ .doc-body :global(ol) {
335
+ list-style: decimal;
336
+ }
337
+
338
+ .doc-body :global(li) {
339
+ font-size: 1rem;
340
+ line-height: 1.75;
341
+ margin-bottom: 0.375rem;
342
+ color: var(--foreground);
343
+ }
344
+
345
+ .doc-body :global(table) {
346
+ width: 100%;
347
+ border-collapse: collapse;
348
+ margin: 1.5rem 0;
349
+ font-size: 0.875rem;
350
+ }
351
+
352
+ .doc-body :global(th) {
353
+ font-weight: 600;
354
+ text-align: left;
355
+ padding: 0.75rem 1rem;
356
+ border-bottom: 2px solid var(--border);
357
+ color: var(--muted-foreground);
358
+ }
359
+
360
+ .doc-body :global(td) {
361
+ padding: 0.75rem 1rem;
362
+ border-bottom: 1px solid var(--border);
363
+ }
364
+
365
+ .doc-body :global(tr:last-child td) {
366
+ border-bottom: none;
367
+ }
368
+
369
+ .doc-body :global(blockquote) {
370
+ border-left: 2px solid var(--border);
371
+ padding-left: 1.5rem;
372
+ margin: 1.5rem 0;
373
+ font-style: italic;
374
+ color: var(--muted-foreground);
375
+ }
376
+
377
+ .doc-body :global(blockquote p) {
378
+ color: var(--muted-foreground);
379
+ }
380
+
381
+ .doc-body :global(img) {
382
+ border-radius: var(--radius);
383
+ border: 1px solid var(--border);
384
+ margin: 1.5rem 0;
385
+ }
386
+
387
+ .doc-body :global(.cards-grid img) {
388
+ border: none;
389
+ margin: 0;
390
+ border-radius: 0;
391
+ }
392
+
393
+ .doc-body :global(hr) {
394
+ border: none;
395
+ border-top: 1px solid var(--border);
396
+ margin: 2.5rem 0;
397
+ }
398
+
399
+ /* Heading anchor # on hover — uses :global block to avoid scoping pseudo-elements */
400
+ :global(.doc-body h2 > a::before),
401
+ :global(.doc-body h3 > a::before),
402
+ :global(.doc-body h4 > a::before) {
403
+ content: "#";
404
+ position: absolute;
405
+ left: -1.5rem;
406
+ color: var(--muted-foreground);
407
+ font-weight: 400;
408
+ opacity: 0;
409
+ transition: opacity 0.15s;
410
+ }
411
+
412
+ :global(.doc-body h2:hover > a::before),
413
+ :global(.doc-body h3:hover > a::before),
414
+ :global(.doc-body h4:hover > a::before) {
415
+ opacity: 0.6;
416
+ }
417
+
418
+ .doc-body :global(.external-link) {
419
+ display: inline-flex;
420
+ align-items: baseline;
421
+ gap: 0.2em;
422
+ }
423
+
424
+ .doc-body :global(.external-link-icon) {
425
+ display: inline;
426
+ vertical-align: middle;
427
+ opacity: 0.5;
428
+ flex-shrink: 0;
429
+ }
430
+ </style>
@@ -0,0 +1,145 @@
1
+ <script lang="ts">
2
+ import "iconify-icon"
3
+ import TopBar from "./TopBar.svelte"
4
+ import Navbar from "./Navbar.svelte"
5
+ import Sidebar from "./Sidebar.svelte"
6
+ import TableOfContents from "./TableOfContents.svelte"
7
+ import SearchDialog from "./SearchDialog.svelte"
8
+ import PageFooter from "./PageFooter.svelte"
9
+ import { SheetContent } from "$lib/components/ui/sheet/index.js"
10
+ import { ScrollArea } from "$lib/components/ui/scroll-area/index.js"
11
+ import { onMount } from "svelte"
12
+ import type { ResolvedConfig, LocaleConfig } from "../../config.js"
13
+ import type { SidebarGroup, SidebarEntry, ResolvedSection } from "../../types.js"
14
+ import type { Heading } from "@tiramisu-docs/core"
15
+ import type { Snippet } from "svelte"
16
+
17
+ let {
18
+ config,
19
+ sidebar,
20
+ headings,
21
+ children,
22
+ sections,
23
+ locale,
24
+ locales,
25
+ showFallbackBanner = false,
26
+ }: { config: ResolvedConfig; sidebar: SidebarGroup[]; headings: Heading[]; children: Snippet; sections?: ResolvedSection[]; locale?: string; locales?: LocaleConfig[]; showFallbackBanner?: boolean } = $props()
27
+
28
+ let searchOpen = $state(false)
29
+ let mobileOpen = $state(false)
30
+
31
+ const hasSections = $derived(sections != null && sections.length > 0)
32
+
33
+ function docHref(slug: string): string {
34
+ const prefix = locale ? `/docs/${locale}` : "/docs"
35
+ if (slug === "index") return prefix
36
+ const clean = slug.replace(/\/index$/, "")
37
+ return `${prefix}/${clean}`
38
+ }
39
+
40
+ onMount(() => {
41
+ function handleKeydown(e: KeyboardEvent) {
42
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
43
+ e.preventDefault()
44
+ searchOpen = !searchOpen
45
+ }
46
+ }
47
+ document.addEventListener("keydown", handleKeydown)
48
+ return () => document.removeEventListener("keydown", handleKeydown)
49
+ })
50
+ </script>
51
+
52
+ <div class="min-h-screen bg-background text-foreground">
53
+ {#if hasSections}
54
+ <TopBar
55
+ {config}
56
+ {sections}
57
+ {locale}
58
+ {locales}
59
+ onSearchClick={() => (searchOpen = true)}
60
+ onMenuClick={() => (mobileOpen = !mobileOpen)}
61
+ />
62
+ {:else}
63
+ <!-- Legacy mobile navbar -->
64
+ <Navbar {config} {sidebar} onSearchClick={() => (searchOpen = true)} {locale} />
65
+ {/if}
66
+
67
+ {#if showFallbackBanner}
68
+ <div class="border-b bg-muted/50 px-4 py-2 text-center text-sm text-muted-foreground">
69
+ This page is not available in the selected language. Showing the default version.
70
+ </div>
71
+ {/if}
72
+
73
+ <div class="mx-auto flex max-w-[90rem]">
74
+ <!-- Sidebar (desktop) -->
75
+ <aside class="relative hidden w-[15rem] shrink-0 border-r lg:block">
76
+ <div class="absolute inset-0 bg-card" style="left: -100vw; width: calc(100% + 100vw);"></div>
77
+ <div class="relative sticky top-0 h-screen" style:top={hasSections ? "6rem" : "0"} style:height={hasSections ? "calc(100vh - 6rem)" : "100vh"}>
78
+ <Sidebar {config} groups={sidebar} onSearchClick={() => (searchOpen = true)} {hasSections} {locale} {locales} />
79
+ </div>
80
+ </aside>
81
+
82
+ <!-- Main content area -->
83
+ <main class="min-w-0 flex-1 px-6 py-10 lg:px-10 xl:px-14">
84
+ <div class="mx-auto max-w-3xl">
85
+ {@render children()}
86
+ </div>
87
+ </main>
88
+
89
+ <!-- Table of contents -->
90
+ {#if headings?.length}
91
+ <aside class="hidden w-[13rem] shrink-0 xl:block">
92
+ <div class="sticky overflow-y-auto overscroll-contain py-10 pr-6" style:top={hasSections ? "6rem" : "0"} style:height={hasSections ? "calc(100vh - 6rem)" : "100vh"}>
93
+ <TableOfContents {headings} />
94
+ </div>
95
+ </aside>
96
+ {/if}
97
+ </div>
98
+
99
+ <PageFooter {config} />
100
+ </div>
101
+
102
+ <SearchDialog bind:open={searchOpen} {locale} />
103
+
104
+ <!-- Mobile sidebar sheet (used when TopBar is active) -->
105
+ {#snippet renderMobileEntries(entries: SidebarEntry[], depth: number)}
106
+ {#each entries as entry}
107
+ {#if entry.type === "item"}
108
+ <a
109
+ href={docHref(entry.slug)}
110
+ onclick={() => (mobileOpen = false)}
111
+ class="block rounded-md py-1.5 text-sm text-muted-foreground hover:text-foreground"
112
+ style:padding-left="{0.5 + depth * 0.75}rem"
113
+ >
114
+ {entry.title}
115
+ </a>
116
+ {:else if entry.type === "subgroup"}
117
+ <div class="mt-2 mb-1">
118
+ <h5
119
+ class="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70"
120
+ style:padding-left="{0.5 + depth * 0.75}rem"
121
+ >{entry.label}</h5>
122
+ <div class="mt-0.5 space-y-0.5">
123
+ {@render renderMobileEntries(entry.items, depth + 1)}
124
+ </div>
125
+ </div>
126
+ {/if}
127
+ {/each}
128
+ {/snippet}
129
+
130
+ {#if hasSections}
131
+ <SheetContent open={mobileOpen} onclose={() => (mobileOpen = false)} side="left">
132
+ <div class="mt-6">
133
+ <ScrollArea class="h-[calc(100vh-8rem)]">
134
+ {#each sidebar as group}
135
+ <div class="mb-4">
136
+ <h4 class="mb-1 px-2 text-sm font-semibold text-foreground">{group.label}</h4>
137
+ <div class="space-y-0.5">
138
+ {@render renderMobileEntries(group.items, 0)}
139
+ </div>
140
+ </div>
141
+ {/each}
142
+ </ScrollArea>
143
+ </div>
144
+ </SheetContent>
145
+ {/if}