@farming-labs/astro-theme 0.0.2-beta.15

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/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@farming-labs/astro-theme",
3
+ "version": "0.0.2-beta.15",
4
+ "description": "Astro UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.d.ts",
9
+ "import": "./src/index.js",
10
+ "default": "./src/index.js"
11
+ },
12
+ "./src/components/*": "./src/components/*",
13
+ "./fumadocs": {
14
+ "types": "./src/themes/default.d.ts",
15
+ "import": "./src/themes/default.js",
16
+ "default": "./src/themes/default.js"
17
+ },
18
+ "./pixel-border": {
19
+ "types": "./src/themes/pixel-border.d.ts",
20
+ "import": "./src/themes/pixel-border.js",
21
+ "default": "./src/themes/pixel-border.js"
22
+ },
23
+ "./darksharp": {
24
+ "types": "./src/themes/darksharp.d.ts",
25
+ "import": "./src/themes/darksharp.js",
26
+ "default": "./src/themes/darksharp.js"
27
+ },
28
+ "./css": "./styles/docs.css",
29
+ "./fumadocs/css": "./styles/docs.css",
30
+ "./styles/pixel-border.css": "./styles/pixel-border.css",
31
+ "./styles/darksharp.css": "./styles/darksharp.css",
32
+ "./pixel-border/css": "./styles/pixel-border-bundle.css",
33
+ "./darksharp/css": "./styles/darksharp-bundle.css"
34
+ },
35
+ "typesVersions": {
36
+ "*": {
37
+ "pixel-border": [
38
+ "./src/themes/pixel-border.d.ts"
39
+ ],
40
+ "darksharp": [
41
+ "./src/themes/darksharp.d.ts"
42
+ ],
43
+ "fumadocs": [
44
+ "./src/themes/default.d.ts"
45
+ ],
46
+ ".": [
47
+ "./src/index.d.ts"
48
+ ]
49
+ }
50
+ },
51
+ "files": [
52
+ "src",
53
+ "styles"
54
+ ],
55
+ "keywords": [
56
+ "docs",
57
+ "astro",
58
+ "theme",
59
+ "documentation"
60
+ ],
61
+ "author": "Farming Labs",
62
+ "license": "MIT",
63
+ "dependencies": {
64
+ "sugar-high": "^0.9.5",
65
+ "@farming-labs/astro": "0.0.2-beta.15",
66
+ "@farming-labs/docs": "0.0.2-beta.15"
67
+ },
68
+ "peerDependencies": {
69
+ "astro": ">=4.0.0"
70
+ },
71
+ "scripts": {
72
+ "build": "echo 'Astro components are shipped as source'",
73
+ "typecheck": "echo 'ok'"
74
+ }
75
+ }
@@ -0,0 +1,39 @@
1
+ ---
2
+ import DocsPage from "./DocsPage.astro";
3
+
4
+ const { data, config = null } = Astro.props;
5
+
6
+ const titleSuffix = config?.metadata?.titleTemplate
7
+ ? config.metadata.titleTemplate.replace("%s", "")
8
+ : " – Docs";
9
+
10
+ const tocEnabled = config?.theme?.ui?.layout?.toc?.enabled ?? true;
11
+
12
+ const breadcrumbEnabled = (() => {
13
+ const bc = config?.breadcrumb;
14
+ if (bc === undefined || bc === true) return true;
15
+ if (bc === false) return false;
16
+ if (typeof bc === "object") return bc.enabled !== false;
17
+ return true;
18
+ })();
19
+
20
+ const showEditOnGithub = !!config?.github && !!data.editOnGithub;
21
+ const showLastModified = !!data.lastModified;
22
+ ---
23
+
24
+ <head>
25
+ <title>{data.title}{titleSuffix}</title>
26
+ {data.description && <meta name="description" content={data.description} />}
27
+ </head>
28
+
29
+ <DocsPage
30
+ entry={config?.entry ?? "docs"}
31
+ tocEnabled={tocEnabled}
32
+ breadcrumbEnabled={breadcrumbEnabled}
33
+ previousPage={data.previousPage}
34
+ nextPage={data.nextPage}
35
+ editOnGithub={showEditOnGithub ? data.editOnGithub : null}
36
+ lastModified={showLastModified ? data.lastModified : null}
37
+ >
38
+ <Fragment set:html={data.html} />
39
+ </DocsPage>
@@ -0,0 +1,321 @@
1
+ ---
2
+ import ThemeToggle from "./ThemeToggle.astro";
3
+
4
+ const { tree, config = null, title, titleUrl } = Astro.props;
5
+
6
+ const resolvedTitle = title ?? config?.nav?.title ?? "Docs";
7
+ const resolvedTitleUrl = titleUrl ?? config?.nav?.url ?? "/docs";
8
+
9
+ const showThemeToggle = (() => {
10
+ const toggle = config?.themeToggle;
11
+ if (toggle === undefined || toggle === true) return true;
12
+ if (toggle === false) return false;
13
+ if (typeof toggle === "object") return toggle.enabled !== false;
14
+ return true;
15
+ })();
16
+
17
+ const forcedTheme = (() => {
18
+ const toggle = config?.themeToggle;
19
+ if (typeof toggle === "object" && toggle.enabled === false && toggle.default && toggle.default !== "system") {
20
+ return toggle.default;
21
+ }
22
+ return null;
23
+ })();
24
+
25
+ const themeInitScript = forcedTheme
26
+ ? `document.documentElement.classList.remove('light','dark');document.documentElement.classList.add('${forcedTheme}')`
27
+ : [
28
+ "(function(){",
29
+ "var m=document.cookie.match(/(?:^|;\\s*)theme=(\\w+)/);",
30
+ "var t=m?m[1]:(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');",
31
+ "document.documentElement.classList.remove('light','dark');",
32
+ "document.documentElement.classList.add(t);",
33
+ "})()",
34
+ ].join("");
35
+
36
+ const COLOR_MAP: Record<string, string> = {
37
+ primary: "--color-fd-primary",
38
+ primaryForeground: "--color-fd-primary-foreground",
39
+ background: "--color-fd-background",
40
+ foreground: "--color-fd-foreground",
41
+ muted: "--color-fd-muted",
42
+ mutedForeground: "--color-fd-muted-foreground",
43
+ border: "--color-fd-border",
44
+ card: "--color-fd-card",
45
+ cardForeground: "--color-fd-card-foreground",
46
+ accent: "--color-fd-accent",
47
+ accentForeground: "--color-fd-accent-foreground",
48
+ popover: "--color-fd-popover",
49
+ popoverForeground: "--color-fd-popover-foreground",
50
+ secondary: "--color-fd-secondary",
51
+ secondaryForeground: "--color-fd-secondary-foreground",
52
+ ring: "--color-fd-ring",
53
+ };
54
+
55
+ function buildColorsCSS(colors?: Record<string, string | undefined>): string {
56
+ if (!colors) return "";
57
+ const vars: string[] = [];
58
+ for (const [key, value] of Object.entries(colors)) {
59
+ if (!value || !COLOR_MAP[key]) continue;
60
+ vars.push(`${COLOR_MAP[key]}: ${value};`);
61
+ }
62
+ if (vars.length === 0) return "";
63
+ return `:root, .dark {\n ${vars.join("\n ")}\n}`;
64
+ }
65
+
66
+ function buildFontStyleVars(prefix: string, style?: { size?: string; weight?: string | number; lineHeight?: string; letterSpacing?: string }): string {
67
+ if (!style) return "";
68
+ const parts: string[] = [];
69
+ if (style.size) parts.push(`${prefix}-size: ${style.size};`);
70
+ if (style.weight != null) parts.push(`${prefix}-weight: ${style.weight};`);
71
+ if (style.lineHeight) parts.push(`${prefix}-line-height: ${style.lineHeight};`);
72
+ if (style.letterSpacing) parts.push(`${prefix}-letter-spacing: ${style.letterSpacing};`);
73
+ return parts.join("\n ");
74
+ }
75
+
76
+ function buildTypographyCSS(typo?: any): string {
77
+ if (!typo?.font) return "";
78
+ const vars: string[] = [];
79
+ const fontStyle = typo.font.style;
80
+ if (fontStyle?.sans) vars.push(`--fd-font-sans: ${fontStyle.sans};`);
81
+ if (fontStyle?.mono) vars.push(`--fd-font-mono: ${fontStyle.mono};`);
82
+ const elements = ["h1", "h2", "h3", "h4", "body", "small"] as const;
83
+ for (const el of elements) {
84
+ const elStyle = typo.font[el];
85
+ if (elStyle) {
86
+ const elVars = buildFontStyleVars(`--fd-${el}`, elStyle);
87
+ if (elVars) vars.push(elVars);
88
+ }
89
+ }
90
+ if (vars.length === 0) return "";
91
+ return `:root {\n ${vars.join("\n ")}\n}`;
92
+ }
93
+
94
+ const colorOverrides = config?.theme?._userColorOverrides as Record<string, string | undefined> | undefined;
95
+ const typography = config?.theme?.ui?.typography;
96
+ const overrideCSS = [buildColorsCSS(colorOverrides), buildTypographyCSS(typography)].filter(Boolean).join("\n");
97
+
98
+ const currentPath = Astro.url.pathname;
99
+
100
+ function isActive(url) {
101
+ const normalised = url.replace(/\/$/, '') || '/';
102
+ const currentNorm = currentPath.replace(/\/$/, '') || '/';
103
+ return normalised === currentNorm;
104
+ }
105
+
106
+ const ICON_MAP = {
107
+ book: `<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="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`,
108
+ terminal: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`,
109
+ rocket: `<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="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>`,
110
+ settings: `<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="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
111
+ file: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
112
+ code: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`,
113
+ };
114
+
115
+ function getIcon(iconKey) {
116
+ if (!iconKey) return null;
117
+ return ICON_MAP[iconKey] || null;
118
+ }
119
+
120
+ const aiConfig = config?.ai;
121
+ const showFloatingAI = aiConfig?.mode === "floating" && aiConfig?.enabled;
122
+ ---
123
+
124
+ <script is:inline set:html={themeInitScript}></script>
125
+ {overrideCSS && <style set:html={overrideCSS} />}
126
+
127
+ <div class="fd-layout">
128
+ <header class="fd-header">
129
+ <button class="fd-menu-btn" id="fd-menu-toggle" aria-label="Toggle sidebar">
130
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
131
+ <line x1="3" y1="6" x2="21" y2="6" />
132
+ <line x1="3" y1="12" x2="21" y2="12" />
133
+ <line x1="3" y1="18" x2="21" y2="18" />
134
+ </svg>
135
+ </button>
136
+ <a href={resolvedTitleUrl} class="fd-header-title">{resolvedTitle}</a>
137
+ <button class="fd-search-trigger-mobile" id="fd-search-open-mobile" aria-label="Search">
138
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
139
+ <circle cx="11" cy="11" r="8" />
140
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
141
+ </svg>
142
+ </button>
143
+ </header>
144
+
145
+ <div class="fd-sidebar-overlay" id="fd-sidebar-overlay" style="display:none"></div>
146
+
147
+ <aside class="fd-sidebar" id="fd-sidebar">
148
+ <div class="fd-sidebar-header">
149
+ <a href={resolvedTitleUrl} class="fd-sidebar-title">
150
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
151
+ <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
152
+ <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
153
+ </svg>
154
+ {resolvedTitle}
155
+ </a>
156
+ </div>
157
+
158
+ <div class="fd-sidebar-search">
159
+ <button class="fd-sidebar-search-btn" id="fd-search-open">
160
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
161
+ <circle cx="11" cy="11" r="8" />
162
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
163
+ </svg>
164
+ <span>Search</span>
165
+ <kbd>⌘</kbd><kbd>K</kbd>
166
+ </button>
167
+ </div>
168
+
169
+ <nav class="fd-sidebar-nav">
170
+ {tree?.children?.map((node, i) => {
171
+ if (node.type === "page") {
172
+ const icon = getIcon(node.icon);
173
+ return (
174
+ <a
175
+ href={node.url}
176
+ class={`fd-sidebar-link fd-sidebar-top-link ${isActive(node.url) ? 'fd-sidebar-link-active' : ''} ${i === 0 ? 'fd-sidebar-first-item' : ''}`}
177
+ data-active={isActive(node.url) || undefined}
178
+ >
179
+ {icon && <span class="fd-sidebar-icon" set:html={icon} />}
180
+ {node.name}
181
+ </a>
182
+ );
183
+ } else if (node.type === "folder") {
184
+ const folderIcon = getIcon(node.icon);
185
+ return (
186
+ <details class={`fd-sidebar-folder ${i === 0 ? 'fd-sidebar-first-item' : ''}`} open>
187
+ <summary class="fd-sidebar-folder-trigger">
188
+ <span class="fd-sidebar-folder-label">
189
+ {folderIcon && <span class="fd-sidebar-icon" set:html={folderIcon} />}
190
+ {node.name}
191
+ </span>
192
+ <svg class="fd-sidebar-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
193
+ <polyline points="6 9 12 15 18 9" />
194
+ </svg>
195
+ </summary>
196
+ <div class="fd-sidebar-folder-content">
197
+ {node.index && (
198
+ <a
199
+ href={node.index.url}
200
+ class={`fd-sidebar-link fd-sidebar-child-link ${isActive(node.index.url) ? 'fd-sidebar-link-active' : ''}`}
201
+ data-active={isActive(node.index.url) || undefined}
202
+ >
203
+ {node.index.name}
204
+ </a>
205
+ )}
206
+ {node.children?.map(child => {
207
+ if (child.type === "page") {
208
+ return (
209
+ <a
210
+ href={child.url}
211
+ class={`fd-sidebar-link fd-sidebar-child-link ${isActive(child.url) ? 'fd-sidebar-link-active' : ''}`}
212
+ data-active={isActive(child.url) || undefined}
213
+ >
214
+ {child.name}
215
+ </a>
216
+ );
217
+ } else if (child.type === "folder") {
218
+ return (
219
+ <details class="fd-sidebar-folder fd-sidebar-nested-folder" open>
220
+ <summary class="fd-sidebar-folder-trigger">
221
+ <span class="fd-sidebar-folder-label">{child.name}</span>
222
+ <svg class="fd-sidebar-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
223
+ <polyline points="6 9 12 15 18 9" />
224
+ </svg>
225
+ </summary>
226
+ <div class="fd-sidebar-folder-content">
227
+ {child.index && (
228
+ <a
229
+ href={child.index.url}
230
+ class={`fd-sidebar-link fd-sidebar-child-link ${isActive(child.index.url) ? 'fd-sidebar-link-active' : ''}`}
231
+ data-active={isActive(child.index.url) || undefined}
232
+ >
233
+ {child.index.name}
234
+ </a>
235
+ )}
236
+ {child.children?.map(grandchild => {
237
+ if (grandchild.type === "page") {
238
+ return (
239
+ <a
240
+ href={grandchild.url}
241
+ class={`fd-sidebar-link fd-sidebar-child-link ${isActive(grandchild.url) ? 'fd-sidebar-link-active' : ''}`}
242
+ data-active={isActive(grandchild.url) || undefined}
243
+ >
244
+ {grandchild.name}
245
+ </a>
246
+ );
247
+ }
248
+ })}
249
+ </div>
250
+ </details>
251
+ );
252
+ }
253
+ })}
254
+ </div>
255
+ </details>
256
+ );
257
+ }
258
+ })}
259
+ </nav>
260
+
261
+ {showThemeToggle && (
262
+ <div class="fd-sidebar-footer">
263
+ <ThemeToggle />
264
+ </div>
265
+ )}
266
+ </aside>
267
+
268
+ <main class="fd-main">
269
+ <slot />
270
+ </main>
271
+ </div>
272
+
273
+ <script>
274
+ // Sidebar toggle
275
+ const menuBtn = document.getElementById('fd-menu-toggle');
276
+ const sidebar = document.getElementById('fd-sidebar');
277
+ const overlay = document.getElementById('fd-sidebar-overlay');
278
+
279
+ menuBtn?.addEventListener('click', () => {
280
+ sidebar?.classList.toggle('fd-sidebar-open');
281
+ overlay!.style.display = sidebar?.classList.contains('fd-sidebar-open') ? 'block' : 'none';
282
+ });
283
+
284
+ overlay?.addEventListener('click', () => {
285
+ sidebar?.classList.remove('fd-sidebar-open');
286
+ overlay!.style.display = 'none';
287
+ });
288
+
289
+ // Search Cmd+K
290
+ document.addEventListener('keydown', (e) => {
291
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
292
+ e.preventDefault();
293
+ const dialog = document.getElementById('fd-search-dialog');
294
+ if (dialog) dialog.style.display = dialog.style.display === 'none' ? 'flex' : 'none';
295
+ }
296
+ if (e.key === 'Escape') {
297
+ sidebar?.classList.remove('fd-sidebar-open');
298
+ overlay!.style.display = 'none';
299
+ const dialog = document.getElementById('fd-search-dialog');
300
+ if (dialog) dialog.style.display = 'none';
301
+ }
302
+ });
303
+
304
+ document.getElementById('fd-search-open')?.addEventListener('click', () => {
305
+ const dialog = document.getElementById('fd-search-dialog');
306
+ if (dialog) dialog.style.display = 'flex';
307
+ });
308
+
309
+ document.getElementById('fd-search-open-mobile')?.addEventListener('click', () => {
310
+ const dialog = document.getElementById('fd-search-dialog');
311
+ if (dialog) dialog.style.display = 'flex';
312
+ });
313
+
314
+ // Close sidebar on link click (mobile)
315
+ sidebar?.querySelectorAll('a').forEach(link => {
316
+ link.addEventListener('click', () => {
317
+ sidebar?.classList.remove('fd-sidebar-open');
318
+ overlay!.style.display = 'none';
319
+ });
320
+ });
321
+ </script>
@@ -0,0 +1,178 @@
1
+ ---
2
+ const {
3
+ tocEnabled = true,
4
+ breadcrumbEnabled = true,
5
+ entry = "docs",
6
+ previousPage = null,
7
+ nextPage = null,
8
+ editOnGithub = null,
9
+ lastModified = null,
10
+ } = Astro.props;
11
+
12
+ const pathname = Astro.url.pathname;
13
+ const segments = pathname.split("/").filter(Boolean).filter(s => s.toLowerCase() !== entry.toLowerCase());
14
+ const parentLabel = segments.length >= 2 ? segments[segments.length - 2].replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()) : "";
15
+ const currentLabel = segments.length >= 2 ? segments[segments.length - 1].replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()) : "";
16
+ const parentUrl = (() => {
17
+ if (segments.length < 2) return "";
18
+ const all = pathname.split("/").filter(Boolean);
19
+ const parentSegment = segments[segments.length - 2];
20
+ const parentIndex = all.indexOf(parentSegment);
21
+ return "/" + all.slice(0, parentIndex + 1).join("/");
22
+ })();
23
+ ---
24
+
25
+ <div class="fd-page">
26
+ <article class="fd-page-article" id="nd-page">
27
+ {breadcrumbEnabled && segments.length >= 2 && (
28
+ <nav class="fd-breadcrumb" aria-label="Breadcrumb">
29
+ <span class="fd-breadcrumb-item">
30
+ <a href={parentUrl} class="fd-breadcrumb-parent fd-breadcrumb-link">{parentLabel}</a>
31
+ </span>
32
+ <span class="fd-breadcrumb-item">
33
+ <span class="fd-breadcrumb-sep">/</span>
34
+ <span class="fd-breadcrumb-current">{currentLabel}</span>
35
+ </span>
36
+ </nav>
37
+ )}
38
+
39
+ <div class="fd-page-body">
40
+ <slot />
41
+ </div>
42
+
43
+ <footer class="fd-page-footer">
44
+ {(editOnGithub || lastModified) && (
45
+ <div class="fd-edit-on-github">
46
+ {editOnGithub && (
47
+ <a href={editOnGithub} target="_blank" rel="noopener noreferrer">
48
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
49
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
50
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
51
+ </svg>
52
+ Edit on GitHub
53
+ </a>
54
+ )}
55
+ {lastModified && (
56
+ <span class="fd-last-modified">Last updated: {lastModified}</span>
57
+ )}
58
+ </div>
59
+ )}
60
+
61
+ {(previousPage || nextPage) && (
62
+ <nav class="fd-page-nav" aria-label="Page navigation">
63
+ {previousPage ? (
64
+ <a href={previousPage.url} class="fd-page-nav-card fd-page-nav-prev">
65
+ <span class="fd-page-nav-label">
66
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
67
+ <polyline points="15 18 9 12 15 6" />
68
+ </svg>
69
+ Previous
70
+ </span>
71
+ <span class="fd-page-nav-title">{previousPage.name}</span>
72
+ </a>
73
+ ) : <div />}
74
+ {nextPage ? (
75
+ <a href={nextPage.url} class="fd-page-nav-card fd-page-nav-next">
76
+ <span class="fd-page-nav-label">
77
+ Next
78
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
79
+ <polyline points="9 18 15 12 9 6" />
80
+ </svg>
81
+ </span>
82
+ <span class="fd-page-nav-title">{nextPage.name}</span>
83
+ </a>
84
+ ) : <div />}
85
+ </nav>
86
+ )}
87
+ </footer>
88
+ </article>
89
+
90
+ {tocEnabled && (
91
+ <aside class="fd-toc">
92
+ <div class="fd-toc-inner">
93
+ <h3 class="fd-toc-title">
94
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
95
+ <line x1="3" y1="6" x2="21" y2="6" />
96
+ <line x1="3" y1="12" x2="15" y2="12" />
97
+ <line x1="3" y1="18" x2="18" y2="18" />
98
+ </svg>
99
+ On this page
100
+ </h3>
101
+ <ul class="fd-toc-list" id="fd-toc-list"></ul>
102
+ </div>
103
+ </aside>
104
+ )}
105
+ </div>
106
+
107
+ <script>
108
+ function initDocsPage() {
109
+ // Scan headings for TOC
110
+ const container = document.querySelector('.fd-page-body');
111
+ const tocList = document.getElementById('fd-toc-list');
112
+ if (container && tocList) {
113
+ const headings = container.querySelectorAll('h2[id], h3[id], h4[id]');
114
+ tocList.innerHTML = '';
115
+ headings.forEach(el => {
116
+ const depth = parseInt(el.tagName[1], 10);
117
+ const li = document.createElement('li');
118
+ li.className = 'fd-toc-item';
119
+ const a = document.createElement('a');
120
+ a.className = 'fd-toc-link';
121
+ a.href = `#${el.id}`;
122
+ a.textContent = el.textContent?.replace(/^#\s*/, '') || '';
123
+ a.style.paddingLeft = `${12 + (depth - 2) * 12}px`;
124
+ li.appendChild(a);
125
+ tocList.appendChild(li);
126
+ });
127
+
128
+ // Intersection observer for active heading
129
+ const observer = new IntersectionObserver((entries) => {
130
+ for (const entry of entries) {
131
+ if (entry.isIntersecting) {
132
+ tocList.querySelectorAll('.fd-toc-link').forEach(link => {
133
+ link.classList.toggle('fd-toc-link-active', link.getAttribute('href') === `#${entry.target.id}`);
134
+ });
135
+ }
136
+ }
137
+ }, { rootMargin: '-80px 0px -80% 0px' });
138
+
139
+ headings.forEach(el => observer.observe(el));
140
+ }
141
+
142
+ // Copy buttons
143
+ document.querySelectorAll('.fd-copy-btn').forEach(btn => {
144
+ btn.addEventListener('click', () => {
145
+ const code = btn.getAttribute('data-code')
146
+ ?.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"');
147
+ if (!code) return;
148
+ navigator.clipboard.writeText(code).then(() => {
149
+ btn.classList.add('fd-copy-btn-copied');
150
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
151
+ setTimeout(() => {
152
+ btn.classList.remove('fd-copy-btn-copied');
153
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>';
154
+ }, 2000);
155
+ });
156
+ });
157
+ });
158
+
159
+ // Tabs
160
+ document.querySelectorAll('[data-tabs]').forEach(tabs => {
161
+ tabs.querySelectorAll('.fd-tab-trigger').forEach(trigger => {
162
+ trigger.addEventListener('click', () => {
163
+ const val = trigger.getAttribute('data-tab-value');
164
+ tabs.querySelectorAll('.fd-tab-trigger').forEach(t => {
165
+ t.classList.toggle('fd-tab-active', t.getAttribute('data-tab-value') === val);
166
+ t.setAttribute('aria-selected', String(t.getAttribute('data-tab-value') === val));
167
+ });
168
+ tabs.querySelectorAll('.fd-tab-panel').forEach(p => {
169
+ p.classList.toggle('fd-tab-panel-active', p.getAttribute('data-tab-panel') === val);
170
+ });
171
+ });
172
+ });
173
+ });
174
+ }
175
+
176
+ initDocsPage();
177
+ document.addEventListener('astro:after-swap', initDocsPage);
178
+ </script>