@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,117 @@
1
+ ---
2
+ import { transformFileTree } from "../../utils/rehype-file-tree";
3
+
4
+ const html = await Astro.slots.render("default");
5
+ const transformed = await transformFileTree(html);
6
+ ---
7
+
8
+ <div class="ft-root not-prose" set:html={transformed} />
9
+
10
+ <style>
11
+ .ft-root {
12
+ --ft-indent: 1.25rem;
13
+ --ft-icon-size: 1rem;
14
+ font-size: 0.875rem;
15
+ line-height: 1.625;
16
+ border: 1px solid var(--color-fd-border);
17
+ border-radius: 0.5rem;
18
+ padding: 1rem 1.25rem;
19
+ background: var(--color-fd-card);
20
+ }
21
+
22
+ .ft-root :global(.ft-list) {
23
+ list-style: none;
24
+ margin: 0;
25
+ padding: 0;
26
+ }
27
+
28
+ .ft-root :global(.ft-list .ft-list) {
29
+ padding-left: var(--ft-indent);
30
+ }
31
+
32
+ .ft-root :global(.ft-entry) {
33
+ position: relative;
34
+ }
35
+
36
+ .ft-root :global(.ft-entry-inner) {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 0.375rem;
40
+ padding: 0.0625rem 0;
41
+ }
42
+
43
+ .ft-root :global(.ft-icon) {
44
+ width: var(--ft-icon-size);
45
+ height: var(--ft-icon-size);
46
+ flex-shrink: 0;
47
+ color: var(--color-fd-muted-foreground);
48
+ }
49
+
50
+ .ft-root :global(.ft-directory > details > .ft-dir-summary .ft-icon),
51
+ .ft-root :global(.ft-directory > .ft-entry-inner .ft-icon) {
52
+ color: var(--color-fd-primary);
53
+ }
54
+
55
+ .ft-root :global(.ft-name) {
56
+ color: var(--color-fd-foreground);
57
+ }
58
+
59
+ .ft-root :global(.ft-highlight) {
60
+ font-weight: 600;
61
+ color: var(--color-fd-primary);
62
+ }
63
+
64
+ .ft-root :global(.ft-comment) {
65
+ color: var(--color-fd-muted-foreground);
66
+ font-size: 0.8125rem;
67
+ }
68
+
69
+ .ft-root :global(.ft-placeholder-text) {
70
+ color: var(--color-fd-muted-foreground);
71
+ user-select: none;
72
+ }
73
+
74
+ /* Collapsible directories */
75
+ .ft-root :global(details) {
76
+ position: relative;
77
+ }
78
+
79
+ .ft-root :global(summary) {
80
+ cursor: pointer;
81
+ list-style: none;
82
+ }
83
+
84
+ .ft-root :global(summary::-webkit-details-marker) {
85
+ display: none;
86
+ }
87
+
88
+ .ft-root :global(summary::marker) {
89
+ display: none;
90
+ content: "";
91
+ }
92
+
93
+ .ft-root :global(.ft-dir-summary) {
94
+ border-radius: 0.25rem;
95
+ }
96
+
97
+ .ft-root :global(.ft-dir-summary:hover) {
98
+ background: var(--color-fd-muted);
99
+ }
100
+
101
+ /* Tree lines for nested items */
102
+ .ft-root :global(.ft-list .ft-list) {
103
+ border-left: 1px solid var(--color-fd-border);
104
+ margin-left: calc(var(--ft-icon-size) / 2);
105
+ }
106
+
107
+ .ft-root :global(a) {
108
+ color: var(--color-fd-foreground);
109
+ text-decoration: underline;
110
+ text-decoration-color: var(--color-fd-border);
111
+ text-underline-offset: 2px;
112
+ }
113
+
114
+ .ft-root :global(a:hover) {
115
+ text-decoration-color: var(--color-fd-foreground);
116
+ }
117
+ </style>
@@ -0,0 +1,18 @@
1
+ ---
2
+ interface Props {
3
+ title?: string;
4
+ }
5
+
6
+ const { title } = Astro.props;
7
+ ---
8
+
9
+ <div class="relative [counter-increment:step]">
10
+ <div
11
+ class="absolute -left-[calc(1.5rem+1px+0.625rem)] w-5 h-5 rounded-full bg-fd-primary text-fd-primary-foreground flex items-center justify-center text-xs font-bold before:content-[counter(step)]"
12
+ >
13
+ </div>
14
+ {title && <h4 class="font-semibold text-sm mb-2">{title}</h4>}
15
+ <div class="text-sm [&>h3]:text-base [&>h3]:font-semibold [&>h3]:mb-2 [&>h3]:mt-0 [&>p]:mb-2 [&>p]:text-fd-muted-foreground">
16
+ <slot />
17
+ </div>
18
+ </div>
@@ -0,0 +1,6 @@
1
+ ---
2
+ ---
3
+
4
+ <div class="steps-container my-6 ml-4 border-l-2 border-fd-border pl-6 space-y-6 [counter-reset:step]">
5
+ <slot />
6
+ </div>
@@ -0,0 +1,11 @@
1
+ ---
2
+ interface Props {
3
+ label: string;
4
+ }
5
+
6
+ const { label } = Astro.props;
7
+ ---
8
+
9
+ <div class="tab-panel hidden" role="tabpanel" data-tab-label={label}>
10
+ <slot />
11
+ </div>
@@ -0,0 +1,73 @@
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="tabs-wrapper my-5 rounded-lg border border-fd-border overflow-hidden" data-tabs-id={id}>
18
+ <!-- Tab buttons -->
19
+ <div class="flex border-b border-fd-border bg-fd-muted/50" role="tablist">
20
+ {
21
+ labels.map((label, i) => (
22
+ <button
23
+ role="tab"
24
+ class:list={[
25
+ "tabs-trigger px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px",
26
+ i === 0
27
+ ? "border-fd-primary text-fd-foreground"
28
+ : "border-transparent text-fd-muted-foreground hover:text-fd-foreground",
29
+ ]}
30
+ data-tab-index={i}
31
+ aria-selected={i === 0 ? "true" : "false"}
32
+ >
33
+ {label}
34
+ </button>
35
+ ))
36
+ }
37
+ </div>
38
+ <!-- Tab panels -->
39
+ <div class="tabs-panels p-4" set:html={tabs} />
40
+ </div>
41
+
42
+ <script>
43
+ document.querySelectorAll<HTMLElement>(".tabs-wrapper").forEach((wrapper) => {
44
+ const triggers = wrapper.querySelectorAll<HTMLButtonElement>(".tabs-trigger");
45
+ const panels = wrapper.querySelectorAll<HTMLElement>(":scope > .tabs-panels > .tab-panel");
46
+ const id = wrapper.dataset.tabsId || "";
47
+
48
+ function activate(index: number) {
49
+ triggers.forEach((t, i) => {
50
+ const active = i === index;
51
+ t.setAttribute("aria-selected", String(active));
52
+ t.classList.toggle("border-fd-primary", active);
53
+ t.classList.toggle("text-fd-foreground", active);
54
+ t.classList.toggle("border-transparent", !active);
55
+ t.classList.toggle("text-fd-muted-foreground", !active);
56
+ });
57
+ panels.forEach((p, i) => {
58
+ p.classList.toggle("hidden", i !== index);
59
+ });
60
+ if (id) localStorage.setItem(`tab-${id}`, String(index));
61
+ }
62
+
63
+ triggers.forEach((trigger) => {
64
+ trigger.addEventListener("click", () => {
65
+ activate(Number(trigger.dataset.tabIndex));
66
+ });
67
+ });
68
+
69
+ // Restore from localStorage or default to first tab
70
+ const stored = id ? localStorage.getItem(`tab-${id}`) : null;
71
+ activate(stored !== null ? Number(stored) : 0);
72
+ });
73
+ </script>
package/components.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { default as Callout } from "./components/mdx/Callout.astro";
2
+ export { default as Card } from "./components/mdx/Card.astro";
3
+ export { default as CardGrid } from "./components/mdx/CardGrid.astro";
4
+ export { default as Tabs } from "./components/mdx/Tabs.astro";
5
+ export { default as Tab } from "./components/mdx/Tab.astro";
6
+ export { default as Steps } from "./components/mdx/Steps.astro";
7
+ export { default as Step } from "./components/mdx/Step.astro";
8
+ export { default as Accordion } from "./components/mdx/Accordion.astro";
9
+ export { default as FileTree } from "./components/mdx/FileTree.astro";
10
+ export { default as CodeTabs } from "./components/mdx/CodeTabs.astro";
11
+ export { Icon } from "astro-icon/components";
package/config.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+
3
+ const sidebarGroupSchema: z.ZodType<{
4
+ label: string;
5
+ order: number;
6
+ children?: Record<string, { label: string; order: number; children?: Record<string, any> }>;
7
+ }> = z.lazy(() =>
8
+ z.object({
9
+ label: z.string(),
10
+ order: z.number(),
11
+ children: z.record(z.string(), sidebarGroupSchema).optional().default({}),
12
+ }),
13
+ );
14
+
15
+ const configSchema = z.object({
16
+ title: z.string().default("Docs"),
17
+ sidebar: z
18
+ .record(z.string(), sidebarGroupSchema)
19
+ .optional()
20
+ .default({}),
21
+ social: z
22
+ .object({
23
+ github: z.string().optional(),
24
+ })
25
+ .optional()
26
+ .default({}),
27
+ primaryHue: z.number().min(0).max(360).optional().default(55),
28
+ customCss: z.array(z.string()).optional().default([]),
29
+ });
30
+
31
+ export type LoreUserConfig = z.input<typeof configSchema>;
32
+ export type LoreConfig = z.output<typeof configSchema>;
33
+
34
+ export interface SidebarGroupConfig {
35
+ label: string;
36
+ order: number;
37
+ children?: Record<string, SidebarGroupConfig>;
38
+ }
39
+
40
+ export function validateConfig(userConfig: LoreUserConfig): LoreConfig {
41
+ return configSchema.parse(userConfig);
42
+ }
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { defineIntegration as default } from "./integration";
2
+ export type { LoreConfig, LoreUserConfig } from "./config";
package/integration.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { AstroIntegration } from "astro";
2
+ import { fileURLToPath } from "node:url";
3
+ import expressiveCode from "astro-expressive-code";
4
+ import mdx from "@astrojs/mdx";
5
+ import sitemap from "@astrojs/sitemap";
6
+ import icon from "astro-icon";
7
+ import pagefind from "astro-pagefind";
8
+ import tailwindcss from "@tailwindcss/vite";
9
+ import { validateConfig, type LoreUserConfig } from "./config";
10
+ import { vitePluginLore } from "./vite-plugin";
11
+
12
+ export function defineIntegration(userConfig: LoreUserConfig = {}): AstroIntegration {
13
+ const config = validateConfig(userConfig);
14
+
15
+ return {
16
+ name: "lore",
17
+ hooks: {
18
+ "astro:config:setup": ({
19
+ injectRoute,
20
+ updateConfig,
21
+ }) => {
22
+ const routePath = fileURLToPath(
23
+ new URL("./routes/docs.astro", import.meta.url),
24
+ );
25
+
26
+ injectRoute({
27
+ pattern: "docs/[...slug]",
28
+ entrypoint: routePath,
29
+ });
30
+
31
+ updateConfig({
32
+ integrations: [
33
+ expressiveCode({
34
+ themes: ["github-dark", "github-light"],
35
+ themeCssSelector: (theme) => {
36
+ if (theme.name === "github-dark") return ".dark";
37
+ return ":root:not(.dark)";
38
+ },
39
+ defaultProps: {
40
+ frame: "code",
41
+ overridesByLang: {
42
+ "bash,sh,shell,zsh,powershell,ps,bat,cmd,terminal": {
43
+ frame: "code",
44
+ },
45
+ },
46
+ },
47
+ styleOverrides: {
48
+ borderRadius: "0.5rem",
49
+ codeFontFamily: "'Geist Mono', monospace",
50
+ codeFontSize: "0.875rem",
51
+ },
52
+ }),
53
+ mdx(),
54
+ sitemap(),
55
+ icon(),
56
+ pagefind(),
57
+ ],
58
+ vite: {
59
+ plugins: [
60
+ tailwindcss(),
61
+ vitePluginLore(config),
62
+ ],
63
+ },
64
+ });
65
+ },
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,277 @@
1
+ ---
2
+ import "../styles/global.css";
3
+ import "../styles/prose.css";
4
+ import "virtual:lore/user-css";
5
+ import { Icon } from "astro-icon/components";
6
+ import Sidebar from "../components/docs/Sidebar.astro";
7
+ import TableOfContents from "../components/docs/TableOfContents.astro";
8
+ import Breadcrumbs from "../components/docs/Breadcrumbs.astro";
9
+ import PrevNext from "../components/docs/PrevNext.astro";
10
+ import SearchModal from "../components/global/SearchModal.astro";
11
+ import type { SidebarEntry } from "../utils/sidebar";
12
+ import type { TocItem } from "../utils/toc";
13
+ import type { FlatNavItem } from "../utils/navigation";
14
+ import config from "virtual:lore/config";
15
+
16
+ interface Props {
17
+ title: string;
18
+ description?: string;
19
+ sidebar: SidebarEntry[];
20
+ toc: TocItem[];
21
+ showToc: boolean;
22
+ prev: FlatNavItem | null;
23
+ next: FlatNavItem | null;
24
+ lastUpdated?: Date | null;
25
+ }
26
+
27
+ const { title, description, sidebar, toc, showToc, prev, next, lastUpdated } = Astro.props;
28
+ const BASE_URL = import.meta.env.BASE_URL || "/";
29
+ const currentPath = Astro.url.pathname.replace(/\/$/, "") || `${BASE_URL}docs`;
30
+ const siteTitle = config.title;
31
+ const githubUrl = config.social?.github;
32
+ const primaryHue = config.primaryHue;
33
+ ---
34
+
35
+ <!doctype html>
36
+ <html lang="en">
37
+ <head>
38
+ <meta charset="utf-8" />
39
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
40
+ <meta name="description" content={description || "Documentation"} />
41
+ <link rel="icon" type="image/svg+xml" href={`${BASE_URL}favicon.svg`} />
42
+ <meta name="generator" content={Astro.generator} />
43
+ <title>{`${title} - ${siteTitle}`}</title>
44
+ <style set:html={`:root { --fd-hue: ${primaryHue}; }`}></style>
45
+ <script is:inline>
46
+ (function () {
47
+ const stored = localStorage.getItem("theme");
48
+ if (
49
+ stored === "dark" ||
50
+ (!stored && window.matchMedia("(prefers-color-scheme: dark)").matches)
51
+ ) {
52
+ document.documentElement.classList.add("dark");
53
+ }
54
+ })();
55
+ </script>
56
+ </head>
57
+ <body class="min-h-screen">
58
+ <!-- Desktop sidebar (fixed left panel) -->
59
+ <aside
60
+ id="docs-sidebar"
61
+ class="hidden lg:flex fixed top-0 bottom-0 w-(--width-sidebar) flex-col bg-fd-background z-30"
62
+ style="left: max(0px, calc((100vw - var(--max-width-layout)) / 2))"
63
+ >
64
+ <!-- Sidebar header -->
65
+ <div class="flex items-center justify-between h-(--height-header) px-4 shrink-0">
66
+ <a href={BASE_URL} class="flex items-center gap-2 font-semibold text-[15px]">
67
+ <Icon name="lucide:book-open" class="w-5 h-5 text-fd-primary" />
68
+ <span>{siteTitle}</span>
69
+ </a>
70
+ </div>
71
+
72
+ <!-- Search trigger -->
73
+ <div class="px-3 mb-3 shrink-0">
74
+ <button
75
+ id="search-trigger"
76
+ class="flex items-center gap-2 w-full text-sm text-fd-muted-foreground hover:text-fd-foreground bg-fd-muted/60 hover:bg-fd-muted rounded-lg px-3 py-2 transition-colors border border-fd-border"
77
+ >
78
+ <Icon name="lucide:search" class="w-4 h-4 shrink-0" />
79
+ <span class="flex-1 text-left">Search</span>
80
+ <span class="flex items-center gap-0.5 text-[10px] font-mono">
81
+ <kbd class="rounded border border-fd-border bg-fd-background px-1 py-0.5">⌘</kbd>
82
+ <kbd class="rounded border border-fd-border bg-fd-background px-1 py-0.5">K</kbd>
83
+ </span>
84
+ </button>
85
+ </div>
86
+
87
+ <!-- Navigation -->
88
+ <nav class="flex-1 overflow-y-auto px-3 pb-4 scrollbar-thin" aria-label="Documentation sidebar">
89
+ <Sidebar entries={sidebar} currentPath={currentPath} />
90
+ </nav>
91
+
92
+ <!-- Sidebar footer -->
93
+ <div class="flex items-center gap-1 px-3 py-3 border-t border-fd-border shrink-0">
94
+ {githubUrl && (
95
+ <a
96
+ href={githubUrl}
97
+ target="_blank"
98
+ rel="noopener noreferrer"
99
+ class="inline-flex items-center justify-center w-8 h-8 rounded-md hover:bg-fd-muted transition-colors text-fd-muted-foreground hover:text-fd-foreground"
100
+ aria-label="GitHub"
101
+ >
102
+ <Icon name="lucide:github" class="w-4 h-4" />
103
+ </a>
104
+ )}
105
+ <button
106
+ id="theme-toggle"
107
+ class="inline-flex items-center justify-center w-8 h-8 rounded-md hover:bg-fd-muted transition-colors text-fd-muted-foreground hover:text-fd-foreground"
108
+ aria-label="Toggle dark mode"
109
+ >
110
+ <Icon name="lucide:sun" class="w-4 h-4 hidden dark:block" />
111
+ <Icon name="lucide:moon" class="w-4 h-4 block dark:hidden" />
112
+ </button>
113
+ </div>
114
+ </aside>
115
+
116
+ <!-- Mobile header -->
117
+ <header class="lg:hidden sticky top-0 z-40 flex items-center justify-between h-(--height-header) px-4 bg-fd-background/80 backdrop-blur-lg border-b border-fd-border">
118
+ <div class="flex items-center gap-3">
119
+ <button
120
+ id="mobile-sidebar-toggle"
121
+ class="inline-flex items-center justify-center w-8 h-8 rounded-md hover:bg-fd-muted transition-colors"
122
+ aria-label="Toggle sidebar"
123
+ >
124
+ <Icon name="lucide:menu" class="w-4.5 h-4.5" />
125
+ </button>
126
+ <a href={BASE_URL} class="font-semibold text-sm">{siteTitle}</a>
127
+ </div>
128
+ <div class="flex items-center gap-1">
129
+ <button
130
+ id="search-trigger-mobile"
131
+ class="inline-flex items-center justify-center w-8 h-8 rounded-md hover:bg-fd-muted transition-colors"
132
+ aria-label="Search"
133
+ >
134
+ <Icon name="lucide:search" class="w-4 h-4" />
135
+ </button>
136
+ </div>
137
+ </header>
138
+
139
+ <!-- Main content area -->
140
+ <div class="max-w-(--max-width-layout) mx-auto lg:pl-(--width-sidebar)">
141
+ <div class="flex">
142
+ <!-- Article content -->
143
+ <article class="min-w-0 flex-1 px-6 lg:px-10 py-8 lg:py-10">
144
+ <Breadcrumbs />
145
+
146
+ <div class="fd-prose">
147
+ <h1>{title}</h1>
148
+ {description && <p class="text-fd-muted-foreground text-base mt-1 mb-6">{description}</p>}
149
+ <slot />
150
+ </div>
151
+
152
+ <PrevNext prev={prev} next={next} />
153
+
154
+ {lastUpdated && (
155
+ <div class="mt-8 text-xs text-fd-muted-foreground">
156
+ Last updated on {lastUpdated.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
157
+ </div>
158
+ )}
159
+ </article>
160
+
161
+ <!-- Table of contents -->
162
+ {
163
+ showToc && toc.length > 0 && (
164
+ <aside class="hidden xl:block w-(--width-toc) shrink-0 py-10">
165
+ <div class="sticky top-(--height-header) pt-2">
166
+ <TableOfContents items={toc} />
167
+ </div>
168
+ </aside>
169
+ )
170
+ }
171
+ </div>
172
+ </div>
173
+
174
+ <!-- Mobile sidebar overlay -->
175
+ <div id="mobile-sidebar-overlay" class="lg:hidden fixed inset-0 z-50 hidden">
176
+ <div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="mobile-sidebar-backdrop"></div>
177
+ <div
178
+ class="absolute left-0 top-0 bottom-0 w-72 bg-fd-background overflow-y-auto transform transition-transform duration-200 -translate-x-full flex flex-col"
179
+ id="mobile-sidebar-panel"
180
+ >
181
+ <!-- Mobile sidebar header -->
182
+ <div class="flex items-center justify-between h-(--height-header) px-4 shrink-0">
183
+ <a href={BASE_URL} class="flex items-center gap-2 font-semibold text-[15px]">
184
+ <Icon name="lucide:book-open" class="w-5 h-5 text-fd-primary" />
185
+ <span>{siteTitle}</span>
186
+ </a>
187
+ <button id="mobile-sidebar-close" class="p-1 rounded hover:bg-fd-muted" aria-label="Close">
188
+ <Icon name="lucide:x" class="w-5 h-5" />
189
+ </button>
190
+ </div>
191
+
192
+ <!-- Mobile search -->
193
+ <div class="px-3 mb-3 shrink-0">
194
+ <button
195
+ id="search-trigger-mobile-sidebar"
196
+ class="flex items-center gap-2 w-full text-sm text-fd-muted-foreground hover:text-fd-foreground bg-fd-muted/60 hover:bg-fd-muted rounded-lg px-3 py-2 transition-colors border border-fd-border"
197
+ >
198
+ <Icon name="lucide:search" class="w-4 h-4 shrink-0" />
199
+ <span>Search</span>
200
+ </button>
201
+ </div>
202
+
203
+ <!-- Mobile nav -->
204
+ <nav class="flex-1 overflow-y-auto px-3 pb-4 scrollbar-thin">
205
+ <Sidebar entries={sidebar} currentPath={currentPath} />
206
+ </nav>
207
+
208
+ <!-- Mobile sidebar footer -->
209
+ <div class="flex items-center gap-1 px-3 py-3 border-t border-fd-border shrink-0">
210
+ {githubUrl && (
211
+ <a
212
+ href={githubUrl}
213
+ target="_blank"
214
+ rel="noopener noreferrer"
215
+ class="inline-flex items-center justify-center w-8 h-8 rounded-md hover:bg-fd-muted transition-colors text-fd-muted-foreground hover:text-fd-foreground"
216
+ aria-label="GitHub"
217
+ >
218
+ <Icon name="lucide:github" class="w-4 h-4" />
219
+ </a>
220
+ )}
221
+ <button
222
+ id="mobile-theme-toggle"
223
+ class="inline-flex items-center justify-center w-8 h-8 rounded-md hover:bg-fd-muted transition-colors text-fd-muted-foreground hover:text-fd-foreground"
224
+ aria-label="Toggle dark mode"
225
+ >
226
+ <Icon name="lucide:sun" class="w-4 h-4 hidden dark:block" />
227
+ <Icon name="lucide:moon" class="w-4 h-4 block dark:hidden" />
228
+ </button>
229
+ </div>
230
+ </div>
231
+ </div>
232
+
233
+ <SearchModal />
234
+
235
+ <script>
236
+ // Theme toggle (desktop + mobile)
237
+ function toggleTheme() {
238
+ const isDark = document.documentElement.classList.toggle("dark");
239
+ localStorage.setItem("theme", isDark ? "dark" : "light");
240
+ }
241
+ document.getElementById("theme-toggle")?.addEventListener("click", toggleTheme);
242
+ document.getElementById("mobile-theme-toggle")?.addEventListener("click", toggleTheme);
243
+
244
+ // Mobile sidebar
245
+ const toggle = document.getElementById("mobile-sidebar-toggle");
246
+ const overlay = document.getElementById("mobile-sidebar-overlay");
247
+ const backdrop = document.getElementById("mobile-sidebar-backdrop");
248
+ const panel = document.getElementById("mobile-sidebar-panel");
249
+ const close = document.getElementById("mobile-sidebar-close");
250
+
251
+ function openSidebar() {
252
+ overlay?.classList.remove("hidden");
253
+ requestAnimationFrame(() => {
254
+ panel?.classList.remove("-translate-x-full");
255
+ });
256
+ }
257
+
258
+ function closeSidebar() {
259
+ panel?.classList.add("-translate-x-full");
260
+ setTimeout(() => overlay?.classList.add("hidden"), 200);
261
+ }
262
+
263
+ toggle?.addEventListener("click", openSidebar);
264
+ backdrop?.addEventListener("click", closeSidebar);
265
+ close?.addEventListener("click", closeSidebar);
266
+
267
+ // Mobile sidebar search trigger
268
+ document.getElementById("search-trigger-mobile-sidebar")?.addEventListener("click", () => {
269
+ closeSidebar();
270
+ const dialog = document.getElementById("search-dialog") as HTMLDialogElement;
271
+ const input = document.getElementById("search-input") as HTMLInputElement;
272
+ dialog?.showModal();
273
+ input?.focus();
274
+ });
275
+ </script>
276
+ </body>
277
+ </html>
package/loaders.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { glob } from "astro/loaders";
2
+
3
+ export function docsLoader() {
4
+ return glob({ pattern: "**/*.mdx", base: "./src/content/docs" });
5
+ }