blodemd 0.0.4 → 0.0.6

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 (185) hide show
  1. package/README.md +12 -1
  2. package/dev-server/app/[[...slug]]/page.tsx +139 -0
  3. package/dev-server/app/blodemd-dev/invalidate/route.ts +12 -0
  4. package/dev-server/app/blodemd-dev/static/[...path]/route.ts +32 -0
  5. package/dev-server/app/blodemd-dev/version/route.ts +14 -0
  6. package/dev-server/app/blodemd-internal/proxy/route.ts +86 -0
  7. package/dev-server/app/error.tsx +24 -0
  8. package/dev-server/app/globals.css +4 -0
  9. package/dev-server/app/layout.tsx +38 -0
  10. package/dev-server/app/not-found.tsx +18 -0
  11. package/dev-server/app/search/route.ts +17 -0
  12. package/dev-server/components/dev-reload-script.tsx +86 -0
  13. package/dev-server/components/providers.tsx +15 -0
  14. package/dev-server/lib/dev-state.ts +8 -0
  15. package/dev-server/lib/local-content-source.ts +103 -0
  16. package/dev-server/lib/local-runtime.tsx +558 -0
  17. package/dev-server/next.config.js +46 -0
  18. package/dev-server/package.json +57 -0
  19. package/dev-server/postcss.config.mjs +7 -0
  20. package/dev-server/public/glide-variable.woff2 +0 -0
  21. package/dev-server/tsconfig.json +49 -0
  22. package/dist/cli.mjs +299 -26
  23. package/dist/cli.mjs.map +1 -1
  24. package/docs/app/globals.css +455 -0
  25. package/docs/components/api/api-playground.tsx +295 -0
  26. package/docs/components/api/api-reference.tsx +121 -0
  27. package/docs/components/content/collection-index.tsx +114 -0
  28. package/docs/components/docs/contextual-menu.tsx +406 -0
  29. package/docs/components/docs/copy-page-menu.tsx +255 -0
  30. package/docs/components/docs/doc-header.tsx +192 -0
  31. package/docs/components/docs/doc-shell.tsx +289 -0
  32. package/docs/components/docs/doc-sidebar.tsx +206 -0
  33. package/docs/components/docs/doc-toc.tsx +45 -0
  34. package/docs/components/docs/mobile-nav.tsx +207 -0
  35. package/docs/components/mdx/accordion.tsx +83 -0
  36. package/docs/components/mdx/badge.tsx +79 -0
  37. package/docs/components/mdx/callout.tsx +88 -0
  38. package/docs/components/mdx/card.tsx +104 -0
  39. package/docs/components/mdx/code-block.tsx +75 -0
  40. package/docs/components/mdx/code-group.tsx +94 -0
  41. package/docs/components/mdx/color.tsx +87 -0
  42. package/docs/components/mdx/columns.tsx +25 -0
  43. package/docs/components/mdx/expandable.tsx +45 -0
  44. package/docs/components/mdx/field-layout.tsx +77 -0
  45. package/docs/components/mdx/frame.tsx +23 -0
  46. package/docs/components/mdx/get-text-content.ts +18 -0
  47. package/docs/components/mdx/icon.tsx +56 -0
  48. package/docs/components/mdx/index.tsx +96 -0
  49. package/docs/components/mdx/installer.tsx +20 -0
  50. package/docs/components/mdx/panel.tsx +11 -0
  51. package/docs/components/mdx/param-field.tsx +56 -0
  52. package/docs/components/mdx/preview.tsx +36 -0
  53. package/docs/components/mdx/prompt.tsx +63 -0
  54. package/docs/components/mdx/request-example.tsx +27 -0
  55. package/docs/components/mdx/response-field.tsx +42 -0
  56. package/docs/components/mdx/steps.tsx +92 -0
  57. package/docs/components/mdx/tabs.tsx +88 -0
  58. package/docs/components/mdx/tile.tsx +43 -0
  59. package/docs/components/mdx/tooltip.tsx +71 -0
  60. package/docs/components/mdx/tree.tsx +120 -0
  61. package/docs/components/mdx/type-table.tsx +71 -0
  62. package/docs/components/mdx/update.tsx +44 -0
  63. package/docs/components/mdx/video.tsx +12 -0
  64. package/docs/components/mdx/view.tsx +66 -0
  65. package/docs/components/providers.tsx +15 -0
  66. package/docs/components/ui/breadcrumb.tsx +92 -0
  67. package/docs/components/ui/button.tsx +90 -0
  68. package/docs/components/ui/card.tsx +92 -0
  69. package/docs/components/ui/command.tsx +139 -0
  70. package/docs/components/ui/dialog.tsx +97 -0
  71. package/docs/components/ui/field.tsx +237 -0
  72. package/docs/components/ui/input.tsx +105 -0
  73. package/docs/components/ui/label.tsx +22 -0
  74. package/docs/components/ui/popover.tsx +72 -0
  75. package/docs/components/ui/search.tsx +380 -0
  76. package/docs/components/ui/separator.tsx +26 -0
  77. package/docs/components/ui/sheet.tsx +104 -0
  78. package/docs/components/ui/sidebar.tsx +433 -0
  79. package/docs/components/ui/theme-toggle.tsx +62 -0
  80. package/docs/components/ui/tooltip.tsx +53 -0
  81. package/docs/lib/contextual-options.ts +193 -0
  82. package/docs/lib/docs-collection.ts +22 -0
  83. package/docs/lib/mdx.ts +90 -0
  84. package/docs/lib/navigation.ts +288 -0
  85. package/docs/lib/openapi.ts +158 -0
  86. package/docs/lib/routes.ts +10 -0
  87. package/docs/lib/server-cache.ts +83 -0
  88. package/docs/lib/shiki.ts +35 -0
  89. package/docs/lib/theme.ts +29 -0
  90. package/docs/lib/toc.ts +2 -0
  91. package/docs/lib/utils.ts +5 -0
  92. package/package.json +34 -3
  93. package/packages/@repo/common/dist/index.d.ts +9 -0
  94. package/packages/@repo/common/dist/index.d.ts.map +1 -0
  95. package/packages/@repo/common/dist/index.js +42 -0
  96. package/packages/@repo/common/package.json +34 -0
  97. package/packages/@repo/common/src/common.unit.test.ts +55 -0
  98. package/packages/@repo/common/src/index.ts +51 -0
  99. package/packages/@repo/contracts/dist/api-key.d.ts +30 -0
  100. package/packages/@repo/contracts/dist/api-key.d.ts.map +1 -0
  101. package/packages/@repo/contracts/dist/api-key.js +20 -0
  102. package/packages/@repo/contracts/dist/dates.d.ts +4 -0
  103. package/packages/@repo/contracts/dist/dates.d.ts.map +1 -0
  104. package/packages/@repo/contracts/dist/dates.js +2 -0
  105. package/packages/@repo/contracts/dist/deployment.d.ts +71 -0
  106. package/packages/@repo/contracts/dist/deployment.d.ts.map +1 -0
  107. package/packages/@repo/contracts/dist/deployment.js +46 -0
  108. package/packages/@repo/contracts/dist/domain.d.ts +94 -0
  109. package/packages/@repo/contracts/dist/domain.d.ts.map +1 -0
  110. package/packages/@repo/contracts/dist/domain.js +36 -0
  111. package/packages/@repo/contracts/dist/ids.d.ts +14 -0
  112. package/packages/@repo/contracts/dist/ids.d.ts.map +1 -0
  113. package/packages/@repo/contracts/dist/ids.js +10 -0
  114. package/packages/@repo/contracts/dist/index.d.ts +10 -0
  115. package/packages/@repo/contracts/dist/index.d.ts.map +1 -0
  116. package/packages/@repo/contracts/dist/index.js +11 -0
  117. package/packages/@repo/contracts/dist/pagination.d.ts +23 -0
  118. package/packages/@repo/contracts/dist/pagination.d.ts.map +1 -0
  119. package/packages/@repo/contracts/dist/pagination.js +15 -0
  120. package/packages/@repo/contracts/dist/project.d.ts +25 -0
  121. package/packages/@repo/contracts/dist/project.d.ts.map +1 -0
  122. package/packages/@repo/contracts/dist/project.js +23 -0
  123. package/packages/@repo/contracts/dist/tenant.d.ts +99 -0
  124. package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -0
  125. package/packages/@repo/contracts/dist/tenant.js +36 -0
  126. package/packages/@repo/contracts/dist/user.d.ts +9 -0
  127. package/packages/@repo/contracts/dist/user.d.ts.map +1 -0
  128. package/packages/@repo/contracts/dist/user.js +9 -0
  129. package/packages/@repo/contracts/package.json +37 -0
  130. package/packages/@repo/contracts/src/api-key.ts +27 -0
  131. package/packages/@repo/contracts/src/dates.ts +4 -0
  132. package/packages/@repo/contracts/src/deployment.ts +73 -0
  133. package/packages/@repo/contracts/src/domain.ts +51 -0
  134. package/packages/@repo/contracts/src/ids.ts +22 -0
  135. package/packages/@repo/contracts/src/index.ts +11 -0
  136. package/packages/@repo/contracts/src/pagination.ts +21 -0
  137. package/packages/@repo/contracts/src/project.ts +30 -0
  138. package/packages/@repo/contracts/src/tenant.ts +54 -0
  139. package/packages/@repo/contracts/src/user.ts +12 -0
  140. package/packages/@repo/models/dist/docs-config.d.ts +985 -0
  141. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -0
  142. package/packages/@repo/models/dist/docs-config.js +548 -0
  143. package/packages/@repo/models/dist/index.d.ts +3 -0
  144. package/packages/@repo/models/dist/index.d.ts.map +1 -0
  145. package/packages/@repo/models/dist/index.js +3 -0
  146. package/packages/@repo/models/dist/tenant.d.ts +25 -0
  147. package/packages/@repo/models/dist/tenant.d.ts.map +1 -0
  148. package/packages/@repo/models/dist/tenant.js +1 -0
  149. package/packages/@repo/models/package.json +37 -0
  150. package/packages/@repo/models/src/docs-config.ts +648 -0
  151. package/packages/@repo/models/src/index.ts +3 -0
  152. package/packages/@repo/models/src/tenant.ts +29 -0
  153. package/packages/@repo/prebuild/dist/index.d.ts +2 -0
  154. package/packages/@repo/prebuild/dist/index.d.ts.map +1 -0
  155. package/packages/@repo/prebuild/dist/index.js +2 -0
  156. package/packages/@repo/prebuild/dist/openapi.d.ts +43 -0
  157. package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -0
  158. package/packages/@repo/prebuild/dist/openapi.js +58 -0
  159. package/packages/@repo/prebuild/package.json +39 -0
  160. package/packages/@repo/prebuild/src/index.ts +2 -0
  161. package/packages/@repo/prebuild/src/openapi.ts +116 -0
  162. package/packages/@repo/previewing/dist/blob-source.d.ts +16 -0
  163. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -0
  164. package/packages/@repo/previewing/dist/blob-source.js +110 -0
  165. package/packages/@repo/previewing/dist/content-source.d.ts +12 -0
  166. package/packages/@repo/previewing/dist/content-source.d.ts.map +1 -0
  167. package/packages/@repo/previewing/dist/content-source.js +1 -0
  168. package/packages/@repo/previewing/dist/fs-source.d.ts +11 -0
  169. package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -0
  170. package/packages/@repo/previewing/dist/fs-source.js +79 -0
  171. package/packages/@repo/previewing/dist/index.d.ts +120 -0
  172. package/packages/@repo/previewing/dist/index.d.ts.map +1 -0
  173. package/packages/@repo/previewing/dist/index.js +984 -0
  174. package/packages/@repo/previewing/package.json +41 -0
  175. package/packages/@repo/previewing/src/blob-source.ts +167 -0
  176. package/packages/@repo/previewing/src/content-source.ts +12 -0
  177. package/packages/@repo/previewing/src/fs-source.ts +111 -0
  178. package/packages/@repo/previewing/src/index.ts +1490 -0
  179. package/packages/@repo/previewing/src/index.unit.test.ts +290 -0
  180. package/packages/@repo/validation/dist/index.d.ts +12 -0
  181. package/packages/@repo/validation/dist/index.d.ts.map +1 -0
  182. package/packages/@repo/validation/dist/index.js +30 -0
  183. package/packages/@repo/validation/package.json +37 -0
  184. package/packages/@repo/validation/src/index.ts +59 -0
  185. package/packages/@repo/validation/src/mintlify-docs-schema.json +5016 -0
@@ -0,0 +1,295 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import type { ChangeEvent } from "react";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import { Field, FieldLabel } from "@/components/ui/field";
8
+ import { Input } from "@/components/ui/input";
9
+ import type { OpenApiEntry } from "@/lib/openapi";
10
+
11
+ const extractParams = (entry: OpenApiEntry, location: "path" | "query") =>
12
+ (entry.operation.parameters ?? []).filter(
13
+ (param) => (param as { in?: string }).in === location
14
+ ) as { name?: string; required?: boolean; description?: string }[];
15
+
16
+ export const ApiPlayground = ({
17
+ entry,
18
+ proxyEnabled,
19
+ }: {
20
+ entry: OpenApiEntry;
21
+ proxyEnabled: boolean;
22
+ }) => {
23
+ const servers = entry.spec.servers ?? [];
24
+ const [serverIndex, setServerIndex] = useState(0);
25
+ const [response, setResponse] = useState<string | null>(null);
26
+ const [status, setStatus] = useState<number | null>(null);
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [body, setBody] = useState("{}");
29
+ const [authToken, setAuthToken] = useState("");
30
+ const [useProxy, setUseProxy] = useState(proxyEnabled);
31
+
32
+ const pathParams = useMemo(() => extractParams(entry, "path"), [entry]);
33
+ const queryParams = useMemo(() => extractParams(entry, "query"), [entry]);
34
+ const [pathValues, setPathValues] = useState<Record<string, string>>({});
35
+ const [queryValues, setQueryValues] = useState<Record<string, string>>({});
36
+
37
+ const baseUrl = servers[serverIndex]?.url ?? "";
38
+ const canSend = Boolean(baseUrl);
39
+
40
+ const buildUrl = useCallback(() => {
41
+ let { path } = entry.operation;
42
+ for (const param of pathParams) {
43
+ const key = param.name ?? "";
44
+ const value = pathValues[key] ?? "";
45
+ path = path.replace(`{${key}}`, encodeURIComponent(value));
46
+ }
47
+
48
+ const url = new URL(path, baseUrl || "http://localhost");
49
+ for (const param of queryParams) {
50
+ const key = param.name ?? "";
51
+ const value = queryValues[key];
52
+ if (value) {
53
+ url.searchParams.set(key, value);
54
+ }
55
+ }
56
+
57
+ return url.toString();
58
+ }, [
59
+ baseUrl,
60
+ entry.operation,
61
+ pathParams,
62
+ pathValues,
63
+ queryParams,
64
+ queryValues,
65
+ ]);
66
+
67
+ const handleUseProxyChange = useCallback(
68
+ (event: ChangeEvent<HTMLInputElement>) => {
69
+ setUseProxy(event.target.checked);
70
+ },
71
+ []
72
+ );
73
+ const handleServerChange = useCallback(
74
+ (event: ChangeEvent<HTMLSelectElement>) => {
75
+ setServerIndex(Number(event.target.value));
76
+ },
77
+ []
78
+ );
79
+ const handlePathValueChange = useCallback(
80
+ (event: ChangeEvent<HTMLInputElement>) => {
81
+ const { name, value } = event.target;
82
+ setPathValues((prev) => ({
83
+ ...prev,
84
+ [name]: value,
85
+ }));
86
+ },
87
+ []
88
+ );
89
+ const handleQueryValueChange = useCallback(
90
+ (event: ChangeEvent<HTMLInputElement>) => {
91
+ const { name, value } = event.target;
92
+ setQueryValues((prev) => ({
93
+ ...prev,
94
+ [name]: value,
95
+ }));
96
+ },
97
+ []
98
+ );
99
+ const handleAuthTokenChange = useCallback(
100
+ (event: ChangeEvent<HTMLInputElement>) => {
101
+ setAuthToken(event.target.value);
102
+ },
103
+ []
104
+ );
105
+ const handleBodyChange = useCallback(
106
+ (event: ChangeEvent<HTMLTextAreaElement>) => {
107
+ setBody(event.target.value);
108
+ },
109
+ []
110
+ );
111
+
112
+ const handleSend = useCallback(async () => {
113
+ const url = buildUrl();
114
+ setIsLoading(true);
115
+ setResponse(null);
116
+ setStatus(null);
117
+
118
+ try {
119
+ const { method } = entry.operation;
120
+ const requestHeaders = {
121
+ "Content-Type": "application/json",
122
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
123
+ };
124
+
125
+ const payload = {
126
+ body,
127
+ headers: requestHeaders,
128
+ method,
129
+ url,
130
+ };
131
+
132
+ const requestUrl = useProxy ? "/blodemd-internal/proxy" : url;
133
+ const requestMethod = useProxy ? "POST" : method;
134
+ const requestHeadersToSend = useProxy
135
+ ? {
136
+ "Content-Type": "application/json",
137
+ }
138
+ : requestHeaders;
139
+
140
+ let requestBody: string | undefined;
141
+ if (useProxy) {
142
+ requestBody = JSON.stringify(payload);
143
+ } else if (method === "GET") {
144
+ requestBody = undefined;
145
+ } else {
146
+ requestBody = body;
147
+ }
148
+
149
+ const res = await fetch(requestUrl, {
150
+ body: requestBody,
151
+ headers: requestHeadersToSend,
152
+ method: requestMethod,
153
+ });
154
+
155
+ const text = await res.text();
156
+ setStatus(res.status);
157
+ let formatted = text;
158
+ try {
159
+ formatted = JSON.stringify(JSON.parse(text), null, 2);
160
+ } catch {
161
+ formatted = text;
162
+ }
163
+ setResponse(formatted || "(empty response)");
164
+ } catch (error) {
165
+ setStatus(0);
166
+ setResponse(error instanceof Error ? error.message : "Request failed.");
167
+ } finally {
168
+ setIsLoading(false);
169
+ }
170
+ }, [authToken, body, buildUrl, entry.operation, useProxy]);
171
+
172
+ return (
173
+ <section className="mt-7 grid gap-3">
174
+ <div className="grid gap-3 rounded-xl border border-border bg-surface p-4">
175
+ <div className="flex items-center justify-between">
176
+ <h2>Try it out</h2>
177
+ {proxyEnabled ? (
178
+ <label className="flex items-center gap-2 text-sm">
179
+ <input
180
+ checked={useProxy}
181
+ className="accent-primary"
182
+ onChange={handleUseProxyChange}
183
+ type="checkbox"
184
+ />
185
+ Use docs proxy
186
+ </label>
187
+ ) : null}
188
+ </div>
189
+
190
+ {servers.length ? (
191
+ <Field>
192
+ <FieldLabel htmlFor="api-server">Server</FieldLabel>
193
+ <select
194
+ className="flex h-[var(--field-height)] w-full rounded-[var(--field-radius)] border border-input bg-card px-[var(--field-padding-x)] py-[var(--field-padding-y)] font-sans text-base text-foreground shadow-input transition-colors hover:border-input-hover focus:border-ring focus:outline-hidden focus:ring-2 focus:ring-ring/15 focus:ring-offset-1 focus:ring-offset-background"
195
+ id="api-server"
196
+ onChange={handleServerChange}
197
+ value={serverIndex}
198
+ >
199
+ {servers.map((server, index) => (
200
+ <option key={server.url} value={index}>
201
+ {server.url}
202
+ </option>
203
+ ))}
204
+ </select>
205
+ </Field>
206
+ ) : null}
207
+
208
+ {pathParams.length ? (
209
+ <div className="grid gap-2.5 grid-cols-[repeat(auto-fit,minmax(180px,1fr))]">
210
+ {pathParams.map((param) => (
211
+ <Field key={param.name}>
212
+ <FieldLabel htmlFor={`path-${param.name}`}>
213
+ {param.name}
214
+ </FieldLabel>
215
+ <Input
216
+ id={`path-${param.name}`}
217
+ name={param.name ?? ""}
218
+ onChange={handlePathValueChange}
219
+ placeholder={param.required ? "Required" : "Optional"}
220
+ type="text"
221
+ value={pathValues[param.name ?? ""] ?? ""}
222
+ />
223
+ </Field>
224
+ ))}
225
+ </div>
226
+ ) : null}
227
+
228
+ {queryParams.length ? (
229
+ <div className="grid gap-2.5 grid-cols-[repeat(auto-fit,minmax(180px,1fr))]">
230
+ {queryParams.map((param) => (
231
+ <Field key={param.name}>
232
+ <FieldLabel htmlFor={`query-${param.name}`}>
233
+ {param.name}
234
+ </FieldLabel>
235
+ <Input
236
+ id={`query-${param.name}`}
237
+ name={param.name ?? ""}
238
+ onChange={handleQueryValueChange}
239
+ placeholder={param.required ? "Required" : "Optional"}
240
+ type="text"
241
+ value={queryValues[param.name ?? ""] ?? ""}
242
+ />
243
+ </Field>
244
+ ))}
245
+ </div>
246
+ ) : null}
247
+
248
+ <Field>
249
+ <FieldLabel htmlFor="auth-token">Auth token</FieldLabel>
250
+ <Input
251
+ id="auth-token"
252
+ onChange={handleAuthTokenChange}
253
+ placeholder="Bearer token"
254
+ type="password"
255
+ value={authToken}
256
+ />
257
+ </Field>
258
+
259
+ {entry.operation.method === "GET" ? null : (
260
+ <Field>
261
+ <FieldLabel htmlFor="request-body">Request body</FieldLabel>
262
+ <textarea
263
+ className="flex w-full rounded-[var(--field-radius)] border border-input bg-card px-[var(--field-padding-x)] py-[var(--field-padding-y)] font-sans text-base text-foreground shadow-input transition-colors placeholder:text-placeholder-foreground hover:border-input-hover focus:border-ring focus:outline-hidden focus:ring-2 focus:ring-ring/15 focus:ring-offset-1 focus:ring-offset-background"
264
+ id="request-body"
265
+ onChange={handleBodyChange}
266
+ rows={6}
267
+ value={body}
268
+ />
269
+ </Field>
270
+ )}
271
+
272
+ <Button
273
+ className="w-full"
274
+ disabled={isLoading || !canSend}
275
+ onClick={handleSend}
276
+ type="button"
277
+ >
278
+ {isLoading ? "Sending..." : "Send request"}
279
+ </Button>
280
+ {canSend ? null : (
281
+ <p className="text-sm text-muted-foreground">
282
+ Add a server URL in your OpenAPI spec to enable requests.
283
+ </p>
284
+ )}
285
+
286
+ {response === null ? null : (
287
+ <div className="rounded-xl border border-border bg-primary/[0.08] p-3">
288
+ <div className="font-semibold">Status: {status}</div>
289
+ <pre className="mt-2 overflow-x-auto">{response}</pre>
290
+ </div>
291
+ )}
292
+ </div>
293
+ </section>
294
+ );
295
+ };
@@ -0,0 +1,121 @@
1
+ import dynamic from "next/dynamic";
2
+
3
+ import type { OpenApiEntry } from "@/lib/openapi";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const ApiPlayground = dynamic(async () => {
7
+ const apiModule = await import("@/components/api/api-playground");
8
+ return { default: apiModule.ApiPlayground };
9
+ });
10
+
11
+ const methodColors: Record<string, string> = {
12
+ delete: "bg-red-500",
13
+ get: "bg-blue-500",
14
+ patch: "bg-amber-500",
15
+ post: "bg-green-500",
16
+ put: "bg-orange-500",
17
+ };
18
+
19
+ export const ApiReference = ({
20
+ entry,
21
+ proxyEnabled,
22
+ }: {
23
+ entry: OpenApiEntry;
24
+ proxyEnabled: boolean;
25
+ }) => {
26
+ const { operation } = entry;
27
+ const parameters = operation.parameters ?? [];
28
+ const requestBody = operation.requestBody as
29
+ | { content?: Record<string, { schema?: unknown }> }
30
+ | undefined;
31
+ const responses = operation.responses ?? {};
32
+
33
+ return (
34
+ <div>
35
+ <header className="mb-4 flex items-center gap-4">
36
+ <div
37
+ className={cn(
38
+ "rounded-full px-2.5 py-1.5 text-xs font-semibold uppercase text-white",
39
+ methodColors[operation.method.toLowerCase()] ?? "bg-primary"
40
+ )}
41
+ >
42
+ {operation.method}
43
+ </div>
44
+ <div className="font-mono">{operation.path}</div>
45
+ </header>
46
+ {operation.summary ? (
47
+ <p className="font-semibold">{operation.summary}</p>
48
+ ) : null}
49
+ {operation.description ? (
50
+ <p className="text-muted-foreground">{operation.description}</p>
51
+ ) : null}
52
+
53
+ {parameters.length ? (
54
+ <section className="mt-7 grid gap-3">
55
+ <h2>Parameters</h2>
56
+ <div className="grid gap-2">
57
+ <div className="grid grid-cols-[120px_80px_80px_1fr] gap-3 rounded-lg border border-border bg-background/60 p-2.5 font-bold">
58
+ <span>Name</span>
59
+ <span>In</span>
60
+ <span>Required</span>
61
+ <span>Description</span>
62
+ </div>
63
+ {parameters.map((param) => {
64
+ const name = (param as { name?: string }).name ?? "";
65
+ const location = (param as { in?: string }).in ?? "";
66
+ const required = (param as { required?: boolean }).required
67
+ ? "Yes"
68
+ : "No";
69
+ const description =
70
+ (param as { description?: string }).description ?? "";
71
+
72
+ return (
73
+ <div
74
+ className="grid grid-cols-[120px_80px_80px_1fr] gap-3 rounded-lg border border-border bg-background/60 p-2.5"
75
+ key={name}
76
+ >
77
+ <span>{name}</span>
78
+ <span>{location}</span>
79
+ <span>{required}</span>
80
+ <span>{description}</span>
81
+ </div>
82
+ );
83
+ })}
84
+ </div>
85
+ </section>
86
+ ) : null}
87
+
88
+ {requestBody?.content ? (
89
+ <section className="mt-7 grid gap-3">
90
+ <h2>Request Body</h2>
91
+ <pre className="overflow-x-auto rounded-lg bg-code p-3 text-code-foreground">
92
+ {JSON.stringify(requestBody.content, null, 2)}
93
+ </pre>
94
+ </section>
95
+ ) : null}
96
+
97
+ {Object.keys(responses).length ? (
98
+ <section className="mt-7 grid gap-3">
99
+ <h2>Responses</h2>
100
+ <div className="grid gap-2">
101
+ {Object.entries(responses).map(([status, response]) => {
102
+ const description =
103
+ (response as { description?: string }).description ?? "";
104
+ return (
105
+ <div
106
+ className="flex justify-between rounded-lg border border-border bg-background/60 p-2.5"
107
+ key={status}
108
+ >
109
+ <span className="font-semibold">{status}</span>
110
+ <span className="text-muted-foreground">{description}</span>
111
+ </div>
112
+ );
113
+ })}
114
+ </div>
115
+ </section>
116
+ ) : null}
117
+
118
+ <ApiPlayground entry={entry} proxyEnabled={proxyEnabled} />
119
+ </div>
120
+ );
121
+ };
@@ -0,0 +1,114 @@
1
+ import type { ContentEntry } from "@repo/previewing";
2
+ import Link from "next/link";
3
+
4
+ import { toDocHref } from "@/lib/routes";
5
+
6
+ type CollectionEntry = Extract<ContentEntry, { kind: "entry" }>;
7
+
8
+ const formatDate = (value?: string) => {
9
+ if (!value) {
10
+ return null;
11
+ }
12
+ const date = new Date(value);
13
+ if (Number.isNaN(date.getTime())) {
14
+ return value;
15
+ }
16
+ return date.toLocaleDateString("en-US", {
17
+ day: "numeric",
18
+ month: "short",
19
+ year: "numeric",
20
+ });
21
+ };
22
+
23
+ const formatPrice = (price?: number, currency?: string) => {
24
+ if (price === undefined || !currency) {
25
+ return null;
26
+ }
27
+ try {
28
+ return new Intl.NumberFormat("en-US", {
29
+ currency,
30
+ style: "currency",
31
+ }).format(price);
32
+ } catch {
33
+ return `${price} ${currency}`;
34
+ }
35
+ };
36
+
37
+ const buildMeta = (entry: CollectionEntry) => {
38
+ const frontmatter = entry.frontmatter as Record<string, unknown>;
39
+ switch (entry.type) {
40
+ case "blog": {
41
+ const date = formatDate(frontmatter.date as string | undefined);
42
+ const tags = Array.isArray(frontmatter.tags)
43
+ ? frontmatter.tags.join(", ")
44
+ : null;
45
+ return [date, tags ? `Tags: ${tags}` : null].filter(Boolean);
46
+ }
47
+ case "courses": {
48
+ const order = frontmatter.order as number | undefined;
49
+ return order === undefined ? [] : [`Lesson ${order}`];
50
+ }
51
+ case "products": {
52
+ const sku = frontmatter.sku as string | undefined;
53
+ const price = formatPrice(
54
+ frontmatter.price as number | undefined,
55
+ frontmatter.currency as string | undefined
56
+ );
57
+ return [sku ? `SKU ${sku}` : null, price].filter(Boolean);
58
+ }
59
+ case "notes":
60
+ case "todos": {
61
+ const date = formatDate(frontmatter.date as string | undefined);
62
+ return date ? [date] : [];
63
+ }
64
+ case "forms": {
65
+ const fields = Array.isArray(frontmatter.fields)
66
+ ? frontmatter.fields.length
67
+ : null;
68
+ return fields ? [`${fields} fields`] : [];
69
+ }
70
+ case "sheets": {
71
+ const columns = Array.isArray(frontmatter.columns)
72
+ ? frontmatter.columns.length
73
+ : null;
74
+ return columns ? [`${columns} columns`] : [];
75
+ }
76
+ default: {
77
+ return [];
78
+ }
79
+ }
80
+ };
81
+
82
+ export const CollectionIndex = ({
83
+ entries,
84
+ basePath,
85
+ }: {
86
+ entries: CollectionEntry[];
87
+ basePath: string;
88
+ }) => (
89
+ <div className="grid gap-4">
90
+ {entries.map((entry) => {
91
+ const meta = buildMeta(entry);
92
+ return (
93
+ <Link
94
+ className="block rounded-xl border border-border bg-surface p-4.5 transition-[border-color,box-shadow] hover:border-primary hover:shadow-md"
95
+ href={toDocHref(entry.slug, basePath)}
96
+ key={`${entry.collectionId}-${entry.slug}`}
97
+ >
98
+ <div className="text-lg font-semibold">{entry.title}</div>
99
+ {entry.description ? (
100
+ <p className="mt-1.5 text-muted-foreground">{entry.description}</p>
101
+ ) : null}
102
+ {meta.length ? (
103
+ <div className="mt-2 text-sm text-muted-foreground">
104
+ {meta.join(" | ")}
105
+ </div>
106
+ ) : null}
107
+ {entry.type === "slides" ? (
108
+ <div className="mt-2 text-sm text-muted-foreground">Slide deck</div>
109
+ ) : null}
110
+ </Link>
111
+ );
112
+ })}
113
+ </div>
114
+ );