@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,26 @@
1
+ <a
2
+ href="https://tiramisudocs.com"
3
+ target="_blank"
4
+ rel="noopener noreferrer"
5
+ class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border bg-muted/40 px-3 py-1.5 text-[11px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
6
+ >
7
+ <!-- Light logo -->
8
+ <svg class="h-3 w-3 dark:hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
9
+ <rect x="3" y="16" width="18" height="3" rx="1" fill="#5c4a3a"/>
10
+ <rect x="4" y="11" width="16" height="3" rx="1" fill="#7a6555"/>
11
+ <rect x="5" y="6" width="14" height="3" rx="1" fill="#3e2e22"/>
12
+ <circle cx="8" cy="4" r="0.7" fill="#5c4a3a"/>
13
+ <circle cx="12" cy="3.5" r="0.7" fill="#3e2e22"/>
14
+ <circle cx="16" cy="4" r="0.7" fill="#5c4a3a"/>
15
+ </svg>
16
+ <!-- Dark logo -->
17
+ <svg class="hidden h-3 w-3 dark:block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
18
+ <rect x="3" y="16" width="18" height="3" rx="1" fill="#e8e0d8" opacity="0.35"/>
19
+ <rect x="4" y="11" width="16" height="3" rx="1" fill="#e8e0d8" opacity="0.6"/>
20
+ <rect x="5" y="6" width="14" height="3" rx="1" fill="#e8e0d8" opacity="0.9"/>
21
+ <circle cx="8" cy="4" r="0.7" fill="#e8e0d8" opacity="0.45"/>
22
+ <circle cx="12" cy="3.5" r="0.7" fill="#e8e0d8" opacity="0.6"/>
23
+ <circle cx="16" cy="4" r="0.7" fill="#e8e0d8" opacity="0.45"/>
24
+ </svg>
25
+ Powered by Tiramisu
26
+ </a>
@@ -0,0 +1,117 @@
1
+ <script lang="ts">
2
+ import { page } from "$app/stores"
3
+ import { SheetContent } from "$lib/components/ui/sheet/index.js"
4
+ import { ScrollArea } from "$lib/components/ui/scroll-area/index.js"
5
+ import { Collapsible } from "$lib/components/ui/collapsible/index.js"
6
+ import type { ResolvedConfig } from "../../config.js"
7
+ import type { SidebarGroup, SidebarEntry, SidebarSubgroup } from "../../types.js"
8
+
9
+ let { config, sidebar = [], onSearchClick, locale }: { config: ResolvedConfig; sidebar?: SidebarGroup[]; onSearchClick: () => void; locale?: string } = $props()
10
+ let mobileOpen = $state(false)
11
+
12
+ function docHref(slug: string): string {
13
+ const prefix = locale ? `/docs/${locale}` : "/docs"
14
+ return slug === "index" ? prefix : `${prefix}/${slug}`
15
+ }
16
+
17
+ function isSubgroupActive(entry: SidebarSubgroup, pathname: string): boolean {
18
+ if (entry.slug) {
19
+ const href = docHref(entry.slug)
20
+ if (pathname === href) return true
21
+ }
22
+ return hasActiveDescendant(entry.items, pathname)
23
+ }
24
+
25
+ function hasActiveDescendant(items: SidebarEntry[], pathname: string): boolean {
26
+ for (const entry of items) {
27
+ if (entry.type === "item") {
28
+ const href = docHref(entry.slug)
29
+ if (pathname === href) return true
30
+ } else if (entry.type === "subgroup") {
31
+ if (isSubgroupActive(entry, pathname)) return true
32
+ }
33
+ }
34
+ return false
35
+ }
36
+ </script>
37
+
38
+ {#snippet renderMobileEntries(entries: SidebarEntry[], depth: number)}
39
+ {#each entries as entry}
40
+ {#if entry.type === "item"}
41
+ <a
42
+ href={docHref(entry.slug)}
43
+ onclick={() => (mobileOpen = false)}
44
+ class="block rounded-md py-1.5 text-sm text-muted-foreground hover:text-foreground"
45
+ style:padding-left="{0.5 + depth * 0.75}rem"
46
+ >
47
+ {entry.title}
48
+ </a>
49
+ {:else}
50
+ {@const subActive = entry.slug && $page.url.pathname === docHref(entry.slug)}
51
+ <Collapsible open={isSubgroupActive(entry, $page.url.pathname)} class="mt-0.5">
52
+ {#snippet trigger()}
53
+ {#if entry.slug}
54
+ <a
55
+ href={docHref(entry.slug)}
56
+ class="text-sm font-medium transition-colors
57
+ {subActive ? 'text-primary' : 'text-muted-foreground hover:text-foreground'}"
58
+ style:padding-left="{0.5 + depth * 0.75}rem"
59
+ onclick={(e: MouseEvent) => { e.stopPropagation(); mobileOpen = false; }}
60
+ >
61
+ {entry.label}
62
+ </a>
63
+ {:else}
64
+ <span
65
+ class="text-sm font-medium text-muted-foreground"
66
+ style:padding-left="{0.5 + depth * 0.75}rem"
67
+ >
68
+ {entry.label}
69
+ </span>
70
+ {/if}
71
+ {/snippet}
72
+ <div class="mt-0.5">
73
+ {@render renderMobileEntries(entry.items, depth + 1)}
74
+ </div>
75
+ </Collapsible>
76
+ {/if}
77
+ {/each}
78
+ {/snippet}
79
+
80
+ <!-- Mobile-only top bar -->
81
+ <header class="sticky top-0 z-50 flex h-14 items-center border-b bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/60 lg:hidden">
82
+ <button
83
+ onclick={() => (mobileOpen = true)}
84
+ class="mr-3 inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
85
+ aria-label="Toggle menu"
86
+ >
87
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
88
+ <line x1="4" x2="20" y1="12" y2="12"></line>
89
+ <line x1="4" x2="20" y1="6" y2="6"></line>
90
+ <line x1="4" x2="20" y1="18" y2="18"></line>
91
+ </svg>
92
+ </button>
93
+
94
+ <a href="/" class="flex items-center gap-2">
95
+ {#if config.logo.light || config.logo.dark}
96
+ <img src={config.logo.light} alt="" class="h-5 w-5 dark:hidden" />
97
+ <img src={config.logo.dark || config.logo.light} alt="" class="hidden h-5 w-5 dark:block" />
98
+ {/if}
99
+ <span class="text-sm font-bold">{config.title}</span>
100
+ </a>
101
+ </header>
102
+
103
+ <!-- Mobile sheet -->
104
+ <SheetContent open={mobileOpen} onclose={() => (mobileOpen = false)} side="left">
105
+ <div class="mt-6">
106
+ <ScrollArea class="h-[calc(100vh-8rem)]">
107
+ {#each sidebar as group}
108
+ <div class="mb-4">
109
+ <h4 class="mb-1 px-2 text-sm font-semibold text-foreground">{group.label}</h4>
110
+ <div class="space-y-0.5">
111
+ {@render renderMobileEntries(group.items, 0)}
112
+ </div>
113
+ </div>
114
+ {/each}
115
+ </ScrollArea>
116
+ </div>
117
+ </SheetContent>
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import Footer from "./Footer.svelte"
3
+ import type { ResolvedConfig } from "../../config.js"
4
+
5
+ let { config }: { config: ResolvedConfig } = $props()
6
+
7
+ const socials = $derived(config.footer?.socials)
8
+ const copyright = $derived(config.footer?.copyright)
9
+
10
+ const socialEntries = $derived(
11
+ socials
12
+ ? Object.entries(socials).filter(([, url]) => url)
13
+ : []
14
+ )
15
+ </script>
16
+
17
+ {#snippet socialIcon(name)}
18
+ {#if name === "github"}
19
+ <svg viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
20
+ {:else if name === "x"}
21
+ <svg viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></svg>
22
+ {:else if name === "discord"}
23
+ <svg viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.947 2.418-2.157 2.418Z"/></svg>
24
+ {:else if name === "bluesky"}
25
+ <svg viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.785 2.627 3.6 3.496 6.158 3.16-4.468.746-5.954 3.217-3.343 5.69 4.862 4.607 7.156-1.158 7.996-3.638.098-.288.14-.49.565-.49.426 0 .467.202.565.49.84 2.48 3.134 8.245 7.996 3.639 2.611-2.474 1.125-4.945-3.343-5.691 2.558.336 5.373-.533 6.158-3.16.246-.828.624-5.788.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.3-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"/></svg>
26
+ {:else if name === "mastodon"}
27
+ <svg viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.668 1.977v6.218H4.823V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
28
+ {:else if name === "youtube"}
29
+ <svg viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
30
+ {:else if name === "linkedin"}
31
+ <svg viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
32
+ {/if}
33
+ {/snippet}
34
+
35
+ <footer class="border-t bg-background">
36
+ <div class="mx-auto max-w-[90rem] px-6 py-8 lg:px-10">
37
+ <div class="flex items-center justify-between">
38
+ <!-- Social icons -->
39
+ <div class="flex items-center gap-3">
40
+ {#each socialEntries as [name, url]}
41
+ <a
42
+ href={url}
43
+ target="_blank"
44
+ rel="noopener noreferrer"
45
+ class="text-muted-foreground transition-colors hover:text-foreground"
46
+ aria-label={name}
47
+ >
48
+ {@render socialIcon(name)}
49
+ </a>
50
+ {/each}
51
+ </div>
52
+
53
+ <!-- Powered by pill -->
54
+ <div class="w-40">
55
+ <Footer />
56
+ </div>
57
+ </div>
58
+
59
+ {#if copyright}
60
+ <p class="mt-4 text-center text-xs text-muted-foreground">{copyright}</p>
61
+ {/if}
62
+ </div>
63
+ </footer>
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import type { SidebarGroup, SidebarEntry } from "../../types.js"
3
+
4
+ let { sidebar = [], currentSlug = "", locale }: { sidebar?: SidebarGroup[]; currentSlug?: string; locale?: string } = $props()
5
+
6
+ function docHref(slug: string) {
7
+ const prefix = locale ? `/docs/${locale}` : "/docs"
8
+ if (slug === "index") return prefix
9
+ const clean = slug.replace(/\/index$/, "")
10
+ return `${prefix}/${clean}`
11
+ }
12
+
13
+ interface FlatItem {
14
+ title: string
15
+ href: string
16
+ slug: string
17
+ }
18
+
19
+ function flattenEntries(entries: SidebarEntry[]): FlatItem[] {
20
+ const result: FlatItem[] = []
21
+ for (const entry of entries) {
22
+ if (entry.type === "item") {
23
+ result.push({
24
+ title: entry.title,
25
+ href: docHref(entry.slug),
26
+ slug: entry.slug,
27
+ })
28
+ } else if (entry.type === "subgroup") {
29
+ if (entry.slug) {
30
+ result.push({
31
+ title: entry.label,
32
+ href: docHref(entry.slug),
33
+ slug: entry.slug,
34
+ })
35
+ }
36
+ result.push(...flattenEntries(entry.items))
37
+ }
38
+ }
39
+ return result
40
+ }
41
+
42
+ const items = $derived(
43
+ sidebar.flatMap((group: SidebarGroup) => flattenEntries(group.items))
44
+ )
45
+
46
+ const currentIndex = $derived(
47
+ items.findIndex((item: FlatItem) => item.slug === (currentSlug || "index"))
48
+ )
49
+
50
+ const prev = $derived(currentIndex > 0 ? items[currentIndex - 1] : null)
51
+ const next = $derived(currentIndex < items.length - 1 ? items[currentIndex + 1] : null)
52
+ </script>
53
+
54
+ {#if prev || next}
55
+ <nav class="mt-8 flex gap-4 border-t pt-8">
56
+ {#if prev}
57
+ <a
58
+ href={prev.href}
59
+ class="group flex flex-1 flex-col gap-1 rounded-lg border p-4 transition-colors hover:border-foreground/20 hover:bg-muted/50"
60
+ >
61
+ <span class="text-xs text-muted-foreground">Previous</span>
62
+ <span class="font-medium text-foreground group-hover:text-foreground/80">
63
+ &larr; {prev.title}
64
+ </span>
65
+ </a>
66
+ {:else}
67
+ <div class="flex-1"></div>
68
+ {/if}
69
+ {#if next}
70
+ <a
71
+ href={next.href}
72
+ class="group flex flex-1 flex-col items-end gap-1 rounded-lg border p-4 text-right transition-colors hover:border-foreground/20 hover:bg-muted/50"
73
+ >
74
+ <span class="text-xs text-muted-foreground">Next</span>
75
+ <span class="font-medium text-foreground group-hover:text-foreground/80">
76
+ {next.title} &rarr;
77
+ </span>
78
+ </a>
79
+ {:else}
80
+ <div class="flex-1"></div>
81
+ {/if}
82
+ </nav>
83
+ {/if}
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ import { goto } from "$app/navigation"
3
+ import { fade, scale, fly } from "svelte/transition"
4
+ import MiniSearch, { type SearchResult } from "minisearch"
5
+ import { searchIndex, locales, defaultLocale } from "virtual:tiramisu-docs"
6
+
7
+ let { open = $bindable(false), locale }: { open?: boolean; locale?: string } = $props()
8
+ let query = $state("")
9
+ let selectedIndex = $state(0)
10
+ let inputEl: HTMLInputElement | null = $state(null)
11
+
12
+ const activeIndex = $derived(
13
+ locale && locales?.[locale]
14
+ ? locales[locale].searchIndex
15
+ : searchIndex
16
+ )
17
+
18
+ const miniSearch = $derived.by(() => {
19
+ const ms = new MiniSearch({
20
+ fields: ["title", "headings", "text"],
21
+ storeFields: ["title", "group", "slug"],
22
+ searchOptions: {
23
+ boost: { title: 3, headings: 2 },
24
+ fuzzy: 0.2,
25
+ prefix: true,
26
+ },
27
+ })
28
+ ms.addAll(activeIndex)
29
+ return ms
30
+ })
31
+
32
+ const results = $derived(
33
+ query.length > 0
34
+ ? miniSearch.search(query).slice(0, 8)
35
+ : []
36
+ )
37
+
38
+ $effect(() => {
39
+ if (open) {
40
+ query = ""
41
+ selectedIndex = 0
42
+ requestAnimationFrame(() => inputEl?.focus())
43
+ }
44
+ })
45
+
46
+ $effect(() => {
47
+ results;
48
+ selectedIndex = 0
49
+ })
50
+
51
+ function handleKeydown(e: KeyboardEvent) {
52
+ if (e.key === "Escape") {
53
+ open = false
54
+ } else if (e.key === "ArrowDown") {
55
+ e.preventDefault()
56
+ selectedIndex = Math.min(selectedIndex + 1, results.length - 1)
57
+ } else if (e.key === "ArrowUp") {
58
+ e.preventDefault()
59
+ selectedIndex = Math.max(selectedIndex - 1, 0)
60
+ } else if (e.key === "Enter" && results.length > 0) {
61
+ e.preventDefault()
62
+ navigate(results[selectedIndex])
63
+ }
64
+ }
65
+
66
+ function navigate(result: SearchResult) {
67
+ const prefix = locale ? `/docs/${locale}` : "/docs"
68
+ const href = result.slug === "index" ? prefix : `${prefix}/${result.slug}`
69
+ open = false
70
+ goto(href)
71
+ }
72
+ </script>
73
+
74
+ {#if open}
75
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
76
+ <div class="fixed inset-0 z-50" onkeydown={handleKeydown}>
77
+ <button
78
+ class="fixed inset-0 bg-black/60 dark:bg-black/70 backdrop-blur-sm"
79
+ onclick={() => (open = false)}
80
+ aria-label="Close search"
81
+ transition:fade={{ duration: 150 }}
82
+ ></button>
83
+
84
+ <div
85
+ class="fixed left-1/2 top-[20%] z-50 w-full max-w-lg -translate-x-1/2 px-4"
86
+ transition:scale={{ duration: 150, start: 0.96, opacity: 0 }}
87
+ >
88
+ <div class="overflow-hidden rounded-xl border bg-background dark:bg-card shadow-2xl">
89
+ <div class="flex items-center gap-3 border-b px-4">
90
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 text-muted-foreground">
91
+ <circle cx="11" cy="11" r="8"></circle>
92
+ <path d="m21 21-4.3-4.3"></path>
93
+ </svg>
94
+ <input
95
+ bind:this={inputEl}
96
+ bind:value={query}
97
+ type="text"
98
+ placeholder="Search documentation..."
99
+ class="h-12 flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
100
+ />
101
+ <kbd class="rounded border bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">ESC</kbd>
102
+ </div>
103
+
104
+ {#if results.length > 0}
105
+ <ul class="max-h-80 overflow-y-auto p-2">
106
+ {#each results as result, i (result.id)}
107
+ <li in:fly={{ y: 8, duration: 150, delay: i * 30 }}>
108
+ <button
109
+ class="flex w-full flex-col rounded-lg px-3 py-2.5 text-left transition-colors
110
+ {i === selectedIndex
111
+ ? 'bg-primary/10 text-foreground'
112
+ : 'text-muted-foreground hover:bg-muted'}"
113
+ onclick={() => navigate(result)}
114
+ onmouseenter={() => (selectedIndex = i)}
115
+ >
116
+ <span class="text-[11px] text-muted-foreground">{result.group}</span>
117
+ <span class="text-sm font-medium">{result.title}</span>
118
+ </button>
119
+ </li>
120
+ {/each}
121
+ </ul>
122
+ {:else if query.length > 0}
123
+ <div class="px-4 py-8 text-center text-sm text-muted-foreground">
124
+ No results found.
125
+ </div>
126
+ {/if}
127
+ </div>
128
+ </div>
129
+ </div>
130
+ {/if}
@@ -0,0 +1,237 @@
1
+ <script lang="ts">
2
+ import { page } from "$app/stores"
3
+ import { Collapsible } from "$lib/components/ui/collapsible/index.js"
4
+ import type { ResolvedConfig, LocaleConfig } from "../../config.js"
5
+ import type { SidebarGroup, SidebarEntry, SidebarSubgroup } from "../../types.js"
6
+
7
+ let { config, groups, onSearchClick, hasSections = false, locale, locales }: { config: ResolvedConfig; groups: SidebarGroup[]; onSearchClick: () => void; hasSections?: boolean; locale?: string; locales?: LocaleConfig[] } = $props()
8
+
9
+ let dark = $state(false)
10
+ let navEl: HTMLElement | null = $state(null)
11
+ let canScrollUp = $state(false)
12
+ let canScrollDown = $state(false)
13
+
14
+ function updateScroll() {
15
+ if (!navEl) return
16
+ const threshold = 4
17
+ canScrollUp = navEl.scrollTop > threshold
18
+ canScrollDown = navEl.scrollTop + navEl.clientHeight < navEl.scrollHeight - threshold
19
+ }
20
+
21
+ function docHref(slug: string): string {
22
+ const prefix = locale ? `/docs/${locale}` : "/docs"
23
+ if (slug === "index") return prefix
24
+ const clean = slug.replace(/\/index$/, "")
25
+ return `${prefix}/${clean}`
26
+ }
27
+
28
+ function initTheme() {
29
+ if (typeof window === "undefined") return
30
+ const stored = window.localStorage.getItem("theme")
31
+ dark = stored === "dark" || (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches)
32
+ document.documentElement.classList.toggle("dark", dark)
33
+ }
34
+
35
+ function toggleTheme() {
36
+ if (typeof window === "undefined") return
37
+ dark = !dark
38
+ document.documentElement.classList.toggle("dark", dark)
39
+ window.localStorage.setItem("theme", dark ? "dark" : "light")
40
+ }
41
+
42
+ function isSubgroupActive(entry: SidebarSubgroup, pathname: string): boolean {
43
+ if (entry.slug) {
44
+ const href = docHref(entry.slug)
45
+ if (pathname === href) return true
46
+ }
47
+ return hasActiveDescendant(entry.items, pathname)
48
+ }
49
+
50
+ function hasActiveDescendant(items: SidebarEntry[], pathname: string): boolean {
51
+ for (const entry of items) {
52
+ if (entry.type === "item") {
53
+ const href = docHref(entry.slug)
54
+ if (pathname === href) return true
55
+ } else if (entry.type === "subgroup") {
56
+ if (isSubgroupActive(entry, pathname)) return true
57
+ }
58
+ }
59
+ return false
60
+ }
61
+
62
+ $effect(() => {
63
+ initTheme()
64
+ updateScroll()
65
+ })
66
+ </script>
67
+
68
+ {#snippet renderEntries(entries: SidebarEntry[], depth: number)}
69
+ {#each entries as entry}
70
+ {#if entry.type === "item"}
71
+ {@const href = docHref(entry.slug)}
72
+ {@const active = $page.url.pathname === href}
73
+ <a
74
+ {href}
75
+ class="flex items-center gap-1.5 rounded-md py-[5px] text-[13px] transition-colors
76
+ {active
77
+ ? 'font-medium text-primary bg-primary/10'
78
+ : 'text-muted-foreground hover:text-foreground'}"
79
+ style:padding-left="{0.5 + depth * 0.75}rem"
80
+ >
81
+ {#if entry.icon}
82
+ <iconify-icon icon={entry.icon.includes(":") ? entry.icon : `lucide:${entry.icon}`} width="14" height="14" class="shrink-0"></iconify-icon>
83
+ {/if}
84
+ {entry.title}
85
+ </a>
86
+ {:else}
87
+ {@const subActive = entry.slug && $page.url.pathname === docHref(entry.slug)}
88
+ <Collapsible
89
+ open={isSubgroupActive(entry, $page.url.pathname)}
90
+ href={entry.slug ? docHref(entry.slug) : undefined}
91
+ class="mt-0.5"
92
+ triggerClass="rounded-md py-[5px] pr-2 transition-colors {subActive ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}"
93
+ >
94
+ {#snippet trigger()}
95
+ <span
96
+ class="flex w-full items-center gap-1.5 text-[13px] font-medium"
97
+ style:padding-left="{0.5 + depth * 0.75}rem"
98
+ >
99
+ {#if entry.icon}
100
+ <iconify-icon icon={entry.icon.includes(":") ? entry.icon : `lucide:${entry.icon}`} width="14" height="14" class="shrink-0"></iconify-icon>
101
+ {/if}
102
+ {entry.label}
103
+ </span>
104
+ {/snippet}
105
+ <div class="mt-0.5">
106
+ {@render renderEntries(entry.items, depth + 1)}
107
+ </div>
108
+ </Collapsible>
109
+ {/if}
110
+ {/each}
111
+ {/snippet}
112
+
113
+ <div class="flex h-full flex-col">
114
+ <!-- Header: logo (hidden when TopBar handles it) -->
115
+ {#if !hasSections}
116
+ <div class="flex h-14 shrink-0 items-center px-4 lg:px-6">
117
+ <a href="/" class="flex items-center gap-2">
118
+ {#if config.logo.light || config.logo.dark}
119
+ <img src={config.logo.light} alt="" class="h-5 w-5 dark:hidden" />
120
+ <img src={config.logo.dark || config.logo.light} alt="" class="hidden h-5 w-5 dark:block" />
121
+ {/if}
122
+ <span class="text-sm font-semibold">{config.title}</span>
123
+ </a>
124
+ </div>
125
+ {/if}
126
+
127
+ <!-- Search (hidden when TopBar has it) -->
128
+ {#if !hasSections}
129
+ <div class="px-4 pb-4 lg:px-6">
130
+ <button onclick={onSearchClick} class="flex h-8 w-full items-center gap-2 rounded-md border bg-muted/40 px-2.5 text-[13px] text-muted-foreground transition-colors hover:bg-muted">
131
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 opacity-60">
132
+ <circle cx="11" cy="11" r="8"></circle>
133
+ <path d="m21 21-4.3-4.3"></path>
134
+ </svg>
135
+ <span class="flex-1 text-left">Search</span>
136
+ <div class="flex items-center gap-0.5">
137
+ <kbd class="rounded border bg-background px-1 font-mono text-[10px] text-muted-foreground">⌘</kbd>
138
+ <kbd class="rounded border bg-background px-1 font-mono text-[10px] text-muted-foreground">K</kbd>
139
+ </div>
140
+ </button>
141
+ </div>
142
+ {/if}
143
+
144
+ <!-- Nav groups -->
145
+ <div class="relative flex-1 overflow-hidden">
146
+ <nav
147
+ bind:this={navEl}
148
+ onscroll={updateScroll}
149
+ class="h-full overflow-y-auto overscroll-contain px-4 pt-4 pb-4 lg:px-6"
150
+ >
151
+ {#each groups as group}
152
+ <div class="mb-5">
153
+ <h4 class="flex items-center gap-1.5 pl-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">
154
+ {#if group.icon}
155
+ <iconify-icon icon={group.icon.includes(":") ? group.icon : `lucide:${group.icon}`} width="14" height="14" class="shrink-0"></iconify-icon>
156
+ {/if}
157
+ {group.label}
158
+ </h4>
159
+ <div class="mt-1 space-y-0.5">
160
+ {@render renderEntries(group.items, 0)}
161
+ </div>
162
+ </div>
163
+ {/each}
164
+ </nav>
165
+
166
+ {#if canScrollUp}
167
+ <div class="pointer-events-none absolute inset-x-0 top-0 flex justify-center items-center h-12 bg-gradient-to-b from-background to-transparent">
168
+ <button
169
+ onclick={() => navEl?.scrollTo({ top: 0, behavior: "smooth" })}
170
+ class="pointer-events-auto flex h-6 w-6 items-center justify-center rounded-full border bg-background text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-foreground"
171
+ aria-label="Scroll to top"
172
+ >
173
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>
174
+ </button>
175
+ </div>
176
+ {/if}
177
+
178
+ {#if canScrollDown}
179
+ <div class="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center items-center h-12 bg-gradient-to-t from-background to-transparent">
180
+ <button
181
+ onclick={() => navEl?.scrollTo({ top: navEl.scrollHeight, behavior: "smooth" })}
182
+ class="pointer-events-auto flex h-6 w-6 items-center justify-center rounded-full border bg-background text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-foreground"
183
+ aria-label="Scroll to bottom"
184
+ >
185
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
186
+ </button>
187
+ </div>
188
+ {/if}
189
+ </div>
190
+
191
+ <!-- Bottom bar (hidden when TopBar handles theme toggle) -->
192
+ {#if !hasSections}
193
+ <div class="flex shrink-0 items-center gap-2 border-t px-4 py-3 lg:px-6">
194
+ {#if locales?.length > 1}
195
+ <select
196
+ class="h-7 rounded-md border bg-background px-1.5 text-xs text-muted-foreground"
197
+ onchange={(e: Event & { currentTarget: HTMLSelectElement }) => {
198
+ const loc = e.currentTarget.value
199
+ const currentPath = window.location.pathname
200
+ const newPath = locale
201
+ ? currentPath.replace(`/docs/${locale}`, `/docs/${loc}`)
202
+ : currentPath.replace("/docs", `/docs/${loc}`)
203
+ window.location.href = newPath
204
+ }}
205
+ >
206
+ {#each locales as loc}
207
+ <option value={loc.code} selected={loc.code === locale}>
208
+ {loc.flag ? loc.flag + " " : ""}{loc.label}
209
+ </option>
210
+ {/each}
211
+ </select>
212
+ {/if}
213
+ <div class="flex-1"></div>
214
+ <button
215
+ onclick={toggleTheme}
216
+ class="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
217
+ aria-label="Toggle dark mode"
218
+ >
219
+ <svg class="h-3.5 w-3.5 dark:hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
220
+ <circle cx="12" cy="12" r="4"></circle>
221
+ <path d="M12 2v2"></path>
222
+ <path d="M12 20v2"></path>
223
+ <path d="m4.93 4.93 1.41 1.41"></path>
224
+ <path d="m17.66 17.66 1.41 1.41"></path>
225
+ <path d="M2 12h2"></path>
226
+ <path d="M20 12h2"></path>
227
+ <path d="m6.34 17.66-1.41 1.41"></path>
228
+ <path d="m19.07 4.93-1.41 1.41"></path>
229
+ </svg>
230
+ <svg class="hidden h-3.5 w-3.5 dark:block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
231
+ <path d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
232
+ </svg>
233
+ </button>
234
+ </div>
235
+ {/if}
236
+
237
+ </div>