@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,123 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fly } from "svelte/transition"
|
|
3
|
+
import { getLangIcon } from "./lang-icons.js"
|
|
4
|
+
|
|
5
|
+
interface TabMeta {
|
|
6
|
+
label: string
|
|
7
|
+
icon?: string
|
|
8
|
+
language?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
group = "",
|
|
13
|
+
tabs = [],
|
|
14
|
+
codes = [],
|
|
15
|
+
langMap = [],
|
|
16
|
+
}: { group?: string; tabs?: TabMeta[]; codes?: string[]; langMap?: string[] } = $props()
|
|
17
|
+
|
|
18
|
+
const storageKey = $derived(group ? `codetabs:${group}` : "")
|
|
19
|
+
|
|
20
|
+
function getInitial() {
|
|
21
|
+
const key = group ? `codetabs:${group}` : ""
|
|
22
|
+
if (key && typeof window !== "undefined" && window.localStorage) {
|
|
23
|
+
const saved = window.localStorage.getItem(key)
|
|
24
|
+
if (saved && tabs.some((t: TabMeta) => t.label === saved)) return saved
|
|
25
|
+
}
|
|
26
|
+
return tabs[0]?.label ?? ""
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let active = $state(getInitial())
|
|
30
|
+
|
|
31
|
+
function select(label: string) {
|
|
32
|
+
if (label === active) return
|
|
33
|
+
active = label
|
|
34
|
+
if (storageKey && typeof localStorage !== "undefined") {
|
|
35
|
+
localStorage.setItem(storageKey, label)
|
|
36
|
+
window.dispatchEvent(new CustomEvent("codetabs-sync", { detail: { group, label } }))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (!storageKey || typeof window === "undefined") return
|
|
42
|
+
|
|
43
|
+
function onSync(e: Event) {
|
|
44
|
+
const detail = (e as CustomEvent<{ group: string; label: string }>).detail
|
|
45
|
+
if (detail?.group === group && detail?.label !== active) {
|
|
46
|
+
select(detail.label)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onStorage(e: StorageEvent) {
|
|
51
|
+
if (e.key === storageKey && e.newValue && e.newValue !== active) {
|
|
52
|
+
select(e.newValue)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
window.addEventListener("codetabs-sync", onSync)
|
|
57
|
+
window.addEventListener("storage", onStorage)
|
|
58
|
+
return () => {
|
|
59
|
+
window.removeEventListener("codetabs-sync", onSync)
|
|
60
|
+
window.removeEventListener("storage", onStorage)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
let copied = $state(false)
|
|
65
|
+
|
|
66
|
+
function copyActive() {
|
|
67
|
+
const idx = tabs.findIndex((t: TabMeta) => t.label === active)
|
|
68
|
+
if (idx === -1) return
|
|
69
|
+
const raw = (codes[idx] ?? "").replace(/<[^>]*>/g, "")
|
|
70
|
+
navigator.clipboard.writeText(raw)
|
|
71
|
+
copied = true
|
|
72
|
+
setTimeout(() => (copied = false), 2000)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function tabIcon(tab: TabMeta): string | undefined {
|
|
76
|
+
if (tab.icon) return tab.icon
|
|
77
|
+
return getLangIcon(tab.label) || getLangIcon(tab.language || "")
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<div class="group relative my-4 overflow-hidden rounded-lg border border-border">
|
|
82
|
+
<div class="flex items-center border-b border-border bg-muted/50">
|
|
83
|
+
<div class="flex overflow-x-auto">
|
|
84
|
+
{#each tabs as tab, i}
|
|
85
|
+
{@const icon = tabIcon(tab)}
|
|
86
|
+
<button
|
|
87
|
+
onclick={() => select(tab.label)}
|
|
88
|
+
class="relative flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors whitespace-nowrap {active === tab.label ? 'text-foreground' : 'text-muted-foreground hover:text-foreground/80'}"
|
|
89
|
+
>
|
|
90
|
+
{#if icon}
|
|
91
|
+
<iconify-icon icon={icon} width="14" height="14" class="shrink-0"></iconify-icon>
|
|
92
|
+
{/if}
|
|
93
|
+
{tab.label}
|
|
94
|
+
{#if active === tab.label}
|
|
95
|
+
<span class="absolute bottom-0 left-2 right-2 h-0.5 rounded-full bg-primary"></span>
|
|
96
|
+
{/if}
|
|
97
|
+
</button>
|
|
98
|
+
{/each}
|
|
99
|
+
</div>
|
|
100
|
+
<div class="ml-auto pr-2">
|
|
101
|
+
<button
|
|
102
|
+
onclick={copyActive}
|
|
103
|
+
class="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100"
|
|
104
|
+
aria-label="Copy code"
|
|
105
|
+
>
|
|
106
|
+
{#if copied}
|
|
107
|
+
<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="M20 6 9 17l-5-5"/></svg>
|
|
108
|
+
Copied
|
|
109
|
+
{:else}
|
|
110
|
+
<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"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
|
111
|
+
Copy
|
|
112
|
+
{/if}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
{#each tabs as tab, i}
|
|
117
|
+
{#if active === tab.label}
|
|
118
|
+
<div in:fly={{ y: 6, duration: 200 }}>
|
|
119
|
+
<pre class="overflow-x-auto p-4 text-sm leading-relaxed"><code>{@html codes[i] ?? ""}</code></pre>
|
|
120
|
+
</div>
|
|
121
|
+
{/if}
|
|
122
|
+
{/each}
|
|
123
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/card/index.js"
|
|
4
|
+
|
|
5
|
+
let { title = "Preview", children }: { title?: string; children: Snippet } = $props()
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<Card class="my-4">
|
|
9
|
+
<CardHeader>
|
|
10
|
+
<CardTitle class="text-xs font-medium tracking-wide text-muted-foreground uppercase">{title}</CardTitle>
|
|
11
|
+
</CardHeader>
|
|
12
|
+
<CardContent>
|
|
13
|
+
{@render children()}
|
|
14
|
+
</CardContent>
|
|
15
|
+
</Card>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let { children }: { children?: import("svelte").Snippet } = $props()
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<div class="file-tree my-4 rounded-lg border bg-card p-4 font-mono text-sm">
|
|
6
|
+
{@render children?.()}
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.file-tree :global(.tree-file),
|
|
11
|
+
.file-tree :global(.tree-folder-name) {
|
|
12
|
+
position: relative;
|
|
13
|
+
display: block;
|
|
14
|
+
padding-left: 1.25rem;
|
|
15
|
+
padding-top: 0.125rem;
|
|
16
|
+
padding-bottom: 0.125rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.file-tree :global(.tree-file)::before {
|
|
20
|
+
content: "";
|
|
21
|
+
position: absolute;
|
|
22
|
+
left: 0;
|
|
23
|
+
top: 0.35rem;
|
|
24
|
+
width: 14px;
|
|
25
|
+
height: 14px;
|
|
26
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'/%3E%3Cpath d='M14 2v4a2 2 0 0 0 2 2h4'/%3E%3C/svg%3E");
|
|
27
|
+
background-size: contain;
|
|
28
|
+
background-repeat: no-repeat;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.file-tree :global(.tree-folder-name)::before {
|
|
32
|
+
content: "";
|
|
33
|
+
position: absolute;
|
|
34
|
+
left: 0;
|
|
35
|
+
top: 0.35rem;
|
|
36
|
+
width: 14px;
|
|
37
|
+
height: 14px;
|
|
38
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'/%3E%3C/svg%3E");
|
|
39
|
+
background-size: contain;
|
|
40
|
+
background-repeat: no-repeat;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.file-tree :global(.tree-folder-name) {
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
color: var(--foreground);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Nesting: border aligned under parent folder icon center (7px ≈ 0.45rem) */
|
|
49
|
+
.file-tree :global(.tree-folder > .tree-file),
|
|
50
|
+
.file-tree :global(.tree-folder > .tree-folder) {
|
|
51
|
+
margin-left: 0.45rem;
|
|
52
|
+
border-left: 1px solid var(--border);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Nested files: shift icon away from border, increase padding to match */
|
|
56
|
+
.file-tree :global(.tree-folder > .tree-file) {
|
|
57
|
+
padding-left: 1.75rem;
|
|
58
|
+
}
|
|
59
|
+
.file-tree :global(.tree-folder > .tree-file)::before {
|
|
60
|
+
left: 0.5rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Nested folders: gap from border to folder-name content */
|
|
64
|
+
.file-tree :global(.tree-folder > .tree-folder) {
|
|
65
|
+
padding-left: 0.5rem;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte"
|
|
3
|
+
|
|
4
|
+
let { formula = "" }: { formula?: string } = $props()
|
|
5
|
+
let container: HTMLElement
|
|
6
|
+
let html = $state("")
|
|
7
|
+
|
|
8
|
+
onMount(async () => {
|
|
9
|
+
const katex = await import("katex")
|
|
10
|
+
html = katex.default.renderToString(formula, {
|
|
11
|
+
throwOnError: false,
|
|
12
|
+
displayMode: true,
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<svelte:head>
|
|
18
|
+
<link
|
|
19
|
+
rel="stylesheet"
|
|
20
|
+
href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css"
|
|
21
|
+
/>
|
|
22
|
+
</svelte:head>
|
|
23
|
+
|
|
24
|
+
<div bind:this={container} class="math-block my-4 overflow-x-auto">
|
|
25
|
+
{@html html}
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte"
|
|
3
|
+
|
|
4
|
+
let { chart = "" }: { chart?: string } = $props()
|
|
5
|
+
let container: HTMLElement
|
|
6
|
+
let idBase = `mermaid-${Math.random().toString(36).slice(2, 9)}`
|
|
7
|
+
let renderCount = 0
|
|
8
|
+
|
|
9
|
+
onMount(async () => {
|
|
10
|
+
const mermaid = (await import("mermaid")).default
|
|
11
|
+
|
|
12
|
+
async function render() {
|
|
13
|
+
const isDark = document.documentElement.classList.contains("dark")
|
|
14
|
+
mermaid.initialize({ startOnLoad: false, theme: isDark ? "dark" : "default" })
|
|
15
|
+
const rid = `${idBase}-${renderCount++}`
|
|
16
|
+
const { svg } = await mermaid.render(rid, chart)
|
|
17
|
+
container.innerHTML = svg
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await render()
|
|
21
|
+
|
|
22
|
+
const observer = new MutationObserver(() => render())
|
|
23
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
|
|
24
|
+
return () => observer.disconnect()
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<div bind:this={container} class="mermaid-block my-4 flex justify-center">
|
|
29
|
+
<pre class="text-sm text-muted-foreground">{chart}</pre>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
title = "",
|
|
4
|
+
description = "",
|
|
5
|
+
href = "",
|
|
6
|
+
icon = "",
|
|
7
|
+
image = "",
|
|
8
|
+
}: {
|
|
9
|
+
title?: string
|
|
10
|
+
description?: string
|
|
11
|
+
href?: string
|
|
12
|
+
icon?: string
|
|
13
|
+
image?: string
|
|
14
|
+
} = $props()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#snippet cardContent()}
|
|
18
|
+
{#if image}
|
|
19
|
+
<div class="aspect-[2/1] w-full overflow-hidden bg-muted">
|
|
20
|
+
<img src={image} alt="" class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" />
|
|
21
|
+
</div>
|
|
22
|
+
{/if}
|
|
23
|
+
<div class="flex flex-col gap-2 p-5">
|
|
24
|
+
<span class="flex items-center gap-2 font-semibold text-card-foreground group-hover:text-primary">
|
|
25
|
+
{#if icon}
|
|
26
|
+
<iconify-icon icon={icon.includes(":") ? icon : `lucide:${icon}`} width="18" height="18" class="shrink-0"></iconify-icon>
|
|
27
|
+
{/if}
|
|
28
|
+
{title}
|
|
29
|
+
</span>
|
|
30
|
+
{#if description}
|
|
31
|
+
<span class="text-sm text-muted-foreground">{description}</span>
|
|
32
|
+
{/if}
|
|
33
|
+
</div>
|
|
34
|
+
{/snippet}
|
|
35
|
+
|
|
36
|
+
{#if href}
|
|
37
|
+
<a
|
|
38
|
+
{href}
|
|
39
|
+
class="group flex flex-col overflow-hidden rounded-xl border bg-card shadow-sm transition-colors hover:border-primary/30 hover:bg-accent"
|
|
40
|
+
>
|
|
41
|
+
{@render cardContent()}
|
|
42
|
+
</a>
|
|
43
|
+
{:else}
|
|
44
|
+
<div
|
|
45
|
+
class="group flex flex-col overflow-hidden rounded-xl border bg-card shadow-sm"
|
|
46
|
+
>
|
|
47
|
+
{@render cardContent()}
|
|
48
|
+
</div>
|
|
49
|
+
{/if}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
|
|
4
|
+
let { children }: { children: Snippet } = $props()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div class="stepper my-6">
|
|
8
|
+
{@render children()}
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<style>
|
|
12
|
+
.stepper :global(ol) {
|
|
13
|
+
list-style: none;
|
|
14
|
+
padding-left: 0;
|
|
15
|
+
margin: 0;
|
|
16
|
+
counter-reset: step;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.stepper :global(li.step) {
|
|
20
|
+
position: relative;
|
|
21
|
+
padding-left: 2.5rem;
|
|
22
|
+
padding-bottom: 1.5rem;
|
|
23
|
+
counter-increment: step;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.stepper :global(li.step)::before {
|
|
27
|
+
content: counter(step);
|
|
28
|
+
position: absolute;
|
|
29
|
+
left: 0;
|
|
30
|
+
top: 0;
|
|
31
|
+
width: 1.75rem;
|
|
32
|
+
height: 1.75rem;
|
|
33
|
+
border-radius: 9999px;
|
|
34
|
+
background: var(--primary);
|
|
35
|
+
color: var(--primary-foreground);
|
|
36
|
+
font-size: 0.75rem;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.stepper :global(li.step)::after {
|
|
44
|
+
content: "";
|
|
45
|
+
position: absolute;
|
|
46
|
+
left: 0.8125rem;
|
|
47
|
+
top: 1.75rem;
|
|
48
|
+
width: 2px;
|
|
49
|
+
bottom: 0;
|
|
50
|
+
background: var(--border);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.stepper :global(li.step:last-child)::after {
|
|
54
|
+
display: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.stepper :global(li.step:last-child) {
|
|
58
|
+
padding-bottom: 0;
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fly } from "svelte/transition"
|
|
3
|
+
|
|
4
|
+
interface TabMeta {
|
|
5
|
+
label: string
|
|
6
|
+
icon?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
group = "",
|
|
11
|
+
tabs = [],
|
|
12
|
+
contents = [],
|
|
13
|
+
}: { group?: string; tabs?: TabMeta[]; contents?: string[] } = $props()
|
|
14
|
+
|
|
15
|
+
const storageKey = $derived(group ? `tabs:${group}` : "")
|
|
16
|
+
|
|
17
|
+
function getInitial() {
|
|
18
|
+
if (storageKey && typeof window !== "undefined" && window.localStorage) {
|
|
19
|
+
const saved = window.localStorage.getItem(storageKey)
|
|
20
|
+
if (saved && tabs.some((t: TabMeta) => t.label === saved)) return saved
|
|
21
|
+
}
|
|
22
|
+
return tabs[0]?.label ?? ""
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let active = $state(getInitial())
|
|
26
|
+
|
|
27
|
+
function select(label: string) {
|
|
28
|
+
if (label === active) return
|
|
29
|
+
active = label
|
|
30
|
+
if (storageKey && typeof window !== "undefined" && window.localStorage) {
|
|
31
|
+
window.localStorage.setItem(storageKey, label)
|
|
32
|
+
window.dispatchEvent(new CustomEvent("tabs-sync", { detail: { group, label } }))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
if (!storageKey || typeof window === "undefined") return
|
|
38
|
+
|
|
39
|
+
function onSync(e: Event) {
|
|
40
|
+
const detail = (e as CustomEvent<{ group: string; label: string }>).detail
|
|
41
|
+
if (detail?.group === group && detail?.label !== active) {
|
|
42
|
+
select(detail.label)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onStorage(e: StorageEvent) {
|
|
47
|
+
if (e.key === storageKey && e.newValue && e.newValue !== active) {
|
|
48
|
+
select(e.newValue)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
window.addEventListener("tabs-sync", onSync)
|
|
53
|
+
window.addEventListener("storage", onStorage)
|
|
54
|
+
return () => {
|
|
55
|
+
window.removeEventListener("tabs-sync", onSync)
|
|
56
|
+
window.removeEventListener("storage", onStorage)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<div class="my-4 overflow-hidden rounded-lg border border-border">
|
|
62
|
+
<div class="flex border-b border-border bg-muted/50">
|
|
63
|
+
<div class="flex overflow-x-auto">
|
|
64
|
+
{#each tabs as tab}
|
|
65
|
+
<button
|
|
66
|
+
onclick={() => select(tab.label)}
|
|
67
|
+
class="relative flex items-center gap-1.5 px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap {active === tab.label ? 'text-foreground' : 'text-muted-foreground hover:text-foreground/80'}"
|
|
68
|
+
>
|
|
69
|
+
{#if tab.icon}
|
|
70
|
+
<iconify-icon icon={tab.icon.includes(":") ? tab.icon : `lucide:${tab.icon}`} width="14" height="14" class="shrink-0"></iconify-icon>
|
|
71
|
+
{/if}
|
|
72
|
+
{tab.label}
|
|
73
|
+
{#if active === tab.label}
|
|
74
|
+
<span class="absolute bottom-0 left-2 right-2 h-0.5 rounded-full bg-primary"></span>
|
|
75
|
+
{/if}
|
|
76
|
+
</button>
|
|
77
|
+
{/each}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
{#each tabs as tab, i}
|
|
81
|
+
{#if active === tab.label}
|
|
82
|
+
<div class="p-4" in:fly={{ y: 6, duration: 200 }}>
|
|
83
|
+
{@html contents[i] ?? ""}
|
|
84
|
+
</div>
|
|
85
|
+
{/if}
|
|
86
|
+
{/each}
|
|
87
|
+
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } from "svelte/transition"
|
|
3
|
+
|
|
4
|
+
let { src = "", alt = "", caption = "" }: { src?: string; alt?: string; caption?: string } = $props()
|
|
5
|
+
let zoomed = $state(false)
|
|
6
|
+
|
|
7
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
8
|
+
if (e.key === "Escape") zoomed = false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function scaleIn(node: Element) {
|
|
12
|
+
return {
|
|
13
|
+
duration: 250,
|
|
14
|
+
css: (t: number) => {
|
|
15
|
+
const ease = 1 - Math.pow(1 - t, 3)
|
|
16
|
+
return `opacity: ${ease}; transform: scale(${0.85 + 0.15 * ease})`
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function scaleOut(node: Element) {
|
|
22
|
+
return {
|
|
23
|
+
duration: 200,
|
|
24
|
+
css: (t: number) => {
|
|
25
|
+
const ease = t * t
|
|
26
|
+
return `opacity: ${ease}; transform: scale(${0.85 + 0.15 * ease})`
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
33
|
+
|
|
34
|
+
<figure class="zoom-figure">
|
|
35
|
+
<button type="button" class="zoom-trigger" onclick={() => (zoomed = true)}>
|
|
36
|
+
<img {src} {alt} class="rounded-lg" />
|
|
37
|
+
</button>
|
|
38
|
+
{#if caption}
|
|
39
|
+
<figcaption class="mt-2 text-center text-sm text-muted-foreground">{caption}</figcaption>
|
|
40
|
+
{/if}
|
|
41
|
+
</figure>
|
|
42
|
+
|
|
43
|
+
{#if zoomed}
|
|
44
|
+
<div class="zoom-overlay" role="dialog" aria-modal="true">
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
class="zoom-backdrop"
|
|
48
|
+
onclick={() => (zoomed = false)}
|
|
49
|
+
in:fade={{ duration: 250 }}
|
|
50
|
+
out:fade={{ duration: 200 }}
|
|
51
|
+
>
|
|
52
|
+
<img
|
|
53
|
+
{src}
|
|
54
|
+
{alt}
|
|
55
|
+
class="zoom-image"
|
|
56
|
+
in:scaleIn
|
|
57
|
+
out:scaleOut
|
|
58
|
+
/>
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
{/if}
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
.zoom-figure {
|
|
65
|
+
margin: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.zoom-trigger {
|
|
69
|
+
cursor: zoom-in;
|
|
70
|
+
background: none;
|
|
71
|
+
border: none;
|
|
72
|
+
padding: 0;
|
|
73
|
+
display: block;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.zoom-trigger img {
|
|
77
|
+
max-width: 100%;
|
|
78
|
+
height: auto;
|
|
79
|
+
transition: opacity 0.15s;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.zoom-trigger:hover img {
|
|
83
|
+
opacity: 0.85;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.zoom-overlay {
|
|
87
|
+
position: fixed;
|
|
88
|
+
inset: 0;
|
|
89
|
+
z-index: 50;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.zoom-backdrop {
|
|
96
|
+
position: fixed;
|
|
97
|
+
inset: 0;
|
|
98
|
+
background: rgba(0, 0, 0, 0.8);
|
|
99
|
+
border: none;
|
|
100
|
+
cursor: zoom-out;
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
padding: 2rem;
|
|
105
|
+
backdrop-filter: blur(4px);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.zoom-image {
|
|
109
|
+
max-width: 90vw;
|
|
110
|
+
max-height: 90vh;
|
|
111
|
+
object-fit: contain;
|
|
112
|
+
border-radius: 0.5rem;
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { addIcon } from "iconify-icon"
|
|
2
|
+
|
|
3
|
+
// Register custom tiramisu icon
|
|
4
|
+
addIcon("custom:tiramisu", {
|
|
5
|
+
body: '<rect x="3" y="16" width="18" height="3" rx="1" fill="currentColor"/><rect x="4" y="11" width="16" height="3" rx="1" fill="currentColor" opacity="0.7"/><rect x="5" y="6" width="14" height="3" rx="1" fill="currentColor" opacity="0.5"/><circle cx="8" cy="4" r="0.7" fill="currentColor" opacity="0.6"/><circle cx="12" cy="3.5" r="0.7" fill="currentColor" opacity="0.4"/><circle cx="16" cy="4" r="0.7" fill="currentColor" opacity="0.6"/>',
|
|
6
|
+
width: 24,
|
|
7
|
+
height: 24,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
/** Map language/tool names to Iconify icon identifiers */
|
|
11
|
+
const icons: Record<string, string> = {
|
|
12
|
+
// JS/TS
|
|
13
|
+
typescript: "devicon-plain:typescript",
|
|
14
|
+
javascript: "devicon-plain:javascript",
|
|
15
|
+
tsx: "devicon-plain:typescript",
|
|
16
|
+
jsx: "devicon-plain:javascript",
|
|
17
|
+
// Web
|
|
18
|
+
html: "devicon-plain:html5",
|
|
19
|
+
css: "devicon-plain:css3",
|
|
20
|
+
svelte: "devicon-plain:svelte",
|
|
21
|
+
react: "devicon-plain:react",
|
|
22
|
+
vue: "devicon-plain:vuejs",
|
|
23
|
+
angular: "devicon-plain:angularjs",
|
|
24
|
+
// Shell
|
|
25
|
+
bash: "devicon-plain:bash",
|
|
26
|
+
shell: "devicon-plain:bash",
|
|
27
|
+
zsh: "devicon-plain:bash",
|
|
28
|
+
// Languages
|
|
29
|
+
python: "devicon-plain:python",
|
|
30
|
+
rust: "devicon-plain:rust",
|
|
31
|
+
go: "devicon-plain:go",
|
|
32
|
+
java: "devicon-plain:java",
|
|
33
|
+
csharp: "devicon-plain:csharp",
|
|
34
|
+
ruby: "devicon-plain:ruby",
|
|
35
|
+
php: "devicon-plain:php",
|
|
36
|
+
swift: "devicon-plain:swift",
|
|
37
|
+
kotlin: "devicon-plain:kotlin",
|
|
38
|
+
dart: "devicon-plain:dart",
|
|
39
|
+
c: "devicon-plain:c",
|
|
40
|
+
cpp: "devicon-plain:cplusplus",
|
|
41
|
+
// Data/Config
|
|
42
|
+
json: "devicon-plain:json",
|
|
43
|
+
yaml: "devicon-plain:yaml",
|
|
44
|
+
markdown: "devicon-plain:markdown",
|
|
45
|
+
graphql: "devicon-plain:graphql",
|
|
46
|
+
// Tools/Runtimes
|
|
47
|
+
docker: "devicon-plain:docker",
|
|
48
|
+
git: "devicon-plain:git",
|
|
49
|
+
nginx: "devicon-plain:nginx-original",
|
|
50
|
+
redis: "devicon-plain:redis",
|
|
51
|
+
postgresql: "devicon-plain:postgresql",
|
|
52
|
+
mongodb: "devicon-plain:mongodb",
|
|
53
|
+
// Package managers / runtimes
|
|
54
|
+
npm: "devicon-plain:npm",
|
|
55
|
+
yarn: "devicon-plain:yarn",
|
|
56
|
+
pnpm: "devicon-plain:pnpm",
|
|
57
|
+
bun: "devicon-plain:bun",
|
|
58
|
+
deno: "devicon-plain:denojs",
|
|
59
|
+
node: "devicon-plain:nodejs",
|
|
60
|
+
nodejs: "devicon-plain:nodejs",
|
|
61
|
+
// Custom
|
|
62
|
+
tiramisu: "custom:tiramisu",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Aliases
|
|
66
|
+
icons.ts = icons.typescript
|
|
67
|
+
icons.js = icons.javascript
|
|
68
|
+
icons.sh = icons.bash
|
|
69
|
+
icons.py = icons.python
|
|
70
|
+
icons.rs = icons.rust
|
|
71
|
+
icons.md = icons.markdown
|
|
72
|
+
icons.yml = icons.yaml
|
|
73
|
+
icons["c++"] = icons.cpp
|
|
74
|
+
icons["c#"] = icons.csharp
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get an Iconify icon name for a language, or empty string if unknown.
|
|
78
|
+
*/
|
|
79
|
+
export function getLangIcon(language: string): string {
|
|
80
|
+
return icons[language.toLowerCase()] ?? ""
|
|
81
|
+
}
|