@tayacrystals/lore 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.
@@ -0,0 +1,41 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ const BASE_URL = import.meta.env.BASE_URL || "/";
5
+ const path = Astro.url.pathname.replace(BASE_URL.replace(/\/$/, ""), "").replace(/\/$/, "");
6
+ const segments = path.split("/").filter(Boolean);
7
+
8
+ function titleCase(s: string) {
9
+ return s
10
+ .split("-")
11
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
12
+ .join(" ");
13
+ }
14
+
15
+ const crumbs = segments.map((seg, i) => ({
16
+ label: titleCase(seg),
17
+ href: BASE_URL + segments.slice(0, i + 1).join("/"),
18
+ current: i === segments.length - 1,
19
+ }));
20
+ ---
21
+
22
+ {
23
+ crumbs.length > 1 && (
24
+ <nav class="flex items-center gap-1 text-[13px] text-fd-muted-foreground mb-6" aria-label="Breadcrumb">
25
+ {crumbs.map((crumb, i) => (
26
+ <>
27
+ {i > 0 && (
28
+ <Icon name="lucide:chevron-right" class="w-3 h-3 opacity-50" />
29
+ )}
30
+ {crumb.current ? (
31
+ <span class="text-fd-foreground">{crumb.label}</span>
32
+ ) : (
33
+ <a href={crumb.href} class="hover:text-fd-foreground transition-colors">
34
+ {crumb.label}
35
+ </a>
36
+ )}
37
+ </>
38
+ ))}
39
+ </nav>
40
+ )
41
+ }
@@ -0,0 +1,50 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import type { FlatNavItem } from "../../utils/navigation";
4
+
5
+ interface Props {
6
+ prev: FlatNavItem | null;
7
+ next: FlatNavItem | null;
8
+ }
9
+
10
+ const { prev, next } = Astro.props;
11
+ ---
12
+
13
+ {
14
+ (prev || next) && (
15
+ <div class="flex flex-col sm:flex-row justify-between gap-3 mt-12 pt-6 border-t border-fd-border">
16
+ {prev ? (
17
+ <a
18
+ href={prev.href}
19
+ class="group flex items-center gap-3 px-4 py-3 rounded-lg border border-fd-border hover:bg-fd-muted/50 transition-colors flex-1"
20
+ >
21
+ <Icon name="lucide:chevron-left" class="w-4 h-4 text-fd-muted-foreground shrink-0" />
22
+ <div class="min-w-0">
23
+ <span class="text-xs text-fd-muted-foreground block">Previous</span>
24
+ <span class="text-sm font-medium text-fd-foreground group-hover:text-fd-primary transition-colors truncate block">
25
+ {prev.label}
26
+ </span>
27
+ </div>
28
+ </a>
29
+ ) : (
30
+ <div class="flex-1" />
31
+ )}
32
+ {next ? (
33
+ <a
34
+ href={next.href}
35
+ class="group flex items-center justify-end gap-3 px-4 py-3 rounded-lg border border-fd-border hover:bg-fd-muted/50 transition-colors flex-1"
36
+ >
37
+ <div class="min-w-0 text-right">
38
+ <span class="text-xs text-fd-muted-foreground block">Next</span>
39
+ <span class="text-sm font-medium text-fd-foreground group-hover:text-fd-primary transition-colors truncate block">
40
+ {next.label}
41
+ </span>
42
+ </div>
43
+ <Icon name="lucide:chevron-right" class="w-4 h-4 text-fd-muted-foreground shrink-0" />
44
+ </a>
45
+ ) : (
46
+ <div class="flex-1" />
47
+ )}
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,28 @@
1
+ ---
2
+ import SidebarItem from "./SidebarItem.astro";
3
+ import SidebarGroup from "./SidebarGroup.astro";
4
+ import type { SidebarEntry } from "../../utils/sidebar";
5
+
6
+ interface Props {
7
+ entries: SidebarEntry[];
8
+ currentPath: string;
9
+ }
10
+
11
+ const { entries, currentPath } = Astro.props;
12
+ const BASE_URL = import.meta.env.BASE_URL || "/";
13
+ const docsHref = `${BASE_URL}docs`;
14
+ ---
15
+
16
+ <div class="space-y-1">
17
+ <SidebarItem label="Overview" href={docsHref} active={currentPath === docsHref} />
18
+ {
19
+ entries.map((entry) => {
20
+ if (entry.type === "link") {
21
+ return <SidebarItem label={entry.label} href={entry.href} active={entry.href === currentPath} icon={entry.icon} />;
22
+ }
23
+ return (
24
+ <SidebarGroup label={entry.label} items={entry.items} currentPath={currentPath} depth={0} />
25
+ );
26
+ })
27
+ }
28
+ </div>
@@ -0,0 +1,55 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import SidebarItem from "./SidebarItem.astro";
4
+ import type { SidebarEntry } from "../../utils/sidebar";
5
+
6
+ interface Props {
7
+ label: string;
8
+ items: SidebarEntry[];
9
+ currentPath: string;
10
+ depth?: number;
11
+ }
12
+
13
+ const { label, items, currentPath, depth = 0 } = Astro.props;
14
+
15
+ function hasActiveDescendant(entries: SidebarEntry[], path: string): boolean {
16
+ for (const entry of entries) {
17
+ if (entry.type === "link" && entry.href === path) return true;
18
+ if (entry.type === "group" && hasActiveDescendant(entry.items, path)) return true;
19
+ }
20
+ return false;
21
+ }
22
+
23
+ const isOpen = hasActiveDescendant(items, currentPath);
24
+ ---
25
+
26
+ {depth === 0 ? (
27
+ <div class="mt-4 first:mt-0">
28
+ <p class="px-2 mb-1 text-xs font-semibold text-fd-foreground/80 tracking-wide">{label}</p>
29
+ <div class="space-y-0.5">
30
+ {items.map((item) =>
31
+ item.type === "link" ? (
32
+ <SidebarItem label={item.label} href={item.href} active={item.href === currentPath} icon={item.icon} />
33
+ ) : (
34
+ <Astro.self label={item.label} items={item.items} currentPath={currentPath} depth={depth + 1} />
35
+ )
36
+ )}
37
+ </div>
38
+ </div>
39
+ ) : (
40
+ <details class="group/sub" open={isOpen}>
41
+ <summary class="flex items-center gap-1 px-2 py-1.5 text-[13px] font-medium text-fd-muted-foreground hover:text-fd-foreground cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
42
+ <Icon name="lucide:chevron-right" class="w-3 h-3 shrink-0 transition-transform group-open/sub:rotate-90" />
43
+ {label}
44
+ </summary>
45
+ <div class="ml-3 pl-2 border-l border-fd-border/50 space-y-0.5 mt-0.5">
46
+ {items.map((item) =>
47
+ item.type === "link" ? (
48
+ <SidebarItem label={item.label} href={item.href} active={item.href === currentPath} icon={item.icon} />
49
+ ) : (
50
+ <Astro.self label={item.label} items={item.items} currentPath={currentPath} depth={depth + 1} />
51
+ )
52
+ )}
53
+ </div>
54
+ </details>
55
+ )}
@@ -0,0 +1,26 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ interface Props {
5
+ label: string;
6
+ href: string;
7
+ active: boolean;
8
+ icon?: string;
9
+ }
10
+
11
+ const { label, href, active, icon } = Astro.props;
12
+ ---
13
+
14
+ <a
15
+ href={href}
16
+ class:list={[
17
+ "flex items-center gap-2 px-2 py-1.5 text-[13px] rounded-md transition-colors",
18
+ active
19
+ ? "bg-fd-primary/10 text-fd-primary font-medium"
20
+ : "text-fd-muted-foreground hover:text-fd-foreground hover:bg-fd-muted/60",
21
+ ]}
22
+ aria-current={active ? "page" : undefined}
23
+ >
24
+ {icon && <Icon name={icon} class="w-3.5 h-3.5 shrink-0 opacity-60" />}
25
+ {label}
26
+ </a>
@@ -0,0 +1,82 @@
1
+ ---
2
+ import type { TocItem } from "../../utils/toc";
3
+
4
+ interface Props {
5
+ items: TocItem[];
6
+ }
7
+
8
+ const { items } = Astro.props;
9
+ ---
10
+
11
+ <nav aria-label="Table of contents">
12
+ <p class="text-xs font-semibold text-fd-foreground/80 mb-3 tracking-wide">On this page</p>
13
+ <ul class="space-y-0.5 text-[13px] border-l border-fd-border" id="toc-list">
14
+ {
15
+ items.map((item) => (
16
+ <li>
17
+ <a
18
+ href={`#${item.slug}`}
19
+ class="toc-link block py-1 pl-3 -ml-px border-l border-transparent text-fd-muted-foreground hover:text-fd-foreground transition-colors"
20
+ data-slug={item.slug}
21
+ >
22
+ {item.text}
23
+ </a>
24
+ {item.children.length > 0 && (
25
+ <ul class="space-y-0.5">
26
+ {item.children.map((child) => (
27
+ <li>
28
+ <a
29
+ href={`#${child.slug}`}
30
+ class="toc-link block py-1 pl-6 -ml-px border-l border-transparent text-fd-muted-foreground hover:text-fd-foreground transition-colors text-[12px]"
31
+ data-slug={child.slug}
32
+ >
33
+ {child.text}
34
+ </a>
35
+ </li>
36
+ ))}
37
+ </ul>
38
+ )}
39
+ </li>
40
+ ))
41
+ }
42
+ </ul>
43
+ </nav>
44
+
45
+ <script>
46
+ const links = document.querySelectorAll<HTMLAnchorElement>(".toc-link");
47
+ const headings: { id: string; el: Element }[] = [];
48
+
49
+ links.forEach((link) => {
50
+ const slug = link.dataset.slug;
51
+ if (slug) {
52
+ const el = document.getElementById(slug);
53
+ if (el) headings.push({ id: slug, el });
54
+ }
55
+ });
56
+
57
+ function setActive(slug: string) {
58
+ links.forEach((link) => {
59
+ if (link.dataset.slug === slug) {
60
+ link.classList.add("text-fd-primary", "font-medium", "!border-fd-primary");
61
+ link.classList.remove("text-fd-muted-foreground", "border-transparent");
62
+ } else {
63
+ link.classList.remove("text-fd-primary", "font-medium", "!border-fd-primary");
64
+ link.classList.add("text-fd-muted-foreground", "border-transparent");
65
+ }
66
+ });
67
+ }
68
+
69
+ const observer = new IntersectionObserver(
70
+ (entries) => {
71
+ for (const entry of entries) {
72
+ if (entry.isIntersecting) {
73
+ setActive(entry.target.id);
74
+ break;
75
+ }
76
+ }
77
+ },
78
+ { rootMargin: "-80px 0px -70% 0px" },
79
+ );
80
+
81
+ headings.forEach(({ el }) => observer.observe(el));
82
+ </script>
@@ -0,0 +1,159 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ ---
4
+
5
+ <dialog
6
+ id="search-dialog"
7
+ class="fixed inset-0 z-50 m-0 w-full h-full max-w-full max-h-full bg-transparent p-0 open:flex items-start justify-center pt-[15vh]"
8
+ >
9
+ <div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="search-backdrop"></div>
10
+ <div class="relative w-full max-w-xl mx-4 bg-fd-popover rounded-2xl shadow-2xl border border-fd-border overflow-hidden">
11
+ <!-- Search header -->
12
+ <div class="flex items-center gap-3 px-4 border-b border-fd-border">
13
+ <Icon name="lucide:search" class="w-5 h-5 text-fd-muted-foreground shrink-0" />
14
+ <input
15
+ id="search-input"
16
+ type="search"
17
+ placeholder="Search documentation..."
18
+ class="flex-1 py-4 bg-transparent text-fd-foreground placeholder:text-fd-muted-foreground outline-none text-sm"
19
+ autocomplete="off"
20
+ />
21
+ <kbd class="hidden sm:inline-flex items-center rounded border border-fd-border bg-fd-muted px-1.5 py-0.5 text-[10px] font-mono text-fd-muted-foreground">
22
+ ESC
23
+ </kbd>
24
+ </div>
25
+
26
+ <!-- Search results -->
27
+ <div id="search-results" class="max-h-80 overflow-y-auto p-2 scrollbar-thin">
28
+ <div class="text-center py-8 text-sm text-fd-muted-foreground" id="search-empty">
29
+ Start typing to search...
30
+ </div>
31
+ </div>
32
+
33
+ <!-- Footer -->
34
+ <div class="flex items-center gap-4 px-4 py-2.5 border-t border-fd-border text-xs text-fd-muted-foreground">
35
+ <span class="flex items-center gap-1">
36
+ <kbd class="inline-flex items-center rounded border border-fd-border bg-fd-muted px-1 py-0.5 font-mono">↵</kbd>
37
+ to select
38
+ </span>
39
+ <span class="flex items-center gap-1">
40
+ <kbd class="inline-flex items-center rounded border border-fd-border bg-fd-muted px-1 py-0.5 font-mono">↑↓</kbd>
41
+ to navigate
42
+ </span>
43
+ <span class="flex items-center gap-1">
44
+ <kbd class="inline-flex items-center rounded border border-fd-border bg-fd-muted px-1 py-0.5 font-mono">esc</kbd>
45
+ to close
46
+ </span>
47
+ </div>
48
+ </div>
49
+ </dialog>
50
+
51
+ <script>
52
+ const dialog = document.getElementById("search-dialog") as HTMLDialogElement;
53
+ const input = document.getElementById("search-input") as HTMLInputElement;
54
+ const backdrop = document.getElementById("search-backdrop");
55
+ const results = document.getElementById("search-results");
56
+ const empty = document.getElementById("search-empty");
57
+ let pagefind: any = null;
58
+
59
+ async function loadPagefind() {
60
+ if (pagefind) return pagefind;
61
+ try {
62
+ // pagefind = await import("/pagefind/pagefind.js");
63
+ // await pagefind.init();
64
+ } catch {
65
+ // Pagefind not available in dev mode
66
+ pagefind = null;
67
+ }
68
+ return pagefind;
69
+ }
70
+
71
+ function openSearch() {
72
+ dialog?.showModal();
73
+ input?.focus();
74
+ loadPagefind();
75
+ }
76
+
77
+ function closeSearch() {
78
+ dialog?.close();
79
+ if (input) input.value = "";
80
+ if (results && empty) {
81
+ results.innerHTML = "";
82
+ results.appendChild(empty);
83
+ empty.textContent = "Start typing to search...";
84
+ }
85
+ }
86
+
87
+ // Triggers
88
+ document.getElementById("search-trigger")?.addEventListener("click", openSearch);
89
+ document.getElementById("search-trigger-mobile")?.addEventListener("click", openSearch);
90
+ backdrop?.addEventListener("click", closeSearch);
91
+
92
+ // Keyboard shortcut
93
+ document.addEventListener("keydown", (e) => {
94
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
95
+ e.preventDefault();
96
+ if (dialog?.open) closeSearch();
97
+ else openSearch();
98
+ }
99
+ if (e.key === "Escape" && dialog?.open) {
100
+ closeSearch();
101
+ }
102
+ });
103
+
104
+ // Search input
105
+ let debounce: ReturnType<typeof setTimeout>;
106
+ input?.addEventListener("input", () => {
107
+ clearTimeout(debounce);
108
+ debounce = setTimeout(async () => {
109
+ const query = input.value.trim();
110
+ if (!query || !results) {
111
+ if (results && empty) {
112
+ results.innerHTML = "";
113
+ results.appendChild(empty);
114
+ empty.textContent = query ? "No results found." : "Start typing to search...";
115
+ }
116
+ return;
117
+ }
118
+
119
+ const pf = await loadPagefind();
120
+ if (!pf) {
121
+ if (results && empty) {
122
+ results.innerHTML = "";
123
+ results.appendChild(empty);
124
+ empty.textContent = "Search is available after building the site.";
125
+ }
126
+ return;
127
+ }
128
+
129
+ const search = await pf.search(query);
130
+ results.innerHTML = "";
131
+
132
+ if (search.results.length === 0) {
133
+ results.appendChild(empty!);
134
+ empty!.textContent = "No results found.";
135
+ return;
136
+ }
137
+
138
+ for (const result of search.results.slice(0, 8)) {
139
+ const data = await result.data();
140
+ const a = document.createElement("a");
141
+ a.href = data.url;
142
+ a.className =
143
+ "block px-3 py-2.5 rounded-lg hover:bg-fd-muted transition-colors group";
144
+ a.innerHTML = `
145
+ <div class="font-medium text-sm text-fd-foreground group-hover:text-fd-primary transition-colors">${data.meta.title || "Untitled"}</div>
146
+ <div class="text-xs text-fd-muted-foreground mt-0.5 line-clamp-1">${data.excerpt}</div>
147
+ `;
148
+ a.addEventListener("click", closeSearch);
149
+ results.appendChild(a);
150
+ }
151
+ }, 200);
152
+ });
153
+ </script>
154
+
155
+ <style>
156
+ dialog::backdrop {
157
+ background: transparent;
158
+ }
159
+ </style>
@@ -0,0 +1,20 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ interface Props {
5
+ title: string;
6
+ open?: boolean;
7
+ }
8
+
9
+ const { title, open = false } = Astro.props;
10
+ ---
11
+
12
+ <details class="my-4 rounded-lg border border-fd-border group" open={open}>
13
+ <summary class="flex items-center justify-between cursor-pointer px-4 py-3 text-sm font-medium hover:bg-fd-muted/50 transition-colors rounded-lg select-none list-none [&::-webkit-details-marker]:hidden">
14
+ <span>{title}</span>
15
+ <Icon name="lucide:chevron-down" class="w-4 h-4 text-fd-muted-foreground transition-transform group-open:rotate-180" />
16
+ </summary>
17
+ <div class="px-4 pb-4 text-sm [&>p]:mb-2">
18
+ <slot />
19
+ </div>
20
+ </details>
@@ -0,0 +1,53 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ interface Props {
5
+ type?: "note" | "warning" | "tip" | "danger";
6
+ title?: string;
7
+ }
8
+
9
+ const { type = "note", title } = Astro.props;
10
+
11
+ const styles = {
12
+ note: {
13
+ bg: "bg-blue-500/5 dark:bg-blue-500/10",
14
+ border: "border-blue-500/20",
15
+ iconColor: "text-blue-500",
16
+ icon: "lucide:info",
17
+ },
18
+ warning: {
19
+ bg: "bg-amber-500/5 dark:bg-amber-500/10",
20
+ border: "border-amber-500/20",
21
+ iconColor: "text-amber-500",
22
+ icon: "lucide:triangle-alert",
23
+ },
24
+ tip: {
25
+ bg: "bg-emerald-500/5 dark:bg-emerald-500/10",
26
+ border: "border-emerald-500/20",
27
+ iconColor: "text-emerald-500",
28
+ icon: "lucide:lightbulb",
29
+ },
30
+ danger: {
31
+ bg: "bg-red-500/5 dark:bg-red-500/10",
32
+ border: "border-red-500/20",
33
+ iconColor: "text-red-500",
34
+ icon: "lucide:octagon-alert",
35
+ },
36
+ };
37
+
38
+ const c = styles[type];
39
+ ---
40
+
41
+ <div class:list={[
42
+ "my-4 flex gap-3 rounded-lg border px-4 py-3",
43
+ title ? "items-start" : "items-center",
44
+ c.border, c.bg,
45
+ ]}>
46
+ <Icon name={c.icon} class={`w-5 h-5 shrink-0 ${c.iconColor}`} />
47
+ <div class="min-w-0 flex-1">
48
+ {title && <p class={`font-medium text-sm mb-1 ${c.iconColor}`}>{title}</p>}
49
+ <div class="callout-content text-sm text-fd-foreground/90">
50
+ <slot />
51
+ </div>
52
+ </div>
53
+ </div>
@@ -0,0 +1,26 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ interface Props {
5
+ title: string;
6
+ href?: string;
7
+ icon?: string;
8
+ }
9
+
10
+ const { title, href, icon } = Astro.props;
11
+ const Tag = href ? "a" : "div";
12
+ ---
13
+
14
+ <Tag
15
+ href={href}
16
+ class:list={[
17
+ "block rounded-lg border border-fd-border p-4 transition-colors",
18
+ href && "hover:border-fd-primary/50 hover:bg-fd-muted/50",
19
+ ]}
20
+ >
21
+ {icon && <Icon name={icon} class="w-5 h-5 mb-2 text-fd-primary" />}
22
+ <h3 class="font-semibold text-sm mb-1">{title}</h3>
23
+ <div class="text-sm text-fd-muted-foreground [&>p]:mb-0">
24
+ <slot />
25
+ </div>
26
+ </Tag>
@@ -0,0 +1,16 @@
1
+ ---
2
+ interface Props {
3
+ cols?: 2 | 3;
4
+ }
5
+
6
+ const { cols = 2 } = Astro.props;
7
+ ---
8
+
9
+ <div
10
+ class:list={[
11
+ "grid gap-4 my-5 not-prose",
12
+ cols === 3 ? "sm:grid-cols-2 lg:grid-cols-3" : "sm:grid-cols-2",
13
+ ]}
14
+ >
15
+ <slot />
16
+ </div>
@@ -0,0 +1,129 @@
1
+ ---
2
+ interface Props {
3
+ id?: string;
4
+ }
5
+
6
+ const { id = "tabs-" + Math.random().toString(36).slice(2, 8) } = Astro.props;
7
+ const tabs = await Astro.slots.render("default");
8
+ const labels: string[] = [];
9
+
10
+ // Extract labels from Tab components
11
+ const labelMatches = tabs.matchAll(/data-tab-label="([^"]+)"/g);
12
+ for (const match of labelMatches) {
13
+ labels.push(match[1]);
14
+ }
15
+ ---
16
+
17
+ <div class="code-tabs" data-tabs-id={id}>
18
+ <!-- Tab buttons -->
19
+ <div class="code-tabs-header" role="tablist">
20
+ {
21
+ labels.map((label, i) => (
22
+ <button
23
+ role="tab"
24
+ class="code-tabs-trigger"
25
+ data-tab-index={i}
26
+ aria-selected={i === 0 ? "true" : "false"}
27
+ >
28
+ {label}
29
+ </button>
30
+ ))
31
+ }
32
+ </div>
33
+ <!-- Tab panels -->
34
+ <div class="code-tabs-panels" set:html={tabs} />
35
+ </div>
36
+
37
+ <style>
38
+ .code-tabs {
39
+ border: 1px solid var(--color-fd-border);
40
+ border-radius: 0.5rem;
41
+ overflow: hidden;
42
+ margin-top: 0;
43
+ margin-bottom: 1rem;
44
+ }
45
+
46
+ .code-tabs-header {
47
+ display: flex;
48
+ background-color: var(--color-fd-muted);
49
+ border-bottom: 1px solid var(--color-fd-border);
50
+ }
51
+
52
+ .code-tabs-trigger {
53
+ padding: 0.375rem 0.75rem;
54
+ font-size: 0.8rem;
55
+ font-family: var(--font-sans);
56
+ font-weight: 500;
57
+ color: var(--color-fd-muted-foreground);
58
+ background: transparent;
59
+ border: none;
60
+ cursor: pointer;
61
+ transition: color 0.15s;
62
+ border-bottom: 2px solid transparent;
63
+ margin-bottom: -1px;
64
+ }
65
+
66
+ .code-tabs-trigger:hover {
67
+ color: var(--color-fd-foreground);
68
+ }
69
+
70
+ .code-tabs-trigger[aria-selected="true"] {
71
+ color: var(--color-fd-foreground);
72
+ border-bottom-color: var(--color-fd-primary);
73
+ }
74
+
75
+ /* Strip EC styling from nested code blocks */
76
+ .code-tabs-panels :global(.expressive-code) {
77
+ margin: 0 !important;
78
+ }
79
+
80
+ .code-tabs-panels :global(.expressive-code .frame) {
81
+ --ec-brdRad: 0 !important;
82
+ --ec-brdWd: 0 !important;
83
+ box-shadow: none !important;
84
+ border: none !important;
85
+ border-radius: 0 !important;
86
+ }
87
+
88
+ .code-tabs-panels :global(.expressive-code figcaption.header) {
89
+ display: none !important;
90
+ }
91
+
92
+ .code-tabs-panels :global(pre) {
93
+ margin: 0 !important;
94
+ border-radius: 0 !important;
95
+ }
96
+
97
+ .code-tabs-panels :global(figure.frame) {
98
+ margin: 0 !important;
99
+ border-radius: 0 !important;
100
+ }
101
+ </style>
102
+
103
+ <script>
104
+ document.querySelectorAll<HTMLElement>(".code-tabs").forEach((wrapper) => {
105
+ const triggers = wrapper.querySelectorAll<HTMLButtonElement>(".code-tabs-trigger");
106
+ const panels = wrapper.querySelectorAll<HTMLElement>(":scope > .code-tabs-panels > .tab-panel");
107
+ const id = wrapper.dataset.tabsId || "";
108
+
109
+ function activate(index: number) {
110
+ triggers.forEach((t, i) => {
111
+ t.setAttribute("aria-selected", String(i === index));
112
+ });
113
+ panels.forEach((p, i) => {
114
+ p.classList.toggle("hidden", i !== index);
115
+ });
116
+ if (id) localStorage.setItem(`tab-${id}`, String(index));
117
+ }
118
+
119
+ triggers.forEach((trigger) => {
120
+ trigger.addEventListener("click", () => {
121
+ activate(Number(trigger.dataset.tabIndex));
122
+ });
123
+ });
124
+
125
+ // Restore from localStorage or default to first tab
126
+ const stored = id ? localStorage.getItem(`tab-${id}`) : null;
127
+ activate(stored !== null ? Number(stored) : 0);
128
+ });
129
+ </script>