@specglass/theme-default 0.0.2

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.
Files changed (141) hide show
  1. package/dist/__tests__/code-tabs.test.d.ts +2 -0
  2. package/dist/__tests__/code-tabs.test.d.ts.map +1 -0
  3. package/dist/__tests__/code-tabs.test.js +219 -0
  4. package/dist/__tests__/code-tabs.test.js.map +1 -0
  5. package/dist/__tests__/copy-button.test.d.ts +2 -0
  6. package/dist/__tests__/copy-button.test.d.ts.map +1 -0
  7. package/dist/__tests__/copy-button.test.js +116 -0
  8. package/dist/__tests__/copy-button.test.js.map +1 -0
  9. package/dist/__tests__/search-palette.test.d.ts +2 -0
  10. package/dist/__tests__/search-palette.test.d.ts.map +1 -0
  11. package/dist/__tests__/search-palette.test.js +71 -0
  12. package/dist/__tests__/search-palette.test.js.map +1 -0
  13. package/dist/__tests__/shiki.test.d.ts +2 -0
  14. package/dist/__tests__/shiki.test.d.ts.map +1 -0
  15. package/dist/__tests__/shiki.test.js +37 -0
  16. package/dist/__tests__/shiki.test.js.map +1 -0
  17. package/dist/__tests__/theme-css.test.d.ts +2 -0
  18. package/dist/__tests__/theme-css.test.d.ts.map +1 -0
  19. package/dist/__tests__/theme-css.test.js +124 -0
  20. package/dist/__tests__/theme-css.test.js.map +1 -0
  21. package/dist/__tests__/theme-helpers.test.d.ts +2 -0
  22. package/dist/__tests__/theme-helpers.test.d.ts.map +1 -0
  23. package/dist/__tests__/theme-helpers.test.js +81 -0
  24. package/dist/__tests__/theme-helpers.test.js.map +1 -0
  25. package/dist/index.d.ts +63 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +13 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/islands/CodeTabs.d.ts +21 -0
  30. package/dist/islands/CodeTabs.d.ts.map +1 -0
  31. package/dist/islands/CodeTabs.js +125 -0
  32. package/dist/islands/CodeTabs.js.map +1 -0
  33. package/dist/islands/CopyButton.d.ts +16 -0
  34. package/dist/islands/CopyButton.d.ts.map +1 -0
  35. package/dist/islands/CopyButton.js +54 -0
  36. package/dist/islands/CopyButton.js.map +1 -0
  37. package/dist/islands/SearchPalette.d.ts +2 -0
  38. package/dist/islands/SearchPalette.d.ts.map +1 -0
  39. package/dist/islands/SearchPalette.js +109 -0
  40. package/dist/islands/SearchPalette.js.map +1 -0
  41. package/dist/islands/SearchResults.d.ts +2 -0
  42. package/dist/islands/SearchResults.d.ts.map +1 -0
  43. package/dist/islands/SearchResults.js +130 -0
  44. package/dist/islands/SearchResults.js.map +1 -0
  45. package/dist/islands/ThemeToggle.d.ts +12 -0
  46. package/dist/islands/ThemeToggle.d.ts.map +1 -0
  47. package/dist/islands/ThemeToggle.js +43 -0
  48. package/dist/islands/ThemeToggle.js.map +1 -0
  49. package/dist/layouts/DocPage.test.d.ts +2 -0
  50. package/dist/layouts/DocPage.test.d.ts.map +1 -0
  51. package/dist/layouts/DocPage.test.js +165 -0
  52. package/dist/layouts/DocPage.test.js.map +1 -0
  53. package/dist/lib/utils.d.ts +10 -0
  54. package/dist/lib/utils.d.ts.map +1 -0
  55. package/dist/lib/utils.js +13 -0
  56. package/dist/lib/utils.js.map +1 -0
  57. package/dist/scripts/code-block-enhancer.d.ts +16 -0
  58. package/dist/scripts/code-block-enhancer.d.ts.map +1 -0
  59. package/dist/scripts/code-block-enhancer.js +55 -0
  60. package/dist/scripts/code-block-enhancer.js.map +1 -0
  61. package/dist/ui/command.d.ts +87 -0
  62. package/dist/ui/command.d.ts.map +1 -0
  63. package/dist/ui/command.js +28 -0
  64. package/dist/ui/command.js.map +1 -0
  65. package/dist/ui/dialog.d.ts +20 -0
  66. package/dist/ui/dialog.d.ts.map +1 -0
  67. package/dist/ui/dialog.js +22 -0
  68. package/dist/ui/dialog.js.map +1 -0
  69. package/dist/utils/parse-highlight-range.d.ts +12 -0
  70. package/dist/utils/parse-highlight-range.d.ts.map +1 -0
  71. package/dist/utils/parse-highlight-range.js +40 -0
  72. package/dist/utils/parse-highlight-range.js.map +1 -0
  73. package/dist/utils/parse-highlight-range.test.d.ts +2 -0
  74. package/dist/utils/parse-highlight-range.test.d.ts.map +1 -0
  75. package/dist/utils/parse-highlight-range.test.js +32 -0
  76. package/dist/utils/parse-highlight-range.test.js.map +1 -0
  77. package/dist/utils/schema-renderer.d.ts +38 -0
  78. package/dist/utils/schema-renderer.d.ts.map +1 -0
  79. package/dist/utils/schema-renderer.js +115 -0
  80. package/dist/utils/schema-renderer.js.map +1 -0
  81. package/dist/utils/schema-renderer.test.d.ts +2 -0
  82. package/dist/utils/schema-renderer.test.d.ts.map +1 -0
  83. package/dist/utils/schema-renderer.test.js +219 -0
  84. package/dist/utils/schema-renderer.test.js.map +1 -0
  85. package/dist/utils/shiki.d.ts +20 -0
  86. package/dist/utils/shiki.d.ts.map +1 -0
  87. package/dist/utils/shiki.js +84 -0
  88. package/dist/utils/shiki.js.map +1 -0
  89. package/dist/utils/sidebar-helpers.d.ts +10 -0
  90. package/dist/utils/sidebar-helpers.d.ts.map +1 -0
  91. package/dist/utils/sidebar-helpers.js +14 -0
  92. package/dist/utils/sidebar-helpers.js.map +1 -0
  93. package/dist/utils/theme-css.d.ts +21 -0
  94. package/dist/utils/theme-css.d.ts.map +1 -0
  95. package/dist/utils/theme-css.js +77 -0
  96. package/dist/utils/theme-css.js.map +1 -0
  97. package/dist/utils/theme-helpers.d.ts +28 -0
  98. package/dist/utils/theme-helpers.d.ts.map +1 -0
  99. package/dist/utils/theme-helpers.js +55 -0
  100. package/dist/utils/theme-helpers.js.map +1 -0
  101. package/dist/utils/toc-helpers.d.ts +12 -0
  102. package/dist/utils/toc-helpers.d.ts.map +1 -0
  103. package/dist/utils/toc-helpers.js +9 -0
  104. package/dist/utils/toc-helpers.js.map +1 -0
  105. package/package.json +68 -0
  106. package/src/components/ApiAuth.astro +116 -0
  107. package/src/components/ApiEndpoint.astro +75 -0
  108. package/src/components/ApiNavigation.astro +110 -0
  109. package/src/components/ApiParameters.astro +204 -0
  110. package/src/components/ApiResponse.astro +144 -0
  111. package/src/components/Callout.astro +54 -0
  112. package/src/components/Card.astro +46 -0
  113. package/src/components/CodeBlock.astro +142 -0
  114. package/src/components/CodeBlockGroup.astro +196 -0
  115. package/src/components/CodeTabs.astro +53 -0
  116. package/src/components/Footer.astro +41 -0
  117. package/src/components/Header.astro +80 -0
  118. package/src/components/Sidebar.astro +117 -0
  119. package/src/components/TabItem.astro +24 -0
  120. package/src/components/TableOfContents.astro +111 -0
  121. package/src/components/Tabs.astro +185 -0
  122. package/src/islands/CodeTabs.tsx +212 -0
  123. package/src/islands/CopyButton.tsx +101 -0
  124. package/src/islands/SearchPalette.tsx +307 -0
  125. package/src/islands/SearchResults.tsx +301 -0
  126. package/src/islands/ThemeToggle.tsx +107 -0
  127. package/src/layouts/ApiReferencePage.astro +239 -0
  128. package/src/layouts/DocPage.astro +199 -0
  129. package/src/layouts/DocPage.test.ts +183 -0
  130. package/src/layouts/LandingPage.astro +143 -0
  131. package/src/lib/utils.ts +13 -0
  132. package/src/styles/global.css +241 -0
  133. package/src/utils/parse-highlight-range.test.ts +40 -0
  134. package/src/utils/parse-highlight-range.ts +41 -0
  135. package/src/utils/schema-renderer.test.ts +269 -0
  136. package/src/utils/schema-renderer.ts +152 -0
  137. package/src/utils/shiki.ts +99 -0
  138. package/src/utils/sidebar-helpers.ts +24 -0
  139. package/src/utils/theme-css.ts +101 -0
  140. package/src/utils/theme-helpers.ts +59 -0
  141. package/src/utils/toc-helpers.ts +11 -0
@@ -0,0 +1,107 @@
1
+ import { useState, useCallback, useEffect } from "react";
2
+ import {
3
+ THEME_STORAGE_KEY,
4
+ applyTheme,
5
+ type Theme,
6
+ } from "../utils/theme-helpers";
7
+
8
+ /**
9
+ * ThemeToggle — React island for switching between dark and light mode.
10
+ *
11
+ * Renders as a button with sun/moon icons that crossfade on toggle.
12
+ * Reads initial state from the DOM (set by the FOUC-prevention inline script),
13
+ * toggles .dark class on <html>, and persists preference to localStorage.
14
+ *
15
+ * Uses inline SVGs — no icon library dependency to keep bundle minimal.
16
+ * Imports shared utilities from theme-helpers.ts for consistency.
17
+ */
18
+
19
+ export function ThemeToggle() {
20
+ const [isDark, setIsDark] = useState(() => {
21
+ // Read initial state from DOM — FOUC script already set .dark class
22
+ if (typeof document !== "undefined") {
23
+ return document.documentElement.classList.contains("dark");
24
+ }
25
+ return true; // SSR fallback: default dark
26
+ });
27
+
28
+ // Sync with external changes (e.g., other tabs via StorageEvent)
29
+ useEffect(() => {
30
+ const handleStorage = (e: StorageEvent) => {
31
+ if (e.key === THEME_STORAGE_KEY) {
32
+ const newTheme: Theme = e.newValue === "light" ? "light" : "dark";
33
+ setIsDark(newTheme === "dark");
34
+ applyTheme(newTheme);
35
+ }
36
+ };
37
+ window.addEventListener("storage", handleStorage);
38
+ return () => window.removeEventListener("storage", handleStorage);
39
+ }, []);
40
+
41
+ const toggleTheme = useCallback(() => {
42
+ // Read from DOM (source of truth) to avoid stale closure issues
43
+ const currentlyDark =
44
+ document.documentElement.classList.contains("dark");
45
+ const newIsDark = !currentlyDark;
46
+ setIsDark(newIsDark);
47
+ applyTheme(newIsDark ? "dark" : "light");
48
+ }, []);
49
+
50
+ return (
51
+ <button
52
+ type="button"
53
+ onClick={toggleTheme}
54
+ aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
55
+ className="inline-flex items-center justify-center rounded-md p-2 text-text-muted hover:text-(--color-text) hover:bg-hover-bg focus:outline-none focus-visible:ring-2 focus-visible:ring-primary transition-colors"
56
+ >
57
+ {/* Icon container — both icons rendered, crossfade via CSS transitions */}
58
+ <span className="relative inline-flex items-center justify-center w-5 h-5">
59
+ {/* Sun icon — visible in dark mode, click to switch to light */}
60
+ <svg
61
+ xmlns="http://www.w3.org/2000/svg"
62
+ width="20"
63
+ height="20"
64
+ viewBox="0 0 24 24"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ strokeWidth="2"
68
+ strokeLinecap="round"
69
+ strokeLinejoin="round"
70
+ aria-hidden="true"
71
+ className={`absolute inset-0 transition-transform duration-300 ${
72
+ isDark ? "rotate-0 scale-100" : "-rotate-90 scale-0"
73
+ }`}
74
+ >
75
+ <circle cx="12" cy="12" r="4" />
76
+ <path d="M12 2v2" />
77
+ <path d="M12 20v2" />
78
+ <path d="m4.93 4.93 1.41 1.41" />
79
+ <path d="m17.66 17.66 1.41 1.41" />
80
+ <path d="M2 12h2" />
81
+ <path d="M20 12h2" />
82
+ <path d="m6.34 17.66-1.41 1.41" />
83
+ <path d="m19.07 4.93-1.41 1.41" />
84
+ </svg>
85
+
86
+ {/* Moon icon — visible in light mode, click to switch to dark */}
87
+ <svg
88
+ xmlns="http://www.w3.org/2000/svg"
89
+ width="20"
90
+ height="20"
91
+ viewBox="0 0 24 24"
92
+ fill="none"
93
+ stroke="currentColor"
94
+ strokeWidth="2"
95
+ strokeLinecap="round"
96
+ strokeLinejoin="round"
97
+ aria-hidden="true"
98
+ className={`absolute inset-0 transition-transform duration-300 ${
99
+ !isDark ? "rotate-0 scale-100" : "rotate-90 scale-0"
100
+ }`}
101
+ >
102
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
103
+ </svg>
104
+ </span>
105
+ </button>
106
+ );
107
+ }
@@ -0,0 +1,239 @@
1
+ ---
2
+ /**
3
+ * ApiReferencePage.astro — Layout for API reference endpoint pages.
4
+ * Mirrors DocPage structure (Header, Sidebar, Footer, mobile menu) but
5
+ * replaces the Table of Contents with API endpoint navigation.
6
+ */
7
+ import Header from "../components/Header.astro";
8
+ import Footer from "../components/Footer.astro";
9
+ import ApiNavigation from "../components/ApiNavigation.astro";
10
+ import ApiEndpoint from "../components/ApiEndpoint.astro";
11
+ import ApiParameters from "../components/ApiParameters.astro";
12
+ import ApiResponse from "../components/ApiResponse.astro";
13
+ import ApiAuth from "../components/ApiAuth.astro";
14
+ import { ThemeToggle } from "../islands/ThemeToggle";
15
+ import { SearchPalette } from "../islands/SearchPalette";
16
+ import "../styles/global.css";
17
+ import type {
18
+ NavigationTree,
19
+ ApiEndpoint as ApiEndpointType,
20
+ ApiSecurityRequirement,
21
+ SpecglassConfig,
22
+ } from "@specglass/core";
23
+ import { generateThemeCSS } from "../utils/theme-css";
24
+
25
+ export interface Props {
26
+ /** The current endpoint to display */
27
+ endpoint: ApiEndpointType;
28
+ /** Documentation navigation tree */
29
+ navigation: NavigationTree;
30
+ /** Site configuration */
31
+ config: SpecglassConfig;
32
+ /** All endpoints for the sidebar navigation */
33
+ allEndpoints: ApiEndpointType[];
34
+ /** Spec-level information */
35
+ specInfo: {
36
+ title: string;
37
+ version: string;
38
+ };
39
+ /** Global security schemes for the whole spec */
40
+ securitySchemes?: Record<string, ApiSecurityRequirement>;
41
+ }
42
+
43
+ const { endpoint, navigation, config, allEndpoints, specInfo, securitySchemes } = Astro.props;
44
+ const themeCSS = generateThemeCSS(config);
45
+
46
+ const pageTitle = `${endpoint.method.toUpperCase()} ${endpoint.path} — ${specInfo.title} API`;
47
+ const pageDescription =
48
+ endpoint.summary ||
49
+ endpoint.description ||
50
+ `API reference for ${endpoint.method.toUpperCase()} ${endpoint.path}`;
51
+ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
52
+ ---
53
+
54
+ <html lang="en" class="dark">
55
+ <head>
56
+ <meta charset="utf-8" />
57
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
58
+ <title>{pageTitle}</title>
59
+ <meta name="description" content={pageDescription} />
60
+ {config.theme?.favicon && <link rel="icon" href={config.theme.favicon} />}
61
+ {config.theme?.socialImage && <meta property="og:image" content={config.theme.socialImage} />}
62
+ {themeCSS && <style set:html={themeCSS} />}
63
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
64
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
65
+ <link
66
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
67
+ rel="stylesheet"
68
+ />
69
+ <!-- FOUC prevention: apply stored theme before first paint -->
70
+ <script is:inline>
71
+ (function () {
72
+ function applyStoredTheme() {
73
+ var stored = null;
74
+ try {
75
+ stored = localStorage.getItem("specglass-theme");
76
+ } catch (e) {}
77
+ var theme = stored === "dark" || stored === "light" ? stored : "dark";
78
+ if (theme === "dark") {
79
+ document.documentElement.classList.add("dark");
80
+ } else {
81
+ document.documentElement.classList.remove("dark");
82
+ }
83
+ }
84
+ applyStoredTheme();
85
+ if (!window.__sgThemeInit) {
86
+ window.__sgThemeInit = true;
87
+ document.addEventListener("astro:after-swap", applyStoredTheme);
88
+ }
89
+ })();
90
+ </script>
91
+ </head>
92
+ <body class="min-h-screen bg-surface text-text">
93
+ <!-- Skip to content link for accessibility -->
94
+ <a
95
+ href="#main-content"
96
+ class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:bg-primary focus:text-white focus:px-4 focus:py-2 focus:rounded"
97
+ >
98
+ Skip to content
99
+ </a>
100
+
101
+ <Header>
102
+ <SearchPalette client:idle />
103
+ <ThemeToggle client:load />
104
+ </Header>
105
+
106
+ <div class="flex pt-(--height-header)">
107
+ <!-- API Endpoint Navigation (desktop) -->
108
+ <nav
109
+ id="sidebar"
110
+ aria-label="API endpoint navigation"
111
+ class="hidden md:block fixed top-(--height-header) left-0 bottom-0 w-(--width-sidebar) overflow-y-auto border-r border-border bg-sidebar-bg px-4 py-6"
112
+ data-pagefind-ignore
113
+ >
114
+ <!-- Spec title header -->
115
+ <div class="mb-4 px-3">
116
+ <h2 class="text-sm font-semibold text-text truncate">{specInfo.title}</h2>
117
+ <span class="text-xs text-text-muted">v{specInfo.version}</span>
118
+ </div>
119
+
120
+ <ApiNavigation
121
+ endpoints={allEndpoints}
122
+ currentEndpointId={currentEndpointId}
123
+ basePath="/api-reference"
124
+ />
125
+ </nav>
126
+
127
+ <!-- Mobile sidebar overlay -->
128
+ <div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-30 hidden" aria-hidden="true">
129
+ </div>
130
+
131
+ <!-- Mobile sidebar drawer -->
132
+ <nav
133
+ id="sidebar-mobile"
134
+ aria-label="API endpoint navigation"
135
+ class="fixed top-(--height-header) left-0 bottom-0 w-(--width-sidebar) overflow-y-auto border-r border-border bg-sidebar-bg px-4 py-6 z-40 -translate-x-full transition-transform duration-(--transition-normal) md:hidden"
136
+ >
137
+ <div class="mb-4 px-3">
138
+ <h2 class="text-sm font-semibold text-text truncate">{specInfo.title}</h2>
139
+ <span class="text-xs text-text-muted">v{specInfo.version}</span>
140
+ </div>
141
+ <ApiNavigation
142
+ endpoints={allEndpoints}
143
+ currentEndpointId={currentEndpointId}
144
+ basePath="/api-reference"
145
+ />
146
+ </nav>
147
+
148
+ <!-- Main content area -->
149
+ <main id="main-content" class="flex-1 min-w-0 md:ml-(--width-sidebar)" data-pagefind-body>
150
+ <article class="max-w-(--width-content-max) mx-auto px-(--spacing-page) py-8">
151
+ <!-- Endpoint header -->
152
+ <ApiEndpoint endpoint={endpoint} />
153
+
154
+ <!-- Authentication -->
155
+ {
156
+ endpoint.security && endpoint.security.length > 0 && (
157
+ <ApiAuth security={endpoint.security} securitySchemes={securitySchemes} />
158
+ )
159
+ }
160
+
161
+ <!-- Parameters & Request Body -->
162
+ {
163
+ (endpoint.parameters.length > 0 || endpoint.requestBody) && (
164
+ <div>
165
+ <h2 class="text-lg font-semibold text-text mb-4 mt-0">Parameters</h2>
166
+ <ApiParameters
167
+ parameters={endpoint.parameters}
168
+ requestBody={endpoint.requestBody}
169
+ />
170
+ </div>
171
+ )
172
+ }
173
+
174
+ <!-- Responses -->
175
+ {
176
+ endpoint.responses.length > 0 && (
177
+ <div>
178
+ <h2 class="text-lg font-semibold text-text mb-4">Responses</h2>
179
+ <ApiResponse responses={endpoint.responses} />
180
+ </div>
181
+ )
182
+ }
183
+ </article>
184
+ </main>
185
+ </div>
186
+
187
+ <!-- Mobile menu toggle script -->
188
+ <script>
189
+ function initMobileMenu() {
190
+ const toggle = document.getElementById("mobile-menu-toggle");
191
+ const sidebar = document.getElementById("sidebar-mobile");
192
+ const overlay = document.getElementById("sidebar-overlay");
193
+ const mainContent = document.getElementById("main-content");
194
+
195
+ if (!toggle || !sidebar || !overlay) return;
196
+
197
+ function openMenu() {
198
+ sidebar!.classList.remove("-translate-x-full");
199
+ overlay!.classList.remove("hidden");
200
+ toggle!.setAttribute("aria-expanded", "true");
201
+ document.body.style.overflow = "hidden";
202
+ mainContent?.setAttribute("inert", "");
203
+ }
204
+
205
+ function closeMenu() {
206
+ sidebar!.classList.add("-translate-x-full");
207
+ overlay!.classList.add("hidden");
208
+ toggle!.setAttribute("aria-expanded", "false");
209
+ document.body.style.overflow = "";
210
+ mainContent?.removeAttribute("inert");
211
+ }
212
+
213
+ toggle.addEventListener("click", () => {
214
+ const isOpen = toggle.getAttribute("aria-expanded") === "true";
215
+ if (isOpen) {
216
+ closeMenu();
217
+ } else {
218
+ openMenu();
219
+ }
220
+ });
221
+
222
+ overlay.addEventListener("click", closeMenu);
223
+
224
+ document.addEventListener("keydown", (e) => {
225
+ if (e.key === "Escape") closeMenu();
226
+ });
227
+ }
228
+
229
+ initMobileMenu();
230
+ document.addEventListener("astro:after-swap", initMobileMenu);
231
+ </script>
232
+ <!-- Fenced code block copy button enhancer -->
233
+ <script>
234
+ import "../scripts/code-block-enhancer.ts";
235
+ </script>
236
+
237
+ <Footer />
238
+ </body>
239
+ </html>
@@ -0,0 +1,199 @@
1
+ ---
2
+ import Header from "../components/Header.astro";
3
+ import Footer from "../components/Footer.astro";
4
+ import Sidebar from "../components/Sidebar.astro";
5
+ import TableOfContents from "../components/TableOfContents.astro";
6
+ import { ThemeToggle } from "../islands/ThemeToggle";
7
+ import { SearchPalette } from "../islands/SearchPalette";
8
+ import "../styles/global.css";
9
+ import type { NavigationTree } from "@specglass/core";
10
+ import { config } from "virtual:specglass/config";
11
+ import { generateThemeCSS } from "../utils/theme-css";
12
+
13
+ const themeCSS = generateThemeCSS(config);
14
+
15
+ export interface Props {
16
+ /** Page title from frontmatter */
17
+ title: string;
18
+ /** Page description from frontmatter */
19
+ description?: string;
20
+ /** Full frontmatter data */
21
+ frontmatter: Record<string, unknown>;
22
+ /** Navigation tree for sidebar */
23
+ navigation: NavigationTree;
24
+ /** Headings extracted from the page content for ToC */
25
+ headings: Array<{ depth: number; slug: string; text: string }>;
26
+ /** Current page slug for sidebar active state */
27
+ currentSlug: string;
28
+ }
29
+
30
+ const { title, description, frontmatter, navigation, headings, currentSlug } = Astro.props;
31
+ ---
32
+
33
+ <html lang="en" class="dark">
34
+ <head>
35
+ <meta charset="utf-8" />
36
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
37
+ <title>{title}</title>
38
+ {description && <meta name="description" content={description} />}
39
+ {config.theme?.favicon && <link rel="icon" href={config.theme.favicon} />}
40
+ {config.theme?.socialImage && <meta property="og:image" content={config.theme.socialImage} />}
41
+ {themeCSS && <style set:html={themeCSS} />}
42
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
43
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
44
+ <link
45
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
46
+ rel="stylesheet"
47
+ />
48
+ <!-- FOUC prevention: apply stored theme before first paint -->
49
+ <script is:inline>
50
+ (function () {
51
+ function applyStoredTheme() {
52
+ var stored = null;
53
+ try {
54
+ stored = localStorage.getItem("specglass-theme");
55
+ } catch (e) {}
56
+ var theme = stored === "dark" || stored === "light" ? stored : "dark";
57
+ if (theme === "dark") {
58
+ document.documentElement.classList.add("dark");
59
+ } else {
60
+ document.documentElement.classList.remove("dark");
61
+ }
62
+ }
63
+ applyStoredTheme();
64
+ // Re-apply after Astro ViewTransitions page swap (guard prevents duplicate listeners)
65
+ if (!window.__sgThemeInit) {
66
+ window.__sgThemeInit = true;
67
+ document.addEventListener("astro:after-swap", applyStoredTheme);
68
+ }
69
+ })();
70
+ </script>
71
+ </head>
72
+ <body class="min-h-screen bg-surface text-text">
73
+ <!-- Skip to content link for accessibility -->
74
+ <a
75
+ href="#main-content"
76
+ class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:bg-primary focus:text-white focus:px-4 focus:py-2 focus:rounded"
77
+ >
78
+ Skip to content
79
+ </a>
80
+
81
+ <Header>
82
+ <SearchPalette client:idle />
83
+ <ThemeToggle client:load />
84
+ </Header>
85
+
86
+ <div class="flex pt-(--height-header)">
87
+ <!-- Sidebar navigation -->
88
+ <nav
89
+ id="sidebar"
90
+ aria-label="Documentation navigation"
91
+ class="hidden md:block fixed top-(--height-header) left-0 bottom-0 w-(--width-sidebar) overflow-y-auto border-r border-border bg-sidebar-bg px-4 py-6"
92
+ data-pagefind-ignore
93
+ >
94
+ <Sidebar items={navigation.items} currentSlug={currentSlug} />
95
+ </nav>
96
+
97
+ <!-- Mobile sidebar overlay -->
98
+ <div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-30 hidden" aria-hidden="true">
99
+ </div>
100
+
101
+ <!-- Mobile sidebar drawer -->
102
+ <nav
103
+ id="sidebar-mobile"
104
+ aria-label="Documentation navigation"
105
+ class="fixed top-(--height-header) left-0 bottom-0 w-(--width-sidebar) overflow-y-auto border-r border-border bg-sidebar-bg px-4 py-6 z-40 -translate-x-full transition-transform duration-(--transition-normal) md:hidden"
106
+ >
107
+ <Sidebar items={navigation.items} currentSlug={currentSlug} />
108
+ </nav>
109
+
110
+ <!-- Main content area -->
111
+ <main
112
+ id="main-content"
113
+ class="flex-1 min-w-0 md:ml-(--width-sidebar) lg:mr-(--width-toc)"
114
+ data-pagefind-body
115
+ >
116
+ <article class="sg-content max-w-(--width-content-max) mx-auto px-(--spacing-page) py-8">
117
+ <h1 class="text-3xl font-bold tracking-tight mb-2">
118
+ {frontmatter.title}
119
+ </h1>
120
+ {
121
+ frontmatter.description && (
122
+ <p class="text-text-muted text-lg mb-8 mt-0">{frontmatter.description}</p>
123
+ )
124
+ }
125
+ <slot />
126
+ </article>
127
+ </main>
128
+
129
+ <!-- Table of Contents -->
130
+ {
131
+ headings.length > 0 && (
132
+ <aside
133
+ aria-label="Table of contents"
134
+ class="hidden lg:block fixed top-(--height-header) right-0 bottom-0 w-(--width-toc) overflow-y-auto py-6 px-4"
135
+ data-pagefind-ignore
136
+ >
137
+ <TableOfContents headings={headings} />
138
+ </aside>
139
+ )
140
+ }
141
+ </div>
142
+
143
+ <!-- Mobile menu toggle script -->
144
+ <script>
145
+ function initMobileMenu() {
146
+ const toggle = document.getElementById("mobile-menu-toggle");
147
+ const sidebar = document.getElementById("sidebar-mobile");
148
+ const overlay = document.getElementById("sidebar-overlay");
149
+ const mainContent = document.getElementById("main-content");
150
+
151
+ if (!toggle || !sidebar || !overlay) return;
152
+
153
+ function openMenu() {
154
+ sidebar!.classList.remove("-translate-x-full");
155
+ overlay!.classList.remove("hidden");
156
+ toggle!.setAttribute("aria-expanded", "true");
157
+ document.body.style.overflow = "hidden";
158
+ // Focus trap: make background content inert
159
+ mainContent?.setAttribute("inert", "");
160
+ }
161
+
162
+ function closeMenu() {
163
+ sidebar!.classList.add("-translate-x-full");
164
+ overlay!.classList.add("hidden");
165
+ toggle!.setAttribute("aria-expanded", "false");
166
+ document.body.style.overflow = "";
167
+ // Remove focus trap
168
+ mainContent?.removeAttribute("inert");
169
+ }
170
+
171
+ toggle.addEventListener("click", () => {
172
+ const isOpen = toggle.getAttribute("aria-expanded") === "true";
173
+ if (isOpen) {
174
+ closeMenu();
175
+ } else {
176
+ openMenu();
177
+ }
178
+ });
179
+
180
+ overlay.addEventListener("click", closeMenu);
181
+
182
+ // Close on Escape key
183
+ document.addEventListener("keydown", (e) => {
184
+ if (e.key === "Escape") closeMenu();
185
+ });
186
+ }
187
+
188
+ // Run on initial load and on Astro page transitions
189
+ initMobileMenu();
190
+ document.addEventListener("astro:after-swap", initMobileMenu);
191
+ </script>
192
+ <!-- Fenced code block copy button enhancer -->
193
+ <script>
194
+ import "../scripts/code-block-enhancer.ts";
195
+ </script>
196
+
197
+ <Footer />
198
+ </body>
199
+ </html>