@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.
- package/dist/__tests__/api-endpoint-fallback.test.d.ts +1 -0
- package/dist/__tests__/api-endpoint-fallback.test.js +153 -0
- package/dist/__tests__/code-tabs.test.d.ts +0 -1
- package/dist/__tests__/code-tabs.test.js +0 -1
- package/dist/__tests__/copy-button.test.d.ts +0 -1
- package/dist/__tests__/copy-button.test.js +0 -1
- package/dist/__tests__/search-palette.test.d.ts +0 -1
- package/dist/__tests__/search-palette.test.js +0 -1
- package/dist/__tests__/shiki.test.d.ts +0 -1
- package/dist/__tests__/shiki.test.js +0 -1
- package/dist/__tests__/theme-css.test.d.ts +0 -1
- package/dist/__tests__/theme-css.test.js +0 -1
- package/dist/__tests__/theme-helpers.test.d.ts +0 -1
- package/dist/__tests__/theme-helpers.test.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/islands/CodeTabs.d.ts +0 -1
- package/dist/islands/CodeTabs.js +0 -1
- package/dist/islands/CopyButton.d.ts +0 -1
- package/dist/islands/CopyButton.js +0 -1
- package/dist/islands/SearchPalette.d.ts +0 -1
- package/dist/islands/SearchPalette.js +0 -1
- package/dist/islands/SearchResults.d.ts +0 -1
- package/dist/islands/SearchResults.js +0 -1
- package/dist/islands/ThemeToggle.d.ts +0 -1
- package/dist/islands/ThemeToggle.js +0 -1
- package/dist/layouts/DocPage.test.d.ts +0 -1
- package/dist/layouts/DocPage.test.js +0 -1
- package/dist/lib/utils.d.ts +0 -1
- package/dist/lib/utils.js +0 -1
- package/dist/scripts/code-block-enhancer.d.ts +0 -1
- package/dist/scripts/code-block-enhancer.js +0 -1
- package/dist/ui/command.d.ts +0 -1
- package/dist/ui/command.js +0 -1
- package/dist/ui/dialog.d.ts +0 -1
- package/dist/ui/dialog.js +0 -1
- package/dist/utils/parse-highlight-range.d.ts +0 -1
- package/dist/utils/parse-highlight-range.js +0 -1
- package/dist/utils/parse-highlight-range.test.d.ts +0 -1
- package/dist/utils/parse-highlight-range.test.js +0 -1
- package/dist/utils/schema-renderer.d.ts +0 -1
- package/dist/utils/schema-renderer.js +0 -1
- package/dist/utils/schema-renderer.test.d.ts +0 -1
- package/dist/utils/schema-renderer.test.js +0 -1
- package/dist/utils/shiki.d.ts +0 -1
- package/dist/utils/shiki.js +0 -1
- package/dist/utils/sidebar-helpers.d.ts +0 -1
- package/dist/utils/sidebar-helpers.js +0 -1
- package/dist/utils/theme-css.d.ts +0 -1
- package/dist/utils/theme-css.js +0 -1
- package/dist/utils/theme-helpers.d.ts +0 -1
- package/dist/utils/theme-helpers.js +0 -1
- package/dist/utils/toc-helpers.d.ts +0 -1
- package/dist/utils/toc-helpers.js +0 -1
- package/package.json +7 -3
- package/src/components/ApiEndpointFallback.astro +102 -0
- package/src/components/ApiExampleRequest.astro +192 -0
- package/src/components/ApiExampleResponse.astro +145 -0
- package/src/components/ApiNavigation.astro +70 -3
- package/src/layouts/ApiReferencePage.astro +75 -38
- package/src/scripts/code-block-enhancer.ts +59 -0
- package/src/ui/command.tsx +183 -0
- package/src/ui/dialog.tsx +133 -0
- package/dist/__tests__/code-tabs.test.d.ts.map +0 -1
- package/dist/__tests__/code-tabs.test.js.map +0 -1
- package/dist/__tests__/copy-button.test.d.ts.map +0 -1
- package/dist/__tests__/copy-button.test.js.map +0 -1
- package/dist/__tests__/search-palette.test.d.ts.map +0 -1
- package/dist/__tests__/search-palette.test.js.map +0 -1
- package/dist/__tests__/shiki.test.d.ts.map +0 -1
- package/dist/__tests__/shiki.test.js.map +0 -1
- package/dist/__tests__/theme-css.test.d.ts.map +0 -1
- package/dist/__tests__/theme-css.test.js.map +0 -1
- package/dist/__tests__/theme-helpers.test.d.ts.map +0 -1
- package/dist/__tests__/theme-helpers.test.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/islands/CodeTabs.d.ts.map +0 -1
- package/dist/islands/CodeTabs.js.map +0 -1
- package/dist/islands/CopyButton.d.ts.map +0 -1
- package/dist/islands/CopyButton.js.map +0 -1
- package/dist/islands/SearchPalette.d.ts.map +0 -1
- package/dist/islands/SearchPalette.js.map +0 -1
- package/dist/islands/SearchResults.d.ts.map +0 -1
- package/dist/islands/SearchResults.js.map +0 -1
- package/dist/islands/ThemeToggle.d.ts.map +0 -1
- package/dist/islands/ThemeToggle.js.map +0 -1
- package/dist/layouts/DocPage.test.d.ts.map +0 -1
- package/dist/layouts/DocPage.test.js.map +0 -1
- package/dist/lib/utils.d.ts.map +0 -1
- package/dist/lib/utils.js.map +0 -1
- package/dist/scripts/code-block-enhancer.d.ts.map +0 -1
- package/dist/scripts/code-block-enhancer.js.map +0 -1
- package/dist/ui/command.d.ts.map +0 -1
- package/dist/ui/command.js.map +0 -1
- package/dist/ui/dialog.d.ts.map +0 -1
- package/dist/ui/dialog.js.map +0 -1
- package/dist/utils/parse-highlight-range.d.ts.map +0 -1
- package/dist/utils/parse-highlight-range.js.map +0 -1
- package/dist/utils/parse-highlight-range.test.d.ts.map +0 -1
- package/dist/utils/parse-highlight-range.test.js.map +0 -1
- package/dist/utils/schema-renderer.d.ts.map +0 -1
- package/dist/utils/schema-renderer.js.map +0 -1
- package/dist/utils/schema-renderer.test.d.ts.map +0 -1
- package/dist/utils/schema-renderer.test.js.map +0 -1
- package/dist/utils/shiki.d.ts.map +0 -1
- package/dist/utils/shiki.js.map +0 -1
- package/dist/utils/sidebar-helpers.d.ts.map +0 -1
- package/dist/utils/sidebar-helpers.js.map +0 -1
- package/dist/utils/theme-css.d.ts.map +0 -1
- package/dist/utils/theme-css.js.map +0 -1
- package/dist/utils/theme-helpers.d.ts.map +0 -1
- package/dist/utils/theme-helpers.js.map +0 -1
- package/dist/utils/toc-helpers.d.ts.map +0 -1
- 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
|
|
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 {
|
|
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
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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);
|