@specglass/theme-default 0.0.2 → 0.0.4

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 (115) hide show
  1. package/dist/__tests__/api-endpoint-fallback.test.d.ts +1 -0
  2. package/dist/__tests__/api-endpoint-fallback.test.js +153 -0
  3. package/dist/__tests__/code-tabs.test.d.ts +0 -1
  4. package/dist/__tests__/code-tabs.test.js +0 -1
  5. package/dist/__tests__/copy-button.test.d.ts +0 -1
  6. package/dist/__tests__/copy-button.test.js +0 -1
  7. package/dist/__tests__/search-palette.test.d.ts +0 -1
  8. package/dist/__tests__/search-palette.test.js +0 -1
  9. package/dist/__tests__/shiki.test.d.ts +0 -1
  10. package/dist/__tests__/shiki.test.js +0 -1
  11. package/dist/__tests__/theme-css.test.d.ts +0 -1
  12. package/dist/__tests__/theme-css.test.js +0 -1
  13. package/dist/__tests__/theme-helpers.test.d.ts +0 -1
  14. package/dist/__tests__/theme-helpers.test.js +0 -1
  15. package/dist/index.d.ts +0 -1
  16. package/dist/index.js +0 -1
  17. package/dist/islands/CodeTabs.d.ts +0 -1
  18. package/dist/islands/CodeTabs.js +0 -1
  19. package/dist/islands/CopyButton.d.ts +0 -1
  20. package/dist/islands/CopyButton.js +0 -1
  21. package/dist/islands/SearchPalette.d.ts +0 -1
  22. package/dist/islands/SearchPalette.js +0 -1
  23. package/dist/islands/SearchResults.d.ts +0 -1
  24. package/dist/islands/SearchResults.js +0 -1
  25. package/dist/islands/ThemeToggle.d.ts +0 -1
  26. package/dist/islands/ThemeToggle.js +0 -1
  27. package/dist/layouts/DocPage.test.d.ts +0 -1
  28. package/dist/layouts/DocPage.test.js +0 -1
  29. package/dist/lib/utils.d.ts +0 -1
  30. package/dist/lib/utils.js +0 -1
  31. package/dist/scripts/code-block-enhancer.d.ts +0 -1
  32. package/dist/scripts/code-block-enhancer.js +0 -1
  33. package/dist/ui/command.d.ts +0 -1
  34. package/dist/ui/command.js +0 -1
  35. package/dist/ui/dialog.d.ts +0 -1
  36. package/dist/ui/dialog.js +0 -1
  37. package/dist/utils/parse-highlight-range.d.ts +0 -1
  38. package/dist/utils/parse-highlight-range.js +0 -1
  39. package/dist/utils/parse-highlight-range.test.d.ts +0 -1
  40. package/dist/utils/parse-highlight-range.test.js +0 -1
  41. package/dist/utils/schema-renderer.d.ts +0 -1
  42. package/dist/utils/schema-renderer.js +0 -1
  43. package/dist/utils/schema-renderer.test.d.ts +0 -1
  44. package/dist/utils/schema-renderer.test.js +0 -1
  45. package/dist/utils/shiki.d.ts +0 -1
  46. package/dist/utils/shiki.js +0 -1
  47. package/dist/utils/sidebar-helpers.d.ts +0 -1
  48. package/dist/utils/sidebar-helpers.js +0 -1
  49. package/dist/utils/theme-css.d.ts +0 -1
  50. package/dist/utils/theme-css.js +0 -1
  51. package/dist/utils/theme-helpers.d.ts +0 -1
  52. package/dist/utils/theme-helpers.js +0 -1
  53. package/dist/utils/toc-helpers.d.ts +0 -1
  54. package/dist/utils/toc-helpers.js +0 -1
  55. package/package.json +7 -3
  56. package/src/components/ApiEndpointFallback.astro +102 -0
  57. package/src/components/ApiExampleRequest.astro +192 -0
  58. package/src/components/ApiExampleResponse.astro +145 -0
  59. package/src/components/ApiNavigation.astro +70 -3
  60. package/src/layouts/ApiReferencePage.astro +75 -38
  61. package/src/scripts/code-block-enhancer.ts +59 -0
  62. package/src/ui/command.tsx +183 -0
  63. package/src/ui/dialog.tsx +133 -0
  64. package/dist/__tests__/code-tabs.test.d.ts.map +0 -1
  65. package/dist/__tests__/code-tabs.test.js.map +0 -1
  66. package/dist/__tests__/copy-button.test.d.ts.map +0 -1
  67. package/dist/__tests__/copy-button.test.js.map +0 -1
  68. package/dist/__tests__/search-palette.test.d.ts.map +0 -1
  69. package/dist/__tests__/search-palette.test.js.map +0 -1
  70. package/dist/__tests__/shiki.test.d.ts.map +0 -1
  71. package/dist/__tests__/shiki.test.js.map +0 -1
  72. package/dist/__tests__/theme-css.test.d.ts.map +0 -1
  73. package/dist/__tests__/theme-css.test.js.map +0 -1
  74. package/dist/__tests__/theme-helpers.test.d.ts.map +0 -1
  75. package/dist/__tests__/theme-helpers.test.js.map +0 -1
  76. package/dist/index.d.ts.map +0 -1
  77. package/dist/index.js.map +0 -1
  78. package/dist/islands/CodeTabs.d.ts.map +0 -1
  79. package/dist/islands/CodeTabs.js.map +0 -1
  80. package/dist/islands/CopyButton.d.ts.map +0 -1
  81. package/dist/islands/CopyButton.js.map +0 -1
  82. package/dist/islands/SearchPalette.d.ts.map +0 -1
  83. package/dist/islands/SearchPalette.js.map +0 -1
  84. package/dist/islands/SearchResults.d.ts.map +0 -1
  85. package/dist/islands/SearchResults.js.map +0 -1
  86. package/dist/islands/ThemeToggle.d.ts.map +0 -1
  87. package/dist/islands/ThemeToggle.js.map +0 -1
  88. package/dist/layouts/DocPage.test.d.ts.map +0 -1
  89. package/dist/layouts/DocPage.test.js.map +0 -1
  90. package/dist/lib/utils.d.ts.map +0 -1
  91. package/dist/lib/utils.js.map +0 -1
  92. package/dist/scripts/code-block-enhancer.d.ts.map +0 -1
  93. package/dist/scripts/code-block-enhancer.js.map +0 -1
  94. package/dist/ui/command.d.ts.map +0 -1
  95. package/dist/ui/command.js.map +0 -1
  96. package/dist/ui/dialog.d.ts.map +0 -1
  97. package/dist/ui/dialog.js.map +0 -1
  98. package/dist/utils/parse-highlight-range.d.ts.map +0 -1
  99. package/dist/utils/parse-highlight-range.js.map +0 -1
  100. package/dist/utils/parse-highlight-range.test.d.ts.map +0 -1
  101. package/dist/utils/parse-highlight-range.test.js.map +0 -1
  102. package/dist/utils/schema-renderer.d.ts.map +0 -1
  103. package/dist/utils/schema-renderer.js.map +0 -1
  104. package/dist/utils/schema-renderer.test.d.ts.map +0 -1
  105. package/dist/utils/schema-renderer.test.js.map +0 -1
  106. package/dist/utils/shiki.d.ts.map +0 -1
  107. package/dist/utils/shiki.js.map +0 -1
  108. package/dist/utils/sidebar-helpers.d.ts.map +0 -1
  109. package/dist/utils/sidebar-helpers.js.map +0 -1
  110. package/dist/utils/theme-css.d.ts.map +0 -1
  111. package/dist/utils/theme-css.js.map +0 -1
  112. package/dist/utils/theme-helpers.d.ts.map +0 -1
  113. package/dist/utils/theme-helpers.js.map +0 -1
  114. package/dist/utils/toc-helpers.d.ts.map +0 -1
  115. package/dist/utils/toc-helpers.js.map +0 -1
@@ -0,0 +1,192 @@
1
+ ---
2
+ /**
3
+ * ApiExampleRequest.astro — Renders multi-language request code examples.
4
+ *
5
+ * Generates cURL, Python, and Node.js request examples from endpoint data.
6
+ * Uses a tab interface with localStorage persistence for language preference.
7
+ * Each tab includes a copy button for the code snippet.
8
+ * Code blocks use Shiki dual-theme highlighting (AC7).
9
+ */
10
+ import type { ApiEndpoint } from "@specglass/core";
11
+ import { generateCurlExample, generatePythonExample, generateNodeExample } from "@specglass/core";
12
+ import { CopyButton } from "../islands/CopyButton";
13
+ import { highlight } from "../utils/shiki";
14
+
15
+ export interface Props {
16
+ endpoint: ApiEndpoint;
17
+ baseUrl?: string;
18
+ }
19
+
20
+ const { endpoint, baseUrl = "https://api.example.com" } = Astro.props;
21
+
22
+ const options = { baseUrl };
23
+
24
+ const rawLanguages = [
25
+ { id: "curl", label: "cURL", code: generateCurlExample(endpoint, options), lang: "bash" },
26
+ { id: "python", label: "Python", code: generatePythonExample(endpoint, options), lang: "python" },
27
+ {
28
+ id: "node",
29
+ label: "Node.js",
30
+ code: generateNodeExample(endpoint, options),
31
+ lang: "javascript",
32
+ },
33
+ ];
34
+
35
+ // Pre-render Shiki highlighting at build time
36
+ const languages = await Promise.all(
37
+ rawLanguages.map(async (item) => ({
38
+ ...item,
39
+ highlighted: await highlight(item.code, item.lang),
40
+ })),
41
+ );
42
+
43
+ const tabGroupId = `api-req-${endpoint.method}-${endpoint.path.replace(/[{}/]/g, "-")}`;
44
+ ---
45
+
46
+ <div class="mt-6">
47
+ <h2 class="text-lg font-semibold text-text mb-4">Request Example</h2>
48
+
49
+ <div
50
+ class="api-code-tabs rounded-lg border border-border overflow-hidden"
51
+ data-tab-group={tabGroupId}
52
+ data-sync-key="api-lang"
53
+ >
54
+ {/* Tab buttons */}
55
+ <div
56
+ class="flex border-b border-border bg-surface-raised"
57
+ role="tablist"
58
+ aria-label="Request language"
59
+ >
60
+ {
61
+ languages.map((lang, i) => (
62
+ <button
63
+ type="button"
64
+ role="tab"
65
+ id={`${tabGroupId}-tab-${lang.id}`}
66
+ aria-controls={`${tabGroupId}-panel-${lang.id}`}
67
+ aria-selected={i === 0 ? "true" : "false"}
68
+ tabindex={i === 0 ? 0 : -1}
69
+ data-lang={lang.id}
70
+ class:list={[
71
+ "px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
72
+ i === 0
73
+ ? "text-text border-b-2 border-primary bg-surface"
74
+ : "text-text-muted hover:text-text",
75
+ ]}
76
+ >
77
+ {lang.label}
78
+ </button>
79
+ ))
80
+ }
81
+ </div>
82
+
83
+ {/* Tab panels */}
84
+ {
85
+ languages.map((lang, i) => (
86
+ <div
87
+ role="tabpanel"
88
+ id={`${tabGroupId}-panel-${lang.id}`}
89
+ aria-labelledby={`${tabGroupId}-tab-${lang.id}`}
90
+ data-lang={lang.id}
91
+ class={i === 0 ? "" : "hidden"}
92
+ >
93
+ <div class="relative group">
94
+ <div
95
+ class="[&_pre]:overflow-x-auto [&_pre]:p-4 [&_pre]:text-sm [&_pre]:leading-relaxed [&_pre]:m-0"
96
+ set:html={lang.highlighted}
97
+ />
98
+ <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
99
+ <CopyButton client:idle code={lang.code} />
100
+ </div>
101
+ </div>
102
+ </div>
103
+ ))
104
+ }
105
+ </div>
106
+ </div>
107
+
108
+ <script>
109
+ function initApiCodeTabs() {
110
+ const SYNC_KEY = "api-lang";
111
+ const STORAGE_KEY = `specglass-preferred-${SYNC_KEY}`;
112
+
113
+ document.querySelectorAll<HTMLElement>(".api-code-tabs").forEach((container) => {
114
+ const tabs = container.querySelectorAll<HTMLButtonElement>('[role="tab"]');
115
+ const panels = container.querySelectorAll<HTMLElement>('[role="tabpanel"]');
116
+
117
+ // Restore saved preference
118
+ try {
119
+ const saved = localStorage.getItem(STORAGE_KEY);
120
+ if (saved) {
121
+ activateTab(saved);
122
+ }
123
+ } catch {
124
+ // noop — localStorage unavailable (e.g., private browsing)
125
+ }
126
+
127
+ function activateTab(langId: string) {
128
+ let found = false;
129
+ tabs.forEach((tab) => {
130
+ const isActive = tab.dataset.lang === langId;
131
+ tab.setAttribute("aria-selected", isActive ? "true" : "false");
132
+ tab.tabIndex = isActive ? 0 : -1;
133
+ if (isActive) {
134
+ tab.classList.add("text-text", "border-b-2", "border-primary", "bg-surface");
135
+ tab.classList.remove("text-text-muted");
136
+ found = true;
137
+ } else {
138
+ tab.classList.remove("text-text", "border-b-2", "border-primary", "bg-surface");
139
+ tab.classList.add("text-text-muted");
140
+ }
141
+ });
142
+ panels.forEach((panel) => {
143
+ panel.classList.toggle("hidden", panel.dataset.lang !== langId);
144
+ });
145
+ return found;
146
+ }
147
+
148
+ tabs.forEach((tab) => {
149
+ tab.addEventListener("click", () => {
150
+ const lang = tab.dataset.lang;
151
+ if (!lang) return;
152
+ activateTab(lang);
153
+ // Persist and sync
154
+ try {
155
+ localStorage.setItem(STORAGE_KEY, lang);
156
+ } catch {
157
+ /* noop */
158
+ }
159
+ // Broadcast sync event for other tab groups on the page
160
+ document.dispatchEvent(
161
+ new CustomEvent("specglass-lang-sync", { detail: { syncKey: SYNC_KEY, lang } }),
162
+ );
163
+ });
164
+
165
+ // Keyboard navigation
166
+ tab.addEventListener("keydown", (e) => {
167
+ const tabList = Array.from(tabs);
168
+ const index = tabList.indexOf(tab);
169
+ let nextIndex = index;
170
+ if (e.key === "ArrowRight") nextIndex = (index + 1) % tabList.length;
171
+ else if (e.key === "ArrowLeft") nextIndex = (index - 1 + tabList.length) % tabList.length;
172
+ else if (e.key === "Home") nextIndex = 0;
173
+ else if (e.key === "End") nextIndex = tabList.length - 1;
174
+ else return;
175
+ e.preventDefault();
176
+ tabList[nextIndex].focus();
177
+ tabList[nextIndex].click();
178
+ });
179
+ });
180
+
181
+ // Listen for sync events from other tab groups
182
+ document.addEventListener("specglass-lang-sync", ((e: CustomEvent) => {
183
+ if (e.detail?.syncKey === SYNC_KEY) {
184
+ activateTab(e.detail.lang);
185
+ }
186
+ }) as EventListener);
187
+ });
188
+ }
189
+
190
+ initApiCodeTabs();
191
+ document.addEventListener("astro:after-swap", initApiCodeTabs);
192
+ </script>
@@ -0,0 +1,145 @@
1
+ ---
2
+ /**
3
+ * ApiExampleResponse.astro — Renders example response bodies for each status code.
4
+ *
5
+ * For each response, generates a JSON example from the response schema or
6
+ * uses spec-provided examples. 2xx responses are expanded by default;
7
+ * others are collapsed.
8
+ */
9
+ import type { ApiResponse as ApiResponseType } from "@specglass/core";
10
+ import { extractMediaTypeExample } from "@specglass/core";
11
+ import { CopyButton } from "../islands/CopyButton";
12
+ import { highlight } from "../utils/shiki";
13
+
14
+ export interface Props {
15
+ responses: ApiResponseType[];
16
+ }
17
+
18
+ const { responses } = Astro.props;
19
+
20
+ /** Status code badge colors */
21
+ function getStatusColor(code: string): string {
22
+ if (code.startsWith("2"))
23
+ return "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-emerald-500/30";
24
+ if (code.startsWith("3"))
25
+ return "bg-blue-500/15 text-blue-700 dark:text-blue-400 border-blue-500/30";
26
+ if (code.startsWith("4"))
27
+ return "bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/30";
28
+ if (code.startsWith("5")) return "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30";
29
+ return "bg-gray-500/15 text-gray-700 dark:text-gray-400 border-gray-500/30";
30
+ }
31
+
32
+ const responseExamplesRaw = responses.map((resp) => {
33
+ let exampleJson: string | null = null;
34
+ let contentType: string | null = null;
35
+
36
+ if (resp.content) {
37
+ const contentTypes = Object.keys(resp.content);
38
+ const jsonType = contentTypes.find((ct) => ct.includes("json")) ?? contentTypes[0];
39
+ if (jsonType) {
40
+ contentType = jsonType;
41
+ const example = extractMediaTypeExample(resp.content[jsonType]);
42
+ exampleJson = JSON.stringify(example, null, 2);
43
+ }
44
+ }
45
+
46
+ const is2xx = resp.statusCode.startsWith("2");
47
+
48
+ return {
49
+ ...resp,
50
+ exampleJson,
51
+ contentType,
52
+ is2xx,
53
+ statusColor: getStatusColor(resp.statusCode),
54
+ };
55
+ });
56
+
57
+ // Pre-render Shiki highlighting at build time
58
+ const responseExamples = await Promise.all(
59
+ responseExamplesRaw.map(async (resp) => ({
60
+ ...resp,
61
+ highlighted: resp.exampleJson ? await highlight(resp.exampleJson, "json") : null,
62
+ })),
63
+ );
64
+ ---
65
+
66
+ <div class="mt-6">
67
+ <h2 class="text-lg font-semibold text-text mb-4">Response Examples</h2>
68
+
69
+ <div class="space-y-3">
70
+ {
71
+ responseExamples.map((resp) => (
72
+ <div class="rounded-lg border border-border overflow-hidden">
73
+ {/* Response header */}
74
+ {resp.is2xx ? (
75
+ <>
76
+ <div class="flex items-center gap-3 px-4 py-3 bg-surface-raised border-b border-border">
77
+ <span
78
+ class:list={[
79
+ "inline-flex items-center px-2 py-0.5 rounded text-xs font-bold border",
80
+ resp.statusColor,
81
+ ]}
82
+ >
83
+ {resp.statusCode}
84
+ </span>
85
+ <span class="text-sm text-text-muted">{resp.description}</span>
86
+ {resp.contentType && (
87
+ <span class="ml-auto text-xs text-text-muted/60 font-mono">
88
+ {resp.contentType}
89
+ </span>
90
+ )}
91
+ </div>
92
+ {resp.highlighted ? (
93
+ <div class="relative group">
94
+ <div
95
+ class="[&_pre]:overflow-x-auto [&_pre]:p-4 [&_pre]:text-sm [&_pre]:leading-relaxed [&_pre]:m-0"
96
+ set:html={resp.highlighted}
97
+ />
98
+ <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
99
+ <CopyButton client:idle code={resp.exampleJson!} />
100
+ </div>
101
+ </div>
102
+ ) : (
103
+ <div class="px-4 py-3 text-sm text-text-muted italic">No response body</div>
104
+ )}
105
+ </>
106
+ ) : (
107
+ <details>
108
+ <summary class="flex items-center gap-3 px-4 py-3 bg-surface-raised cursor-pointer hover:bg-surface-raised/80 transition-colors">
109
+ <span
110
+ class:list={[
111
+ "inline-flex items-center px-2 py-0.5 rounded text-xs font-bold border",
112
+ resp.statusColor,
113
+ ]}
114
+ >
115
+ {resp.statusCode}
116
+ </span>
117
+ <span class="text-sm text-text-muted">{resp.description}</span>
118
+ {resp.contentType && (
119
+ <span class="ml-auto text-xs text-text-muted/60 font-mono">
120
+ {resp.contentType}
121
+ </span>
122
+ )}
123
+ </summary>
124
+ {resp.highlighted ? (
125
+ <div class="relative group border-t border-border">
126
+ <div
127
+ class="[&_pre]:overflow-x-auto [&_pre]:p-4 [&_pre]:text-sm [&_pre]:leading-relaxed [&_pre]:m-0"
128
+ set:html={resp.highlighted}
129
+ />
130
+ <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
131
+ <CopyButton client:idle code={resp.exampleJson!} />
132
+ </div>
133
+ </div>
134
+ ) : (
135
+ <div class="px-4 py-3 text-sm text-text-muted italic border-t border-border">
136
+ No response body
137
+ </div>
138
+ )}
139
+ </details>
140
+ )}
141
+ </div>
142
+ ))
143
+ }
144
+ </div>
145
+ </div>
@@ -2,17 +2,19 @@
2
2
  /**
3
3
  * ApiNavigation.astro — Sidebar navigation for API reference endpoints.
4
4
  * Groups endpoints by tag and highlights the current endpoint.
5
+ * Also shows errored endpoints with a warning indicator (FR40).
5
6
  */
6
- import type { ApiEndpoint } from "@specglass/core";
7
- import { buildEndpointSlug, buildEndpointId } from "@specglass/core";
7
+ import type { ApiEndpoint, ApiEndpointError } from "@specglass/core";
8
+ import { buildEndpointSlug, buildErrorEndpointSlug, buildEndpointId } from "@specglass/core";
8
9
 
9
10
  export interface Props {
10
11
  endpoints: ApiEndpoint[];
12
+ errors?: ApiEndpointError[];
11
13
  currentEndpointId: string;
12
14
  basePath: string;
13
15
  }
14
16
 
15
- const { endpoints, currentEndpointId, basePath } = Astro.props;
17
+ const { endpoints, errors = [], currentEndpointId, basePath } = Astro.props;
16
18
 
17
19
  // Group endpoints by tag
18
20
  const grouped = new Map<string, ApiEndpoint[]>();
@@ -88,6 +90,71 @@ const methodBadgeColors: Record<string, string> = {
88
90
  </div>
89
91
  ))
90
92
  }
93
+ {/* Errored endpoints section (FR40 fallback) */}
94
+ {
95
+ errors.length > 0 && (
96
+ <div class="mt-4 pt-4 border-t border-border">
97
+ <button
98
+ class="flex w-full items-center gap-2 text-xs font-semibold text-amber-600 dark:text-amber-400 uppercase tracking-wider px-3 py-2 hover:text-amber-700 dark:hover:text-amber-300 transition-colors"
99
+ aria-expanded="true"
100
+ data-api-nav-group="unsupported"
101
+ >
102
+ <svg
103
+ class="h-3 w-3 shrink-0 transition-transform rotate-90"
104
+ viewBox="0 0 12 12"
105
+ fill="currentColor"
106
+ aria-hidden="true"
107
+ >
108
+ <path d="M4.5 2l4 4-4 4" />
109
+ </svg>
110
+ <span aria-hidden="true" class="mr-0.5">
111
+ ⚠️
112
+ </span>{" "}
113
+ Unsupported
114
+ </button>
115
+ <ul class="ml-3 space-y-0.5" data-api-nav-items="unsupported">
116
+ {errors.map((err) => {
117
+ const errId = `${err.method}-${err.path}`;
118
+ const isActive = errId === currentEndpointId;
119
+ const slug = buildErrorEndpointSlug(err);
120
+ const badgeColor = methodBadgeColors[err.method.toLowerCase()] ?? methodBadgeColors.get;
121
+
122
+ return (
123
+ <li>
124
+ <a
125
+ href={`${basePath}/${slug}`}
126
+ class:list={[
127
+ "flex items-center gap-2 px-3 py-1.5 rounded-md text-xs transition-colors group",
128
+ isActive
129
+ ? "bg-amber-500/10 text-amber-700 dark:text-amber-400 font-medium"
130
+ : "text-text-muted/60 hover:bg-surface-raised hover:text-text-muted",
131
+ ]}
132
+ aria-current={isActive ? "page" : undefined}
133
+ title={`Unsupported: ${err.reason}`}
134
+ >
135
+ <span
136
+ class:list={[
137
+ "inline-flex w-12 justify-center shrink-0 px-1 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider opacity-60",
138
+ badgeColor,
139
+ ]}
140
+ >
141
+ {err.method.toUpperCase()}
142
+ </span>
143
+ <span class="font-mono truncate opacity-60">{err.path}</span>
144
+ <span
145
+ class="text-amber-500 dark:text-amber-400 text-[10px] ml-auto shrink-0"
146
+ aria-hidden="true"
147
+ >
148
+ ⚠️
149
+ </span>
150
+ </a>
151
+ </li>
152
+ );
153
+ })}
154
+ </ul>
155
+ </div>
156
+ )
157
+ }
91
158
  </nav>
92
159
 
93
160
  <script>
@@ -3,6 +3,7 @@
3
3
  * ApiReferencePage.astro — Layout for API reference endpoint pages.
4
4
  * Mirrors DocPage structure (Header, Sidebar, Footer, mobile menu) but
5
5
  * replaces the Table of Contents with API endpoint navigation.
6
+ * Supports fallback rendering for errored endpoints (FR40).
6
7
  */
7
8
  import Header from "../components/Header.astro";
8
9
  import Footer from "../components/Footer.astro";
@@ -11,26 +12,34 @@ import ApiEndpoint from "../components/ApiEndpoint.astro";
11
12
  import ApiParameters from "../components/ApiParameters.astro";
12
13
  import ApiResponse from "../components/ApiResponse.astro";
13
14
  import ApiAuth from "../components/ApiAuth.astro";
15
+ import ApiExampleRequest from "../components/ApiExampleRequest.astro";
16
+ import ApiExampleResponse from "../components/ApiExampleResponse.astro";
17
+ import ApiEndpointFallback from "../components/ApiEndpointFallback.astro";
14
18
  import { ThemeToggle } from "../islands/ThemeToggle";
15
19
  import { SearchPalette } from "../islands/SearchPalette";
16
20
  import "../styles/global.css";
17
21
  import type {
18
22
  NavigationTree,
19
23
  ApiEndpoint as ApiEndpointType,
24
+ ApiEndpointError,
20
25
  ApiSecurityRequirement,
21
26
  SpecglassConfig,
22
27
  } from "@specglass/core";
23
28
  import { generateThemeCSS } from "../utils/theme-css";
24
29
 
25
30
  export interface Props {
26
- /** The current endpoint to display */
27
- endpoint: ApiEndpointType;
31
+ /** The current endpoint to display (undefined for errored endpoints) */
32
+ endpoint?: ApiEndpointType;
33
+ /** Error data for endpoints with unsupported patterns (FR40) */
34
+ endpointError?: ApiEndpointError;
28
35
  /** Documentation navigation tree */
29
36
  navigation: NavigationTree;
30
37
  /** Site configuration */
31
38
  config: SpecglassConfig;
32
- /** All endpoints for the sidebar navigation */
39
+ /** All valid endpoints for the sidebar navigation */
33
40
  allEndpoints: ApiEndpointType[];
41
+ /** All errored endpoints for the sidebar navigation */
42
+ allErrors?: ApiEndpointError[];
34
43
  /** Spec-level information */
35
44
  specInfo: {
36
45
  title: string;
@@ -40,15 +49,28 @@ export interface Props {
40
49
  securitySchemes?: Record<string, ApiSecurityRequirement>;
41
50
  }
42
51
 
43
- const { endpoint, navigation, config, allEndpoints, specInfo, securitySchemes } = Astro.props;
52
+ const {
53
+ endpoint,
54
+ endpointError,
55
+ navigation,
56
+ config,
57
+ allEndpoints,
58
+ allErrors = [],
59
+ specInfo,
60
+ securitySchemes,
61
+ } = Astro.props;
62
+ const isErrorPage = !!endpointError;
44
63
  const themeCSS = generateThemeCSS(config);
45
64
 
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}`;
65
+ const displayMethod = isErrorPage ? endpointError!.method : endpoint!.method;
66
+ const displayPath = isErrorPage ? endpointError!.path : endpoint!.path;
67
+ const pageTitle = `${displayMethod.toUpperCase()} ${displayPath} — ${specInfo.title} API`;
68
+ const pageDescription = isErrorPage
69
+ ? `Manual documentation required for ${displayMethod.toUpperCase()} ${displayPath}`
70
+ : endpoint!.summary ||
71
+ endpoint!.description ||
72
+ `API reference for ${displayMethod.toUpperCase()} ${displayPath}`;
73
+ const currentEndpointId = `${displayMethod}-${displayPath}`;
52
74
  ---
53
75
 
54
76
  <html lang="en" class="dark">
@@ -73,7 +95,9 @@ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
73
95
  var stored = null;
74
96
  try {
75
97
  stored = localStorage.getItem("specglass-theme");
76
- } catch (e) {}
98
+ } catch (e) {
99
+ /* noop */
100
+ }
77
101
  var theme = stored === "dark" || stored === "light" ? stored : "dark";
78
102
  if (theme === "dark") {
79
103
  document.documentElement.classList.add("dark");
@@ -119,6 +143,7 @@ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
119
143
 
120
144
  <ApiNavigation
121
145
  endpoints={allEndpoints}
146
+ errors={allErrors}
122
147
  currentEndpointId={currentEndpointId}
123
148
  basePath="/api-reference"
124
149
  />
@@ -140,6 +165,7 @@ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
140
165
  </div>
141
166
  <ApiNavigation
142
167
  endpoints={allEndpoints}
168
+ errors={allErrors}
143
169
  currentEndpointId={currentEndpointId}
144
170
  basePath="/api-reference"
145
171
  />
@@ -148,36 +174,47 @@ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
148
174
  <!-- Main content area -->
149
175
  <main id="main-content" class="flex-1 min-w-0 md:ml-(--width-sidebar)" data-pagefind-body>
150
176
  <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
177
  {
156
- endpoint.security && endpoint.security.length > 0 && (
157
- <ApiAuth security={endpoint.security} securitySchemes={securitySchemes} />
158
- )
159
- }
178
+ isErrorPage ? (
179
+ /* Fallback rendering for errored endpoints (FR40) */
180
+ <ApiEndpointFallback error={endpointError!} />
181
+ ) : (
182
+ <>
183
+ {/* Endpoint header */}
184
+ <ApiEndpoint endpoint={endpoint!} />
160
185
 
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
- }
186
+ {/* Authentication */}
187
+ {endpoint!.security && endpoint!.security.length > 0 && (
188
+ <ApiAuth security={endpoint!.security} securitySchemes={securitySchemes} />
189
+ )}
173
190
 
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>
191
+ {/* Parameters & Request Body */}
192
+ {(endpoint!.parameters.length > 0 || endpoint!.requestBody) && (
193
+ <div>
194
+ <h2 class="text-lg font-semibold text-text mb-4 mt-0">Parameters</h2>
195
+ <ApiParameters
196
+ parameters={endpoint!.parameters}
197
+ requestBody={endpoint!.requestBody}
198
+ />
199
+ </div>
200
+ )}
201
+
202
+ {/* Responses */}
203
+ {endpoint!.responses.length > 0 && (
204
+ <div>
205
+ <h2 class="text-lg font-semibold text-text mb-4">Responses</h2>
206
+ <ApiResponse responses={endpoint!.responses} />
207
+ </div>
208
+ )}
209
+
210
+ {/* Request Example (code tabs: cURL, Python, Node.js) */}
211
+ <ApiExampleRequest endpoint={endpoint!} />
212
+
213
+ {/* Response Examples (per status code) */}
214
+ {endpoint!.responses.length > 0 && (
215
+ <ApiExampleResponse responses={endpoint!.responses} />
216
+ )}
217
+ </>
181
218
  )
182
219
  }
183
220
  </article>
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Client-side enhancer for fenced code blocks.
3
+ *
4
+ * The rehype plugin wraps fenced code <pre> elements in:
5
+ * <div class="code-block code-block--fenced" data-code="...">
6
+ * <div class="code-block-body">
7
+ * <pre>...</pre>
8
+ * </div>
9
+ * </div>
10
+ *
11
+ * This script adds a lightweight copy button to each fenced code block
12
+ * WITHOUT requiring React hydration — keeping fenced blocks zero-JS
13
+ * until the user actually clicks copy.
14
+ */
15
+
16
+ function initCopyButtons() {
17
+ document
18
+ .querySelectorAll<HTMLDivElement>(".code-block--fenced")
19
+ .forEach((block) => {
20
+ // Skip if already enhanced
21
+ if (block.querySelector(".sg-copy-btn-native")) return;
22
+
23
+ const code = block.getAttribute("data-code") ?? "";
24
+
25
+ const btn = document.createElement("button");
26
+ btn.type = "button";
27
+ btn.className = "sg-copy-btn-native sg-copy-btn";
28
+ btn.setAttribute("aria-label", "Copy code");
29
+ btn.title = "Copy code";
30
+ btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
31
+
32
+ btn.addEventListener("click", () => {
33
+ navigator.clipboard.writeText(code).then(() => {
34
+ btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:#22c55e"><polyline points="20 6 9 17 4 12"/></svg>`;
35
+ btn.setAttribute("aria-label", "Copied!");
36
+ btn.title = "Copied!";
37
+ setTimeout(() => {
38
+ btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
39
+ btn.setAttribute("aria-label", "Copy code");
40
+ btn.title = "Copy code";
41
+ }, 2000);
42
+ }).catch(() => {
43
+ console.warn("[CopyButton] Clipboard API unavailable");
44
+ });
45
+ });
46
+
47
+ // Insert into the code-block-body
48
+ const body = block.querySelector<HTMLDivElement>(".code-block-body");
49
+ if (body) {
50
+ body.appendChild(btn);
51
+ }
52
+ });
53
+ }
54
+
55
+ // Run on initial load
56
+ document.addEventListener("DOMContentLoaded", initCopyButtons);
57
+
58
+ // Re-run after Astro ViewTransitions page swaps
59
+ document.addEventListener("astro:after-swap", initCopyButtons);