@specglass/theme-default 0.0.3 → 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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Tests for error endpoint slug generation, data contract, and
3
+ * the shared buildErrorEndpointSlug utility from @specglass/core.
4
+ */
5
+ import { describe, it, expect } from "vitest";
6
+ import { buildEndpointSlug, buildErrorEndpointSlug, buildEndpointId } from "@specglass/core";
7
+ describe("buildErrorEndpointSlug", () => {
8
+ it("generates correct slug for a simple errored endpoint", () => {
9
+ const error = {
10
+ path: "/pets",
11
+ method: "get",
12
+ reason: "Complex polymorphism not supported",
13
+ rawSpec: { get: { summary: "List pets" } },
14
+ };
15
+ expect(buildErrorEndpointSlug(error)).toBe("_unsupported/get-pets");
16
+ });
17
+ it("generates correct slug for nested path with parameters", () => {
18
+ const error = {
19
+ path: "/users/{userId}/orders/{orderId}",
20
+ method: "patch",
21
+ reason: "Unsupported allOf/oneOf",
22
+ rawSpec: {},
23
+ };
24
+ expect(buildErrorEndpointSlug(error)).toBe("_unsupported/patch-users-userId-orders-orderId");
25
+ });
26
+ it("generates correct slug for root path", () => {
27
+ const error = {
28
+ path: "/",
29
+ method: "get",
30
+ reason: "Unsupported extension",
31
+ rawSpec: {},
32
+ };
33
+ expect(buildErrorEndpointSlug(error)).toBe("_unsupported/get-");
34
+ });
35
+ it("uses _unsupported prefix to avoid collision with valid endpoint slugs", () => {
36
+ // An errored endpoint at /pets GET should NOT collide with a valid
37
+ // endpoint at /pets GET (which gets slug "default/get-pets")
38
+ const error = {
39
+ path: "/pets",
40
+ method: "get",
41
+ reason: "test",
42
+ rawSpec: {},
43
+ };
44
+ const validEndpoint = {
45
+ path: "/pets",
46
+ method: "get",
47
+ tags: [],
48
+ summary: "",
49
+ description: "",
50
+ operationId: "",
51
+ deprecated: false,
52
+ parameters: [],
53
+ responses: [],
54
+ security: [],
55
+ requestBody: undefined,
56
+ };
57
+ const errorSlug = buildErrorEndpointSlug(error);
58
+ const validSlug = buildEndpointSlug(validEndpoint);
59
+ expect(errorSlug).not.toBe(validSlug);
60
+ expect(errorSlug).toContain("_unsupported/");
61
+ expect(validSlug).toContain("default/");
62
+ });
63
+ it("generates unique IDs for errored endpoints", () => {
64
+ const error = {
65
+ path: "/pets/{petId}",
66
+ method: "delete",
67
+ reason: "test",
68
+ rawSpec: {},
69
+ };
70
+ const errId = `${error.method}-${error.path}`;
71
+ expect(errId).toBe("delete-/pets/{petId}");
72
+ // This matches buildEndpointId behavior
73
+ const validEndpoint = {
74
+ path: "/pets/{petId}",
75
+ method: "delete",
76
+ tags: [],
77
+ summary: "",
78
+ description: "",
79
+ operationId: "",
80
+ deprecated: false,
81
+ parameters: [],
82
+ responses: [],
83
+ security: [],
84
+ requestBody: undefined,
85
+ };
86
+ expect(errId).toBe(buildEndpointId(validEndpoint));
87
+ });
88
+ });
89
+ describe("ApiEndpointError data contract", () => {
90
+ it("has all required fields for rendering", () => {
91
+ const error = {
92
+ path: "/complex-endpoint",
93
+ method: "post",
94
+ reason: "Complex polymorphism (allOf with discriminator) is not supported",
95
+ rawSpec: {
96
+ post: {
97
+ summary: "Create complex resource",
98
+ requestBody: {
99
+ content: {
100
+ "application/json": {
101
+ schema: {
102
+ allOf: [{ $ref: "#/components/schemas/Base" }],
103
+ discriminator: { propertyName: "type" },
104
+ },
105
+ },
106
+ },
107
+ },
108
+ },
109
+ },
110
+ };
111
+ // Verify all fields exist and are the correct types
112
+ expect(typeof error.path).toBe("string");
113
+ expect(typeof error.method).toBe("string");
114
+ expect(typeof error.reason).toBe("string");
115
+ expect(error.rawSpec).toBeDefined();
116
+ expect(typeof error.rawSpec).toBe("object");
117
+ });
118
+ it("rawSpec serializes to valid JSON for display", () => {
119
+ const error = {
120
+ path: "/test",
121
+ method: "get",
122
+ reason: "test",
123
+ rawSpec: {
124
+ nested: { deeply: { value: [1, 2, 3] } },
125
+ special: 'chars "in" strings',
126
+ },
127
+ };
128
+ const json = JSON.stringify(error.rawSpec, null, 2);
129
+ expect(() => JSON.parse(json)).not.toThrow();
130
+ expect(json).toContain("nested");
131
+ expect(json).toContain("deeply");
132
+ });
133
+ it("handles empty rawSpec gracefully", () => {
134
+ const error = {
135
+ path: "/test",
136
+ method: "get",
137
+ reason: "test",
138
+ rawSpec: {},
139
+ };
140
+ const json = JSON.stringify(error.rawSpec, null, 2);
141
+ expect(json).toBe("{}");
142
+ });
143
+ it("handles null rawSpec gracefully", () => {
144
+ const error = {
145
+ path: "/test",
146
+ method: "get",
147
+ reason: "test",
148
+ rawSpec: null,
149
+ };
150
+ const json = JSON.stringify(error.rawSpec, null, 2);
151
+ expect(json).toBe("null");
152
+ });
153
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specglass/theme-default",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Default theme for Specglass — layouts, components, React islands, and styles",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,102 @@
1
+ ---
2
+ /**
3
+ * ApiEndpointFallback.astro — Renders a "Manual Documentation Required" placeholder
4
+ * for endpoints where the OpenAPI parser encountered unsupported spec patterns (FR40).
5
+ *
6
+ * Shows the error reason and raw spec data in a collapsible, syntax-highlighted block.
7
+ */
8
+ import type { ApiEndpointError } from "@specglass/core";
9
+ import { CopyButton } from "../islands/CopyButton";
10
+ import { highlight } from "../utils/shiki";
11
+
12
+ export interface Props {
13
+ error: ApiEndpointError;
14
+ }
15
+
16
+ const { error } = Astro.props;
17
+
18
+ const methodColors: Record<string, string> = {
19
+ get: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-emerald-500/30",
20
+ post: "bg-blue-500/15 text-blue-700 dark:text-blue-400 border-blue-500/30",
21
+ put: "bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/30",
22
+ patch: "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400 border-yellow-500/30",
23
+ delete: "bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/30",
24
+ options: "bg-purple-500/15 text-purple-700 dark:text-purple-400 border-purple-500/30",
25
+ head: "bg-gray-500/15 text-gray-700 dark:text-gray-400 border-gray-500/30",
26
+ };
27
+
28
+ const methodColor = methodColors[error.method.toLowerCase()] ?? methodColors.get;
29
+ const rawSpecJson = JSON.stringify(error.rawSpec, null, 2);
30
+ const highlightedSpec = await highlight(rawSpecJson, "json");
31
+ ---
32
+
33
+ <div id={`endpoint-${error.method}-${error.path.replace(/[{}\\\/]/g, "-")}`} class="mb-8">
34
+ {/* Method + Path header */}
35
+ <div class="flex items-center gap-3 mb-4">
36
+ <span
37
+ class:list={[
38
+ "inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider border",
39
+ methodColor,
40
+ ]}
41
+ >
42
+ {error.method.toUpperCase()}
43
+ </span>
44
+ <code class="text-lg font-mono text-text font-semibold break-all">
45
+ {error.path}
46
+ </code>
47
+ </div>
48
+
49
+ {/* Warning alert */}
50
+ <div
51
+ role="alert"
52
+ class="rounded-lg border border-amber-500/30 bg-amber-50 dark:bg-amber-950/20 px-5 py-4 mb-6"
53
+ >
54
+ <div class="flex items-start gap-3">
55
+ <span class="text-amber-600 dark:text-amber-400 text-xl mt-0.5" aria-hidden="true">⚠️</span>
56
+ <div>
57
+ <h3 class="text-base font-semibold text-amber-800 dark:text-amber-200 mt-0 mb-1">
58
+ Manual Documentation Required
59
+ </h3>
60
+ <p class="text-sm text-amber-700 dark:text-amber-300 leading-relaxed m-0">
61
+ This endpoint uses an OpenAPI spec pattern that couldn't be fully parsed.
62
+ {
63
+ error.reason && (
64
+ <span class="block mt-1 text-amber-600 dark:text-amber-400 font-medium">
65
+ {error.reason}
66
+ </span>
67
+ )
68
+ }
69
+ </p>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ {/* Raw spec data — collapsed by default */}
75
+ <details class="rounded-lg border border-border bg-surface-code overflow-hidden">
76
+ <summary
77
+ class="cursor-pointer px-4 py-3 text-sm font-medium text-text-muted hover:text-text transition-colors select-none flex items-center gap-2"
78
+ >
79
+ <svg
80
+ class="w-4 h-4 transition-transform open:rotate-90"
81
+ viewBox="0 0 20 20"
82
+ fill="currentColor"
83
+ aria-hidden="true"
84
+ >
85
+ <path
86
+ fill-rule="evenodd"
87
+ d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
88
+ clip-rule="evenodd"></path>
89
+ </svg>
90
+ View raw OpenAPI spec data
91
+ </summary>
92
+ <div class="relative group border-t border-border">
93
+ <div
94
+ class="[&_pre]:overflow-x-auto [&_pre]:p-4 [&_pre]:text-sm [&_pre]:leading-relaxed [&_pre]:m-0"
95
+ set:html={highlightedSpec}
96
+ />
97
+ <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
98
+ <CopyButton client:idle code={rawSpecJson} />
99
+ </div>
100
+ </div>
101
+ </details>
102
+ </div>
@@ -5,10 +5,12 @@
5
5
  * Generates cURL, Python, and Node.js request examples from endpoint data.
6
6
  * Uses a tab interface with localStorage persistence for language preference.
7
7
  * Each tab includes a copy button for the code snippet.
8
+ * Code blocks use Shiki dual-theme highlighting (AC7).
8
9
  */
9
10
  import type { ApiEndpoint } from "@specglass/core";
10
11
  import { generateCurlExample, generatePythonExample, generateNodeExample } from "@specglass/core";
11
12
  import { CopyButton } from "../islands/CopyButton";
13
+ import { highlight } from "../utils/shiki";
12
14
 
13
15
  export interface Props {
14
16
  endpoint: ApiEndpoint;
@@ -19,7 +21,7 @@ const { endpoint, baseUrl = "https://api.example.com" } = Astro.props;
19
21
 
20
22
  const options = { baseUrl };
21
23
 
22
- const languages = [
24
+ const rawLanguages = [
23
25
  { id: "curl", label: "cURL", code: generateCurlExample(endpoint, options), lang: "bash" },
24
26
  { id: "python", label: "Python", code: generatePythonExample(endpoint, options), lang: "python" },
25
27
  {
@@ -30,6 +32,14 @@ const languages = [
30
32
  },
31
33
  ];
32
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
+
33
43
  const tabGroupId = `api-req-${endpoint.method}-${endpoint.path.replace(/[{}/]/g, "-")}`;
34
44
  ---
35
45
 
@@ -81,9 +91,10 @@ const tabGroupId = `api-req-${endpoint.method}-${endpoint.path.replace(/[{}/]/g,
81
91
  class={i === 0 ? "" : "hidden"}
82
92
  >
83
93
  <div class="relative group">
84
- <pre class="overflow-x-auto p-4 bg-[#0d1117] dark:bg-[#0d1117] text-gray-300 text-sm font-mono leading-relaxed m-0">
85
- <code>{lang.code}</code>
86
- </pre>
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
+ />
87
98
  <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
88
99
  <CopyButton client:idle code={lang.code} />
89
100
  </div>
@@ -110,7 +121,7 @@ const tabGroupId = `api-req-${endpoint.method}-${endpoint.path.replace(/[{}/]/g,
110
121
  activateTab(saved);
111
122
  }
112
123
  } catch {
113
- // localStorage unavailable
124
+ // noop — localStorage unavailable (e.g., private browsing)
114
125
  }
115
126
 
116
127
  function activateTab(langId: string) {
@@ -142,7 +153,9 @@ const tabGroupId = `api-req-${endpoint.method}-${endpoint.path.replace(/[{}/]/g,
142
153
  // Persist and sync
143
154
  try {
144
155
  localStorage.setItem(STORAGE_KEY, lang);
145
- } catch {}
156
+ } catch {
157
+ /* noop */
158
+ }
146
159
  // Broadcast sync event for other tab groups on the page
147
160
  document.dispatchEvent(
148
161
  new CustomEvent("specglass-lang-sync", { detail: { syncKey: SYNC_KEY, lang } }),
@@ -9,6 +9,7 @@
9
9
  import type { ApiResponse as ApiResponseType } from "@specglass/core";
10
10
  import { extractMediaTypeExample } from "@specglass/core";
11
11
  import { CopyButton } from "../islands/CopyButton";
12
+ import { highlight } from "../utils/shiki";
12
13
 
13
14
  export interface Props {
14
15
  responses: ApiResponseType[];
@@ -28,7 +29,7 @@ function getStatusColor(code: string): string {
28
29
  return "bg-gray-500/15 text-gray-700 dark:text-gray-400 border-gray-500/30";
29
30
  }
30
31
 
31
- const responseExamples = responses.map((resp) => {
32
+ const responseExamplesRaw = responses.map((resp) => {
32
33
  let exampleJson: string | null = null;
33
34
  let contentType: string | null = null;
34
35
 
@@ -52,6 +53,14 @@ const responseExamples = responses.map((resp) => {
52
53
  statusColor: getStatusColor(resp.statusCode),
53
54
  };
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
+ );
55
64
  ---
56
65
 
57
66
  <div class="mt-6">
@@ -80,13 +89,14 @@ const responseExamples = responses.map((resp) => {
80
89
  </span>
81
90
  )}
82
91
  </div>
83
- {resp.exampleJson ? (
92
+ {resp.highlighted ? (
84
93
  <div class="relative group">
85
- <pre class="overflow-x-auto p-4 bg-[#0d1117] dark:bg-[#0d1117] text-gray-300 text-sm font-mono leading-relaxed m-0">
86
- <code>{resp.exampleJson}</code>
87
- </pre>
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
+ />
88
98
  <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
89
- <CopyButton client:idle code={resp.exampleJson} />
99
+ <CopyButton client:idle code={resp.exampleJson!} />
90
100
  </div>
91
101
  </div>
92
102
  ) : (
@@ -111,13 +121,14 @@ const responseExamples = responses.map((resp) => {
111
121
  </span>
112
122
  )}
113
123
  </summary>
114
- {resp.exampleJson ? (
124
+ {resp.highlighted ? (
115
125
  <div class="relative group border-t border-border">
116
- <pre class="overflow-x-auto p-4 bg-[#0d1117] dark:bg-[#0d1117] text-gray-300 text-sm font-mono leading-relaxed m-0">
117
- <code>{resp.exampleJson}</code>
118
- </pre>
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
+ />
119
130
  <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
120
- <CopyButton client:idle code={resp.exampleJson} />
131
+ <CopyButton client:idle code={resp.exampleJson!} />
121
132
  </div>
122
133
  </div>
123
134
  ) : (
@@ -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";
@@ -13,26 +14,32 @@ import ApiResponse from "../components/ApiResponse.astro";
13
14
  import ApiAuth from "../components/ApiAuth.astro";
14
15
  import ApiExampleRequest from "../components/ApiExampleRequest.astro";
15
16
  import ApiExampleResponse from "../components/ApiExampleResponse.astro";
17
+ import ApiEndpointFallback from "../components/ApiEndpointFallback.astro";
16
18
  import { ThemeToggle } from "../islands/ThemeToggle";
17
19
  import { SearchPalette } from "../islands/SearchPalette";
18
20
  import "../styles/global.css";
19
21
  import type {
20
22
  NavigationTree,
21
23
  ApiEndpoint as ApiEndpointType,
24
+ ApiEndpointError,
22
25
  ApiSecurityRequirement,
23
26
  SpecglassConfig,
24
27
  } from "@specglass/core";
25
28
  import { generateThemeCSS } from "../utils/theme-css";
26
29
 
27
30
  export interface Props {
28
- /** The current endpoint to display */
29
- 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;
30
35
  /** Documentation navigation tree */
31
36
  navigation: NavigationTree;
32
37
  /** Site configuration */
33
38
  config: SpecglassConfig;
34
- /** All endpoints for the sidebar navigation */
39
+ /** All valid endpoints for the sidebar navigation */
35
40
  allEndpoints: ApiEndpointType[];
41
+ /** All errored endpoints for the sidebar navigation */
42
+ allErrors?: ApiEndpointError[];
36
43
  /** Spec-level information */
37
44
  specInfo: {
38
45
  title: string;
@@ -42,15 +49,28 @@ export interface Props {
42
49
  securitySchemes?: Record<string, ApiSecurityRequirement>;
43
50
  }
44
51
 
45
- 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;
46
63
  const themeCSS = generateThemeCSS(config);
47
64
 
48
- const pageTitle = `${endpoint.method.toUpperCase()} ${endpoint.path} ${specInfo.title} API`;
49
- const pageDescription =
50
- endpoint.summary ||
51
- endpoint.description ||
52
- `API reference for ${endpoint.method.toUpperCase()} ${endpoint.path}`;
53
- 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}`;
54
74
  ---
55
75
 
56
76
  <html lang="en" class="dark">
@@ -75,7 +95,9 @@ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
75
95
  var stored = null;
76
96
  try {
77
97
  stored = localStorage.getItem("specglass-theme");
78
- } catch (e) {}
98
+ } catch (e) {
99
+ /* noop */
100
+ }
79
101
  var theme = stored === "dark" || stored === "light" ? stored : "dark";
80
102
  if (theme === "dark") {
81
103
  document.documentElement.classList.add("dark");
@@ -121,6 +143,7 @@ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
121
143
 
122
144
  <ApiNavigation
123
145
  endpoints={allEndpoints}
146
+ errors={allErrors}
124
147
  currentEndpointId={currentEndpointId}
125
148
  basePath="/api-reference"
126
149
  />
@@ -142,6 +165,7 @@ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
142
165
  </div>
143
166
  <ApiNavigation
144
167
  endpoints={allEndpoints}
168
+ errors={allErrors}
145
169
  currentEndpointId={currentEndpointId}
146
170
  basePath="/api-reference"
147
171
  />
@@ -150,44 +174,49 @@ const currentEndpointId = `${endpoint.method}-${endpoint.path}`;
150
174
  <!-- Main content area -->
151
175
  <main id="main-content" class="flex-1 min-w-0 md:ml-(--width-sidebar)" data-pagefind-body>
152
176
  <article class="max-w-(--width-content-max) mx-auto px-(--spacing-page) py-8">
153
- <!-- Endpoint header -->
154
- <ApiEndpoint endpoint={endpoint} />
155
-
156
- <!-- Authentication -->
157
177
  {
158
- endpoint.security && endpoint.security.length > 0 && (
159
- <ApiAuth security={endpoint.security} securitySchemes={securitySchemes} />
160
- )
161
- }
178
+ isErrorPage ? (
179
+ /* Fallback rendering for errored endpoints (FR40) */
180
+ <ApiEndpointFallback error={endpointError!} />
181
+ ) : (
182
+ <>
183
+ {/* Endpoint header */}
184
+ <ApiEndpoint endpoint={endpoint!} />
162
185
 
163
- <!-- Parameters & Request Body -->
164
- {
165
- (endpoint.parameters.length > 0 || endpoint.requestBody) && (
166
- <div>
167
- <h2 class="text-lg font-semibold text-text mb-4 mt-0">Parameters</h2>
168
- <ApiParameters
169
- parameters={endpoint.parameters}
170
- requestBody={endpoint.requestBody}
171
- />
172
- </div>
173
- )
174
- }
186
+ {/* Authentication */}
187
+ {endpoint!.security && endpoint!.security.length > 0 && (
188
+ <ApiAuth security={endpoint!.security} securitySchemes={securitySchemes} />
189
+ )}
175
190
 
176
- <!-- Responses -->
177
- {
178
- endpoint.responses.length > 0 && (
179
- <div>
180
- <h2 class="text-lg font-semibold text-text mb-4">Responses</h2>
181
- <ApiResponse responses={endpoint.responses} />
182
- </div>
183
- )
184
- }
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
+ )}
185
201
 
186
- {/* Request Example (code tabs: cURL, Python, Node.js) */}
187
- <ApiExampleRequest endpoint={endpoint} />
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
+ )}
188
209
 
189
- {/* Response Examples (per status code) */}
190
- {endpoint.responses.length > 0 && <ApiExampleResponse responses={endpoint.responses} />}
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
+ </>
218
+ )
219
+ }
191
220
  </article>
192
221
  </main>
193
222
  </div>