specra 0.1.0
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/LICENSE.MD +21 -0
- package/README.md +157 -0
- package/dist/app/api/mdx-watch/route.d.mts +12 -0
- package/dist/app/api/mdx-watch/route.d.ts +12 -0
- package/dist/app/api/mdx-watch/route.js +98 -0
- package/dist/app/api/mdx-watch/route.js.map +1 -0
- package/dist/app/api/mdx-watch/route.mjs +71 -0
- package/dist/app/api/mdx-watch/route.mjs.map +1 -0
- package/dist/app/docs-page.d.mts +32 -0
- package/dist/app/docs-page.d.ts +32 -0
- package/dist/app/docs-page.js +4072 -0
- package/dist/app/docs-page.js.map +1 -0
- package/dist/app/docs-page.mjs +14 -0
- package/dist/app/docs-page.mjs.map +1 -0
- package/dist/app/layout.css +297 -0
- package/dist/app/layout.css.map +1 -0
- package/dist/app/layout.d.mts +19 -0
- package/dist/app/layout.d.ts +19 -0
- package/dist/app/layout.js +112 -0
- package/dist/app/layout.js.map +1 -0
- package/dist/app/layout.mjs +13 -0
- package/dist/app/layout.mjs.map +1 -0
- package/dist/chunk-DR4EPLMT.mjs +1013 -0
- package/dist/chunk-DR4EPLMT.mjs.map +1 -0
- package/dist/chunk-INL2EC72.mjs +170 -0
- package/dist/chunk-INL2EC72.mjs.map +1 -0
- package/dist/chunk-IZFGEAD6.mjs +61 -0
- package/dist/chunk-IZFGEAD6.mjs.map +1 -0
- package/dist/chunk-KTRWWAGL.mjs +50 -0
- package/dist/chunk-KTRWWAGL.mjs.map +1 -0
- package/dist/chunk-MZJHJ6BV.mjs +21 -0
- package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
- package/dist/chunk-NXRIAL7T.mjs +3119 -0
- package/dist/chunk-NXRIAL7T.mjs.map +1 -0
- package/dist/components/index.d.mts +822 -0
- package/dist/components/index.d.ts +822 -0
- package/dist/components/index.js +3738 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +3627 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/index.css +297 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +545 -0
- package/dist/index.d.ts +545 -0
- package/dist/index.js +4648 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +347 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/index.d.mts +798 -0
- package/dist/lib/index.d.ts +798 -0
- package/dist/lib/index.js +1301 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/index.mjs +89 -0
- package/dist/lib/index.mjs.map +1 -0
- package/package.json +119 -0
- package/src/app/api/mdx-watch/route.ts +86 -0
- package/src/app/docs-page.tsx +212 -0
- package/src/app/layout.tsx +74 -0
- package/src/components/docs/accordion.tsx +53 -0
- package/src/components/docs/api/api-endpoint.tsx +59 -0
- package/src/components/docs/api/api-params.tsx +43 -0
- package/src/components/docs/api/api-playground.tsx +233 -0
- package/src/components/docs/api/api-reference.tsx +291 -0
- package/src/components/docs/api/api-response.tsx +48 -0
- package/src/components/docs/api/index.ts +5 -0
- package/src/components/docs/badge.tsx +22 -0
- package/src/components/docs/breadcrumb.tsx +51 -0
- package/src/components/docs/callout.tsx +109 -0
- package/src/components/docs/card.tsx +84 -0
- package/src/components/docs/category-index.tsx +112 -0
- package/src/components/docs/code-block.tsx +129 -0
- package/src/components/docs/columns.tsx +45 -0
- package/src/components/docs/componentTextProps.ts +85 -0
- package/src/components/docs/dev-mode-badge.tsx +35 -0
- package/src/components/docs/doc-layout-wrapper.tsx +54 -0
- package/src/components/docs/doc-layout.tsx +111 -0
- package/src/components/docs/doc-loading.tsx +15 -0
- package/src/components/docs/doc-metadata.tsx +55 -0
- package/src/components/docs/doc-navigation.tsx +62 -0
- package/src/components/docs/doc-tags.tsx +25 -0
- package/src/components/docs/draft-badge.tsx +10 -0
- package/src/components/docs/footer.tsx +47 -0
- package/src/components/docs/frame.tsx +22 -0
- package/src/components/docs/header.tsx +122 -0
- package/src/components/docs/hot-reload-indicator.tsx +77 -0
- package/src/components/docs/icon.tsx +70 -0
- package/src/components/docs/image-card.tsx +95 -0
- package/src/components/docs/image.tsx +73 -0
- package/src/components/docs/index.ts +48 -0
- package/src/components/docs/math.tsx +46 -0
- package/src/components/docs/mdx-components.tsx +166 -0
- package/src/components/docs/mdx-hot-reload.tsx +37 -0
- package/src/components/docs/mermaid.tsx +77 -0
- package/src/components/docs/mobile-doc-layout.tsx +115 -0
- package/src/components/docs/not-found-content.tsx +55 -0
- package/src/components/docs/search-highlight.tsx +127 -0
- package/src/components/docs/search-modal.tsx +223 -0
- package/src/components/docs/sidebar-skeleton.tsx +39 -0
- package/src/components/docs/sidebar.tsx +323 -0
- package/src/components/docs/site-banner.tsx +92 -0
- package/src/components/docs/steps.tsx +29 -0
- package/src/components/docs/tab-context.tsx +28 -0
- package/src/components/docs/tab-groups.tsx +50 -0
- package/src/components/docs/table-of-contents.tsx +104 -0
- package/src/components/docs/tabs.tsx +63 -0
- package/src/components/docs/theme-toggle.tsx +39 -0
- package/src/components/docs/tooltip.tsx +37 -0
- package/src/components/docs/version-switcher.tsx +52 -0
- package/src/components/docs/video.tsx +80 -0
- package/src/components/global/index.ts +3 -0
- package/src/components/global/version-not-found.tsx +26 -0
- package/src/components/index.ts +8 -0
- package/src/components/theme-provider.tsx +11 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/index.ts +41 -0
- package/src/lib/api-parser.types.ts +78 -0
- package/src/lib/api.types.ts +202 -0
- package/src/lib/category.ts +71 -0
- package/src/lib/config.server.ts +170 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/config.types.ts +295 -0
- package/src/lib/dev-utils.ts +75 -0
- package/src/lib/index.ts +27 -0
- package/src/lib/mdx-cache.ts +200 -0
- package/src/lib/mdx.ts +402 -0
- package/src/lib/parsers/base-parser.ts +16 -0
- package/src/lib/parsers/index.ts +69 -0
- package/src/lib/parsers/openapi-parser.ts +251 -0
- package/src/lib/parsers/postman-parser.ts +301 -0
- package/src/lib/parsers/specra-parser.ts +24 -0
- package/src/lib/redirects.ts +40 -0
- package/src/lib/remark-code-meta.ts +23 -0
- package/src/lib/sidebar-utils.ts +188 -0
- package/src/lib/toc.ts +24 -0
- package/src/lib/utils.ts +36 -0
- package/src/specra.config.json +124 -0
- package/src/styles/globals.css +427 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import type { SpecraApiSpec } from "@/lib/api-parser.types"
|
|
5
|
+
import { parseApiSpec, type ParserType } from "@/lib/parsers"
|
|
6
|
+
import { Accordion, AccordionItem } from "../accordion"
|
|
7
|
+
import { ApiEndpoint } from "./api-endpoint"
|
|
8
|
+
import { ApiParams } from "./api-params"
|
|
9
|
+
import { ApiResponse } from "./api-response"
|
|
10
|
+
import { ApiPlayground } from "./api-playground"
|
|
11
|
+
import { CodeBlock } from "../code-block"
|
|
12
|
+
import { Loader2 } from "lucide-react"
|
|
13
|
+
|
|
14
|
+
interface ApiReferenceProps {
|
|
15
|
+
/**
|
|
16
|
+
* Path to the API spec JSON file (relative to /public)
|
|
17
|
+
* Example: "/api-specs/my-api.json"
|
|
18
|
+
*/
|
|
19
|
+
spec: string
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parser type - auto-detect by default
|
|
23
|
+
* - "auto": Auto-detect format (Specra, OpenAPI, or Postman)
|
|
24
|
+
* - "specra": Native Specra format
|
|
25
|
+
* - "openapi": OpenAPI 3.x / Swagger
|
|
26
|
+
* - "postman": Postman Collection v2.x
|
|
27
|
+
*/
|
|
28
|
+
parser?: ParserType
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Show API playground for testing
|
|
32
|
+
*/
|
|
33
|
+
showPlayground?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ApiReference({ spec, parser = "auto", showPlayground = true }: ApiReferenceProps) {
|
|
37
|
+
const [apiSpec, setApiSpec] = useState<SpecraApiSpec | null>(null)
|
|
38
|
+
const [loading, setLoading] = useState(true)
|
|
39
|
+
const [error, setError] = useState<string | null>(null)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
async function loadSpec() {
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(spec)
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to load API spec: ${response.statusText}`)
|
|
47
|
+
}
|
|
48
|
+
const data = await response.json()
|
|
49
|
+
|
|
50
|
+
// Parse using the appropriate parser
|
|
51
|
+
const parsedSpec = parseApiSpec(data, parser)
|
|
52
|
+
setApiSpec(parsedSpec)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
setError(err instanceof Error ? err.message : "Failed to load API spec")
|
|
55
|
+
} finally {
|
|
56
|
+
setLoading(false)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
loadSpec()
|
|
61
|
+
}, [spec, parser])
|
|
62
|
+
|
|
63
|
+
// Replace environment variables in text
|
|
64
|
+
const interpolateEnv = (text: string, env?: Record<string, string>): string => {
|
|
65
|
+
if (!env) return text
|
|
66
|
+
return text.replace(/\{(\w+)\}/g, (match, key) => {
|
|
67
|
+
return env[key] || match
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (loading) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex items-center justify-center py-12">
|
|
74
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
75
|
+
<span className="ml-2 text-muted-foreground">Loading API specification...</span>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (error) {
|
|
81
|
+
return (
|
|
82
|
+
<div className="rounded-lg border border-red-500/20 bg-red-500/10 p-4">
|
|
83
|
+
<p className="text-sm text-red-600 dark:text-red-400">Error: {error}</p>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!apiSpec) {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="space-y-6">
|
|
94
|
+
{/* API Info */}
|
|
95
|
+
{(apiSpec.title || apiSpec.description) && (
|
|
96
|
+
<div className="mb-8">
|
|
97
|
+
{apiSpec.title && (
|
|
98
|
+
<h2 className="text-2xl font-semibold mb-2 text-foreground">{apiSpec.title}</h2>
|
|
99
|
+
)}
|
|
100
|
+
{apiSpec.description && (
|
|
101
|
+
<p className="text-muted-foreground">{apiSpec.description}</p>
|
|
102
|
+
)}
|
|
103
|
+
{apiSpec.baseUrl && (
|
|
104
|
+
<div className="mt-4">
|
|
105
|
+
<p className="text-sm font-semibold text-muted-foreground mb-1">Base URL</p>
|
|
106
|
+
<code className="text-sm px-2 py-1 bg-muted rounded">{apiSpec.baseUrl}</code>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Authentication */}
|
|
113
|
+
{apiSpec.auth && (
|
|
114
|
+
<div className="rounded-lg border border-border bg-card/30 p-4 mb-6">
|
|
115
|
+
<h3 className="text-lg font-semibold mb-2 text-foreground">Authentication</h3>
|
|
116
|
+
<p className="text-sm text-muted-foreground mb-2">
|
|
117
|
+
{apiSpec.auth.description || `This API uses ${apiSpec.auth.type} authentication.`}
|
|
118
|
+
</p>
|
|
119
|
+
{apiSpec.auth.type === "bearer" && (
|
|
120
|
+
<CodeBlock
|
|
121
|
+
code={`Authorization: ${apiSpec.auth.tokenPrefix || "Bearer"} {YOUR_TOKEN}`}
|
|
122
|
+
language="bash"
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
{apiSpec.auth.type === "apiKey" && (
|
|
126
|
+
<CodeBlock
|
|
127
|
+
code={`${apiSpec.auth.headerName || "X-API-Key"}: {YOUR_API_KEY}`}
|
|
128
|
+
language="bash"
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{/* Endpoints as Accordion */}
|
|
135
|
+
<Accordion type="single" collapsible className="space-y-4">
|
|
136
|
+
{apiSpec.endpoints.map((endpoint, index) => {
|
|
137
|
+
// Merge global and endpoint-specific headers
|
|
138
|
+
const allHeaders = [
|
|
139
|
+
...(apiSpec.globalHeaders || []),
|
|
140
|
+
...(endpoint.headers || []),
|
|
141
|
+
].map((header) => ({
|
|
142
|
+
...header,
|
|
143
|
+
value: interpolateEnv(header.value, apiSpec.env),
|
|
144
|
+
}))
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<AccordionItem
|
|
148
|
+
key={index}
|
|
149
|
+
value={`endpoint-${index}`}
|
|
150
|
+
title={
|
|
151
|
+
<div className="flex items-center gap-3">
|
|
152
|
+
<span
|
|
153
|
+
className={`text-xs font-semibold px-2 py-0.5 rounded ${
|
|
154
|
+
endpoint.method === "GET"
|
|
155
|
+
? "bg-blue-500/10 text-blue-600 dark:text-blue-400"
|
|
156
|
+
: endpoint.method === "POST"
|
|
157
|
+
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
|
158
|
+
: endpoint.method === "PUT"
|
|
159
|
+
? "bg-orange-500/10 text-orange-600 dark:text-orange-400"
|
|
160
|
+
: endpoint.method === "PATCH"
|
|
161
|
+
? "bg-purple-500/10 text-purple-600 dark:text-purple-400"
|
|
162
|
+
: "bg-red-500/10 text-red-600 dark:text-red-400"
|
|
163
|
+
}`}
|
|
164
|
+
>
|
|
165
|
+
{endpoint.method}
|
|
166
|
+
</span>
|
|
167
|
+
<code className="text-sm font-mono">{endpoint.path}</code>
|
|
168
|
+
<span className="text-sm text-muted-foreground ml-auto">{endpoint.title}</span>
|
|
169
|
+
</div>
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
<div className="space-y-6 pt-4">
|
|
173
|
+
{/* Description */}
|
|
174
|
+
{endpoint.description && (
|
|
175
|
+
<p className="text-sm text-muted-foreground">{endpoint.description}</p>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Path Parameters */}
|
|
179
|
+
{endpoint.pathParams && endpoint.pathParams.length > 0 && (
|
|
180
|
+
<ApiParams title="Path Parameters" params={endpoint.pathParams} />
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* Query Parameters */}
|
|
184
|
+
{endpoint.queryParams && endpoint.queryParams.length > 0 && (
|
|
185
|
+
<ApiParams title="Query Parameters" params={endpoint.queryParams} />
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{/* Headers */}
|
|
189
|
+
{allHeaders.length > 0 && (
|
|
190
|
+
<div>
|
|
191
|
+
<h4 className="text-sm font-semibold text-foreground mb-3">Headers</h4>
|
|
192
|
+
<div className="space-y-2">
|
|
193
|
+
{allHeaders.map((header, idx) => (
|
|
194
|
+
<div key={idx} className="flex flex-col gap-1">
|
|
195
|
+
<div className="flex items-center gap-2">
|
|
196
|
+
<code className="text-sm font-mono text-foreground">{header.name}</code>
|
|
197
|
+
<span className="text-xs text-muted-foreground">{header.value}</span>
|
|
198
|
+
</div>
|
|
199
|
+
{header.description && (
|
|
200
|
+
<p className="text-sm text-muted-foreground">{header.description}</p>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* Request Body */}
|
|
209
|
+
{endpoint.body && (
|
|
210
|
+
<div>
|
|
211
|
+
<h4 className="text-sm font-semibold text-foreground mb-3">Request Body</h4>
|
|
212
|
+
{endpoint.body.description && (
|
|
213
|
+
<p className="text-sm text-muted-foreground mb-2">
|
|
214
|
+
{endpoint.body.description}
|
|
215
|
+
</p>
|
|
216
|
+
)}
|
|
217
|
+
{endpoint.body.example && (
|
|
218
|
+
<CodeBlock
|
|
219
|
+
code={
|
|
220
|
+
typeof endpoint.body.example === "string"
|
|
221
|
+
? endpoint.body.example
|
|
222
|
+
: JSON.stringify(endpoint.body.example, null, 2)
|
|
223
|
+
}
|
|
224
|
+
language="json"
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Responses */}
|
|
231
|
+
<div>
|
|
232
|
+
<h4 className="text-sm font-semibold text-foreground mb-3">Responses</h4>
|
|
233
|
+
{endpoint.successResponse && (
|
|
234
|
+
<ApiResponse
|
|
235
|
+
status={endpoint.successResponse.status}
|
|
236
|
+
description={endpoint.successResponse.description}
|
|
237
|
+
example={endpoint.successResponse.example}
|
|
238
|
+
schema={endpoint.successResponse.schema}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
{endpoint.errorResponses?.map((response, idx) => (
|
|
242
|
+
<ApiResponse
|
|
243
|
+
key={idx}
|
|
244
|
+
status={response.status}
|
|
245
|
+
description={response.description}
|
|
246
|
+
example={response.example}
|
|
247
|
+
schema={response.schema}
|
|
248
|
+
/>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* Code Examples */}
|
|
253
|
+
{endpoint.examples && endpoint.examples.length > 0 && (
|
|
254
|
+
<div>
|
|
255
|
+
<h4 className="text-sm font-semibold text-foreground mb-3">Examples</h4>
|
|
256
|
+
{endpoint.examples.map((example, idx) => (
|
|
257
|
+
<div key={idx} className="mb-3">
|
|
258
|
+
<p className="text-xs font-semibold text-muted-foreground mb-2">
|
|
259
|
+
{example.title}
|
|
260
|
+
</p>
|
|
261
|
+
<CodeBlock code={example.code} language={example.language} />
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{/* API Playground */}
|
|
268
|
+
{showPlayground && (
|
|
269
|
+
<ApiPlayground
|
|
270
|
+
method={endpoint.method}
|
|
271
|
+
path={endpoint.path}
|
|
272
|
+
baseUrl={apiSpec.baseUrl}
|
|
273
|
+
headers={Object.fromEntries(allHeaders.map((h) => [h.name, h.value]))}
|
|
274
|
+
pathParams={endpoint.pathParams}
|
|
275
|
+
defaultBody={
|
|
276
|
+
endpoint.body?.example
|
|
277
|
+
? typeof endpoint.body.example === "string"
|
|
278
|
+
? endpoint.body.example
|
|
279
|
+
: JSON.stringify(endpoint.body.example, null, 2)
|
|
280
|
+
: undefined
|
|
281
|
+
}
|
|
282
|
+
/>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</AccordionItem>
|
|
286
|
+
)
|
|
287
|
+
})}
|
|
288
|
+
</Accordion>
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CodeBlock } from "../code-block"
|
|
2
|
+
|
|
3
|
+
interface ApiResponseProps {
|
|
4
|
+
status: number
|
|
5
|
+
description?: string
|
|
6
|
+
example?: any
|
|
7
|
+
schema?: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const statusColors: Record<string, string> = {
|
|
11
|
+
"2": "text-green-600 dark:text-green-400",
|
|
12
|
+
"3": "text-blue-600 dark:text-blue-400",
|
|
13
|
+
"4": "text-orange-600 dark:text-orange-400",
|
|
14
|
+
"5": "text-red-600 dark:text-red-400",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ApiResponse({ status, description, example, schema }: ApiResponseProps) {
|
|
18
|
+
const statusClass = statusColors[String(status)[0]] || "text-muted-foreground"
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="mb-4">
|
|
22
|
+
<div className="flex items-center gap-2 mb-2">
|
|
23
|
+
<span className={`text-sm font-semibold ${statusClass}`}>{status}</span>
|
|
24
|
+
{description && <span className="text-sm text-muted-foreground">{description}</span>}
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{example && (
|
|
28
|
+
<div className="mb-3">
|
|
29
|
+
<p className="text-xs font-semibold text-muted-foreground mb-2">Example Response</p>
|
|
30
|
+
<CodeBlock
|
|
31
|
+
code={typeof example === "string" ? example : JSON.stringify(example, null, 2)}
|
|
32
|
+
language="json"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
{schema && (
|
|
38
|
+
<div>
|
|
39
|
+
<p className="text-xs font-semibold text-muted-foreground mb-2">Schema</p>
|
|
40
|
+
<CodeBlock
|
|
41
|
+
code={typeof schema === "string" ? schema : JSON.stringify(schema, null, 2)}
|
|
42
|
+
language="json"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface BadgeProps {
|
|
2
|
+
children: React.ReactNode
|
|
3
|
+
variant?: "default" | "success" | "warning" | "error" | "info"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function Badge({ children, variant = "default" }: BadgeProps) {
|
|
7
|
+
const variants = {
|
|
8
|
+
default: "bg-muted text-foreground border-border",
|
|
9
|
+
success: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20",
|
|
10
|
+
warning: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/20",
|
|
11
|
+
error: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20",
|
|
12
|
+
info: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<span
|
|
17
|
+
className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border ${variants[variant]}`}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</span>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { ChevronRight } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
interface BreadcrumbProps {
|
|
5
|
+
version: string
|
|
6
|
+
slug: string
|
|
7
|
+
title: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Breadcrumb({ version, slug, title }: BreadcrumbProps) {
|
|
11
|
+
const parts = slug.split("/")
|
|
12
|
+
const breadcrumbs = [
|
|
13
|
+
{ label: "Docs", href: `/docs/${version}` },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
// Build breadcrumb path
|
|
17
|
+
let currentPath = ""
|
|
18
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
19
|
+
currentPath += (currentPath ? "/" : "") + parts[i]
|
|
20
|
+
breadcrumbs.push({
|
|
21
|
+
label: parts[i].replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
22
|
+
href: `/docs/${version}/${currentPath}`,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Add current page
|
|
27
|
+
breadcrumbs.push({
|
|
28
|
+
label: title,
|
|
29
|
+
href: `/docs/${version}/${slug}`,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<nav className="flex items-center gap-2 text-sm text-muted-foreground mb-4" aria-label="Breadcrumb">
|
|
34
|
+
{breadcrumbs.map((crumb, index) => (
|
|
35
|
+
<div key={crumb.href} className="flex items-center gap-2">
|
|
36
|
+
{index > 0 && <ChevronRight className="h-4 w-4" />}
|
|
37
|
+
{index === breadcrumbs.length - 1 ? (
|
|
38
|
+
<span className="text-foreground font-medium">{crumb.label}</span>
|
|
39
|
+
) : (
|
|
40
|
+
<Link
|
|
41
|
+
href={crumb.href}
|
|
42
|
+
className="hover:text-foreground transition-colors"
|
|
43
|
+
>
|
|
44
|
+
{crumb.label}
|
|
45
|
+
</Link>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</nav>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react"
|
|
4
|
+
import { Info, AlertTriangle, CheckCircle2, XCircle, Lightbulb } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
interface CalloutProps {
|
|
7
|
+
children: ReactNode
|
|
8
|
+
type?: "info" | "warning" | "success" | "error" | "tip" | "note" | "danger"
|
|
9
|
+
title?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Callout({ children, type = "info", title }: CalloutProps) {
|
|
13
|
+
const configs = {
|
|
14
|
+
info: {
|
|
15
|
+
icon: Info,
|
|
16
|
+
className: "bg-blue-500/10 border-blue-500/30 text-blue-900 dark:bg-blue-400/5 dark:border-blue-500/20 dark:text-blue-400",
|
|
17
|
+
iconClassName: "text-blue-600 dark:text-blue-400",
|
|
18
|
+
titleClassName: "text-blue-700 dark:text-blue-300",
|
|
19
|
+
defaultTitle: "Info",
|
|
20
|
+
},
|
|
21
|
+
note: {
|
|
22
|
+
icon: Info,
|
|
23
|
+
className: "bg-blue-500/10 border-blue-500/30 text-blue-900 dark:bg-blue-400/5 dark:border-blue-500/20 dark:text-blue-400",
|
|
24
|
+
iconClassName: "text-blue-600 dark:text-blue-400",
|
|
25
|
+
titleClassName: "text-blue-700 dark:text-blue-300",
|
|
26
|
+
defaultTitle: "Note",
|
|
27
|
+
},
|
|
28
|
+
warning: {
|
|
29
|
+
icon: AlertTriangle,
|
|
30
|
+
className: "bg-yellow-500/10 border-yellow-500/30 text-yellow-900 dark:bg-yellow-400/5 dark:border-yellow-500/20 dark:text-yellow-400",
|
|
31
|
+
iconClassName: "text-yellow-600 dark:text-yellow-400",
|
|
32
|
+
titleClassName: "text-yellow-700 dark:text-yellow-300",
|
|
33
|
+
defaultTitle: "Warning",
|
|
34
|
+
},
|
|
35
|
+
success: {
|
|
36
|
+
icon: CheckCircle2,
|
|
37
|
+
className: "bg-green-500/10 border-green-500/30 text-green-900 dark:bg-green-400/5 dark:border-green-500/20 dark:text-green-400",
|
|
38
|
+
iconClassName: "text-green-600 dark:text-green-400",
|
|
39
|
+
titleClassName: "text-green-700 dark:text-green-300",
|
|
40
|
+
defaultTitle: "Success",
|
|
41
|
+
},
|
|
42
|
+
error: {
|
|
43
|
+
icon: XCircle,
|
|
44
|
+
className: "bg-red-500/10 border-red-500/30 text-red-900 dark:bg-red-400/5 dark:border-red-500/20 dark:text-red-400",
|
|
45
|
+
iconClassName: "text-red-600 dark:text-red-400",
|
|
46
|
+
titleClassName: "text-red-700 dark:text-red-300",
|
|
47
|
+
defaultTitle: "Error",
|
|
48
|
+
},
|
|
49
|
+
danger: {
|
|
50
|
+
icon: XCircle,
|
|
51
|
+
className: "bg-red-500/10 border-red-500/30 text-red-900 dark:bg-red-400/5 dark:border-red-500/20 dark:text-red-400",
|
|
52
|
+
iconClassName: "text-red-600 dark:text-red-400",
|
|
53
|
+
titleClassName: "text-red-700 dark:text-red-300",
|
|
54
|
+
defaultTitle: "Danger",
|
|
55
|
+
},
|
|
56
|
+
tip: {
|
|
57
|
+
icon: Lightbulb,
|
|
58
|
+
className: "bg-purple-500/10 border-purple-500/30 text-purple-900 dark:bg-purple-400/5 dark:border-purple-500/20 dark:text-purple-400",
|
|
59
|
+
iconClassName: "text-purple-600 dark:text-purple-400",
|
|
60
|
+
titleClassName: "text-purple-700 dark:text-purple-300",
|
|
61
|
+
defaultTitle: "Tip",
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const config = configs[type]
|
|
66
|
+
const Icon = config.icon
|
|
67
|
+
|
|
68
|
+
// Extract title from strong/bold text if present, but ONLY if no explicit title prop was provided
|
|
69
|
+
let _title = title || config.defaultTitle
|
|
70
|
+
let content = children
|
|
71
|
+
|
|
72
|
+
if (!title && children && typeof children === "object") {
|
|
73
|
+
const childArray = Array.isArray(children) ? children : [children]
|
|
74
|
+
const firstElement = childArray[0]
|
|
75
|
+
|
|
76
|
+
// Check if first child is a paragraph with a strong element
|
|
77
|
+
if (firstElement && typeof firstElement === "object" && "props" in firstElement) {
|
|
78
|
+
const props = (firstElement as any).props
|
|
79
|
+
if (props.children && Array.isArray(props.children)) {
|
|
80
|
+
const strongChild = props.children.find(
|
|
81
|
+
(child: any) => child && typeof child === "object" && child.type === "strong",
|
|
82
|
+
)
|
|
83
|
+
if (strongChild) {
|
|
84
|
+
_title = strongChild.props.children
|
|
85
|
+
// Remove the title from content
|
|
86
|
+
content = childArray.map((child, idx) => {
|
|
87
|
+
if (idx === 0 && typeof child === "object" && "props" in child) {
|
|
88
|
+
const newChildren = (child as any).props.children.filter((c: any) => c !== strongChild)
|
|
89
|
+
return { ...child, props: { ...(child as any).props, children: newChildren } }
|
|
90
|
+
}
|
|
91
|
+
return child
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className={`flex gap-3 p-4 rounded-xl border my-2 ${config.className}`}>
|
|
100
|
+
<div className="flex-shrink-0 mt-0.5">
|
|
101
|
+
<Icon className={`h-5 w-5 ${config.iconClassName}`} />
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex-1 space-y-0">
|
|
104
|
+
<div className={`font-semibold text-sm ${config.titleClassName}`}>{_title}</div>
|
|
105
|
+
<div className="text-sm leading-relaxed [&>p]:mb-0 [&>p]:text-current">{content}</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ArrowRight, ExternalLink } from "lucide-react"
|
|
2
|
+
import Link from "next/link"
|
|
3
|
+
import { Icon } from "./icon"
|
|
4
|
+
|
|
5
|
+
interface CardProps {
|
|
6
|
+
title: string
|
|
7
|
+
description?: string
|
|
8
|
+
href?: string
|
|
9
|
+
icon?: string | React.ReactNode
|
|
10
|
+
children?: React.ReactNode
|
|
11
|
+
external?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Card({ title, description, href, icon, children, external = false }: CardProps) {
|
|
15
|
+
const content = (
|
|
16
|
+
<>
|
|
17
|
+
<div className="flex items-center gap-3">
|
|
18
|
+
{icon && (
|
|
19
|
+
<div className="shrink-0 w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
|
|
20
|
+
{typeof icon === "string" ? <Icon icon={icon} size={20} /> : icon}
|
|
21
|
+
</div>
|
|
22
|
+
)}
|
|
23
|
+
<div className="flex-1 min-w-0">
|
|
24
|
+
<h3 className={`font-semibold text-foreground mb-1 no-underline ${href ? 'group-hover:text-primary transition-colors' : ''}`}>
|
|
25
|
+
{title}
|
|
26
|
+
</h3>
|
|
27
|
+
{description && (
|
|
28
|
+
<p className="text-sm text-muted-foreground line-clamp-2 no-underline">{description}</p>
|
|
29
|
+
)}
|
|
30
|
+
{children && (
|
|
31
|
+
<div className="mt-2 text-sm text-muted-foreground no-underline">{children}</div>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
{href && (
|
|
35
|
+
<div className="shrink-0 self-start mt-1">
|
|
36
|
+
{external ? (
|
|
37
|
+
<ExternalLink className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
|
38
|
+
) : (
|
|
39
|
+
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
</>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (href) {
|
|
48
|
+
const Component = external ? "a" : Link
|
|
49
|
+
return (
|
|
50
|
+
<Component
|
|
51
|
+
href={href}
|
|
52
|
+
className="card-link group block p-4 rounded-xl border border-border hover:border-primary/50 hover:bg-muted/50 transition-all"
|
|
53
|
+
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
54
|
+
>
|
|
55
|
+
{content}
|
|
56
|
+
</Component>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="p-4 rounded-xl border border-border bg-muted/30 no-underline">
|
|
62
|
+
{content}
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface CardGridProps {
|
|
68
|
+
children: React.ReactNode
|
|
69
|
+
cols?: 1 | 2 | 3
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function CardGrid({ children, cols = 2 }: CardGridProps) {
|
|
73
|
+
const gridCols = {
|
|
74
|
+
1: "grid-cols-1",
|
|
75
|
+
2: "grid-cols-1 md:grid-cols-2",
|
|
76
|
+
3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={`grid ${gridCols[cols]} gap-4 my-6`}>
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|