@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.
- package/README.md +103 -0
- package/components.json +14 -0
- package/dist/bin/mcp.d.ts +2 -0
- package/dist/bin/mcp.js +4 -0
- package/dist/config.d.ts +99 -0
- package/dist/config.js +36 -0
- package/dist/highlight.d.ts +10 -0
- package/dist/highlight.js +93 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/lib/components/index.d.ts +16 -0
- package/dist/lib/components/index.js +18 -0
- package/dist/lib/components/tiramisu/lang-icons.d.ts +4 -0
- package/dist/lib/components/tiramisu/lang-icons.js +77 -0
- package/dist/lib/components/ui/alert/index.d.ts +5 -0
- package/dist/lib/components/ui/alert/index.js +6 -0
- package/dist/lib/components/ui/badge/index.d.ts +2 -0
- package/dist/lib/components/ui/badge/index.js +1 -0
- package/dist/lib/components/ui/button/index.d.ts +4 -0
- package/dist/lib/components/ui/button/index.js +2 -0
- package/dist/lib/components/ui/card/index.d.ts +8 -0
- package/dist/lib/components/ui/card/index.js +10 -0
- package/dist/lib/components/ui/collapsible/index.d.ts +1 -0
- package/dist/lib/components/ui/collapsible/index.js +1 -0
- package/dist/lib/components/ui/dropdown-menu/index.d.ts +18 -0
- package/dist/lib/components/ui/dropdown-menu/index.js +18 -0
- package/dist/lib/components/ui/scroll-area/index.d.ts +1 -0
- package/dist/lib/components/ui/scroll-area/index.js +1 -0
- package/dist/lib/components/ui/separator/index.d.ts +1 -0
- package/dist/lib/components/ui/separator/index.js +1 -0
- package/dist/lib/components/ui/sheet/index.d.ts +3 -0
- package/dist/lib/components/ui/sheet/index.js +3 -0
- package/dist/lib/components/ui/tabs/index.d.ts +5 -0
- package/dist/lib/components/ui/tabs/index.js +7 -0
- package/dist/lib/open-links.d.ts +22 -0
- package/dist/lib/open-links.js +33 -0
- package/dist/lib/routes/docs/[...slug]/+page.d.ts +25 -0
- package/dist/lib/routes/docs/[...slug]/+page.js +109 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +5 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.js +155 -0
- package/dist/scan.d.ts +15 -0
- package/dist/scan.js +72 -0
- package/dist/seo.d.ts +63 -0
- package/dist/seo.js +160 -0
- package/dist/tiramisu-grammar.d.ts +2 -0
- package/dist/tiramisu-grammar.js +77 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.js +1 -0
- package/dist/vite.d.ts +33 -0
- package/dist/vite.js +406 -0
- package/package.json +74 -0
- package/src/config.ts +133 -0
- package/src/highlight.ts +110 -0
- package/src/index.ts +6 -0
- package/src/lib/components/DocPage.svelte +430 -0
- package/src/lib/components/DocsLayout.svelte +145 -0
- package/src/lib/components/Footer.svelte +26 -0
- package/src/lib/components/Navbar.svelte +117 -0
- package/src/lib/components/PageFooter.svelte +63 -0
- package/src/lib/components/PrevNextNav.svelte +83 -0
- package/src/lib/components/SearchDialog.svelte +130 -0
- package/src/lib/components/Sidebar.svelte +237 -0
- package/src/lib/components/TableOfContents.svelte +50 -0
- package/src/lib/components/TopBar.svelte +407 -0
- package/src/lib/components/index.ts +19 -0
- package/src/lib/components/tiramisu/Accordion.svelte +16 -0
- package/src/lib/components/tiramisu/Badge.svelte +16 -0
- package/src/lib/components/tiramisu/Callout.svelte +26 -0
- package/src/lib/components/tiramisu/CodeBlock.svelte +56 -0
- package/src/lib/components/tiramisu/CodeTabs.svelte +123 -0
- package/src/lib/components/tiramisu/Demo.svelte +15 -0
- package/src/lib/components/tiramisu/FileTree.svelte +67 -0
- package/src/lib/components/tiramisu/MathBlock.svelte +26 -0
- package/src/lib/components/tiramisu/Mermaid.svelte +30 -0
- package/src/lib/components/tiramisu/NavCard.svelte +49 -0
- package/src/lib/components/tiramisu/Steps.svelte +60 -0
- package/src/lib/components/tiramisu/Tabs.svelte +87 -0
- package/src/lib/components/tiramisu/ZoomImage.svelte +114 -0
- package/src/lib/components/tiramisu/lang-icons.ts +81 -0
- package/src/lib/open-links.ts +50 -0
- package/src/lib/routes/docs/[...slug]/+page.svelte +26 -0
- package/src/lib/routes/docs/[...slug]/+page.ts +117 -0
- package/src/lib/styles/theme.css +222 -0
- package/src/lib/utils.ts +10 -0
- package/src/mcp.ts +180 -0
- package/src/scan.ts +92 -0
- package/src/seo.ts +193 -0
- package/src/tiramisu-grammar.ts +80 -0
- package/src/types.ts +71 -0
- package/src/virtual.d.ts +11 -0
- package/src/vite.ts +478 -0
- package/tests/config.test.ts +60 -0
- package/tests/mcp.test.ts +116 -0
- package/tests/scan.test.ts +48 -0
- package/tests/seo.test.ts +174 -0
- package/tests/vite.test.ts +283 -0
- 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
|
+
← {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} →
|
|
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>
|