camelmind 0.1.0 → 0.2.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.
Files changed (28) hide show
  1. package/dist/index.js +4 -1
  2. package/package.json +4 -2
  3. package/template/app/[...slug]/page.tsx +27 -5
  4. package/template/app/api/feedback/route.ts +40 -0
  5. package/template/app/api-reference/[[...slug]]/page.tsx +165 -0
  6. package/template/app/home/page.tsx +6 -0
  7. package/template/components/ApiReference/ApiOverview.tsx +63 -0
  8. package/template/components/ApiReference/ApiSidebar.tsx +69 -0
  9. package/template/components/ApiReference/CodeSamples.tsx +186 -0
  10. package/template/components/ApiReference/EndpointPage.tsx +92 -0
  11. package/template/components/ApiReference/ExpandableCode.tsx +132 -0
  12. package/template/components/ApiReference/MethodBadge.tsx +29 -0
  13. package/template/components/ApiReference/ParamsTable.tsx +70 -0
  14. package/template/components/ApiReference/ResponseContext.tsx +32 -0
  15. package/template/components/ApiReference/ResponseDocs.tsx +135 -0
  16. package/template/components/ApiReference/ResponseExample.tsx +98 -0
  17. package/template/components/ApiReference/SchemaViewer.tsx +205 -0
  18. package/template/components/DocFeedback/DocFeedback.tsx +155 -0
  19. package/template/components/LastUpdated/LastUpdated.tsx +23 -0
  20. package/template/components/Nav/TopNav.tsx +14 -1
  21. package/template/components/PageNav/PageNav.tsx +2 -2
  22. package/template/lib/api-reference.ts +441 -0
  23. package/template/lib/api-types.ts +102 -0
  24. package/template/lib/api-utils.ts +62 -0
  25. package/template/lib/config-types.ts +16 -0
  26. package/template/lib/config.ts +12 -0
  27. package/template/lib/mdx.ts +27 -1
  28. package/template/lib/versions.ts +26 -0
@@ -0,0 +1,92 @@
1
+ import type { ParsedOperation, ParsedSpec } from "@/lib/api-types"
2
+ import { MethodBadge } from "./MethodBadge"
3
+ import { ParamsTable } from "./ParamsTable"
4
+ import { SchemaViewer } from "./SchemaViewer"
5
+ import { ResponseDocs } from "./ResponseDocs"
6
+ import { CodeSamples } from "./CodeSamples"
7
+ import { ResponseExample } from "./ResponseExample"
8
+ import { ResponseProvider } from "./ResponseContext"
9
+
10
+ type Props = {
11
+ operation: ParsedOperation
12
+ spec: ParsedSpec
13
+ }
14
+
15
+ export function EndpointPage({ operation: op, spec }: Props) {
16
+ const defaultCode =
17
+ op.responses.find((r) => r.statusCode.startsWith("2") && r.schema)?.statusCode ?? null
18
+
19
+ return (
20
+ <ResponseProvider defaultCode={defaultCode}>
21
+ <div className="flex flex-col flex-1 overflow-hidden">
22
+
23
+ {/* Full-width header — title + method + path on their own row */}
24
+ <div className="px-8 pt-8 pb-5 border-b border-gray-100 dark:border-gray-800 shrink-0">
25
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-50 mb-3">{op.summary}</h1>
26
+ <div className="flex items-center gap-3">
27
+ <MethodBadge method={op.method} />
28
+ <code className="text-sm font-mono text-gray-600 dark:text-gray-400">
29
+ {op.path}
30
+ </code>
31
+ {op.deprecated && (
32
+ <span className="text-xs font-medium text-amber-600 bg-amber-100 dark:bg-amber-900/30 dark:text-amber-400 px-2 py-0.5 rounded">
33
+ deprecated
34
+ </span>
35
+ )}
36
+ </div>
37
+ </div>
38
+
39
+ {/* Two-column body */}
40
+ <div className="flex flex-1 min-h-0 overflow-hidden">
41
+
42
+ {/* Left: description, params, request body, responses */}
43
+ <div className="flex-1 px-8 py-6 min-w-0 max-w-2xl overflow-y-auto">
44
+ {op.description && (
45
+ <div className="text-gray-600 dark:text-gray-400 mb-8 leading-relaxed whitespace-pre-line">
46
+ {op.description}
47
+ </div>
48
+ )}
49
+
50
+ <ParamsTable parameters={op.parameters} schemas={spec.schemas} group="path" />
51
+ <ParamsTable parameters={op.parameters} schemas={spec.schemas} group="query" />
52
+ <ParamsTable parameters={op.parameters} schemas={spec.schemas} group="header" />
53
+
54
+ {op.requestBody && (
55
+ <section className="mb-8">
56
+ <div className="flex items-center gap-2 mb-3">
57
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
58
+ Request Body
59
+ </h3>
60
+ {op.requestBody.required && (
61
+ <span className="text-[10px] font-medium text-red-500 uppercase tracking-wide">required</span>
62
+ )}
63
+ </div>
64
+ {op.requestBody.description && (
65
+ <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{op.requestBody.description}</p>
66
+ )}
67
+ <p className="text-xs font-mono text-gray-400 mb-3">{op.requestBody.contentType}</p>
68
+ <div className="rounded-lg border border-gray-200 dark:border-gray-800 px-4 py-2">
69
+ <SchemaViewer schema={op.requestBody.schema} schemas={spec.schemas} />
70
+ </div>
71
+ </section>
72
+ )}
73
+
74
+ <ResponseDocs responses={op.responses} schemas={spec.schemas} />
75
+ </div>
76
+
77
+ {/* Right: code samples + interactive response example */}
78
+ <div className="hidden xl:flex flex-col w-[520px] shrink-0 overflow-y-auto">
79
+ <div className="p-6">
80
+ <p className="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 mb-3">
81
+ Request
82
+ </p>
83
+ <CodeSamples samples={op.codeSamples} />
84
+ <ResponseExample responses={op.responses} schemas={spec.schemas} />
85
+ </div>
86
+ </div>
87
+
88
+ </div>
89
+ </div>
90
+ </ResponseProvider>
91
+ )
92
+ }
@@ -0,0 +1,132 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+
5
+ const CopyIcon = () => (
6
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
7
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
8
+ </svg>
9
+ )
10
+
11
+ const CheckIcon = () => (
12
+ <svg className="w-4 h-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
13
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
14
+ </svg>
15
+ )
16
+
17
+ const ExpandIcon = () => (
18
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
19
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
20
+ </svg>
21
+ )
22
+
23
+ const CloseIcon = () => (
24
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
25
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
26
+ </svg>
27
+ )
28
+
29
+ type Props = {
30
+ code: string
31
+ }
32
+
33
+ export function ExpandableCode({ code }: Props) {
34
+ const [copied, setCopied] = useState(false)
35
+ const [modalCopied, setModalCopied] = useState(false)
36
+ const [expanded, setExpanded] = useState(false)
37
+
38
+ useEffect(() => {
39
+ if (!expanded) return
40
+ function onKey(e: KeyboardEvent) {
41
+ if (e.key === "Escape") setExpanded(false)
42
+ }
43
+ window.addEventListener("keydown", onKey)
44
+ return () => window.removeEventListener("keydown", onKey)
45
+ }, [expanded])
46
+
47
+ function copy(setFlag: (v: boolean) => void) {
48
+ navigator.clipboard.writeText(code).then(() => {
49
+ setFlag(true)
50
+ setTimeout(() => setFlag(false), 1500)
51
+ })
52
+ }
53
+
54
+ const lines = code.split("\n")
55
+
56
+ return (
57
+ <>
58
+ {/* Inline block */}
59
+ <div className="rounded-lg overflow-hidden border border-gray-700 bg-[#1a1d23]">
60
+ {/* Toolbar */}
61
+ <div className="flex items-center justify-end gap-0.5 px-2 py-1 border-b border-gray-700">
62
+ <button
63
+ onClick={() => copy(setCopied)}
64
+ className="p-1.5 text-gray-400 hover:text-gray-200 transition-colors"
65
+ title="Copy"
66
+ >
67
+ {copied ? <CheckIcon /> : <CopyIcon />}
68
+ </button>
69
+ <button
70
+ onClick={() => setExpanded(true)}
71
+ className="p-1.5 text-gray-400 hover:text-gray-200 transition-colors"
72
+ title="Expand"
73
+ >
74
+ <ExpandIcon />
75
+ </button>
76
+ </div>
77
+
78
+ <pre className="p-4 text-[13px] leading-relaxed text-gray-200 overflow-x-auto font-mono whitespace-pre">
79
+ <code>{code}</code>
80
+ </pre>
81
+ </div>
82
+
83
+ {/* Expanded modal */}
84
+ {expanded && (
85
+ <div
86
+ className="fixed inset-0 z-50 flex items-center justify-center p-6"
87
+ onClick={(e) => { if (e.target === e.currentTarget) setExpanded(false) }}
88
+ >
89
+ <div className="absolute inset-0 bg-black/70" />
90
+
91
+ <div className="relative w-full max-w-4xl max-h-[85vh] flex flex-col rounded-xl overflow-hidden border border-gray-700 bg-[#1a1d23] shadow-2xl">
92
+ {/* Modal header */}
93
+ <div className="flex items-center justify-end gap-0.5 px-3 py-2 border-b border-gray-700 shrink-0">
94
+ <button
95
+ onClick={() => copy(setModalCopied)}
96
+ className="p-1.5 text-gray-400 hover:text-gray-200 transition-colors"
97
+ title="Copy"
98
+ >
99
+ {modalCopied ? <CheckIcon /> : <CopyIcon />}
100
+ </button>
101
+ <button
102
+ onClick={() => setExpanded(false)}
103
+ className="p-1.5 text-gray-400 hover:text-gray-200 transition-colors"
104
+ title="Close"
105
+ >
106
+ <CloseIcon />
107
+ </button>
108
+ </div>
109
+
110
+ {/* Code with line numbers */}
111
+ <div className="overflow-auto flex-1">
112
+ <table className="w-full border-collapse">
113
+ <tbody>
114
+ {lines.map((line, i) => (
115
+ <tr key={i} className="hover:bg-white/5">
116
+ <td className="select-none text-right text-gray-600 text-[13px] font-mono px-4 py-0 leading-6 w-10 shrink-0">
117
+ {i + 1}
118
+ </td>
119
+ <td className="text-gray-200 text-[13px] font-mono pl-4 pr-8 py-0 leading-6 whitespace-pre">
120
+ {line || " "}
121
+ </td>
122
+ </tr>
123
+ ))}
124
+ </tbody>
125
+ </table>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ )}
130
+ </>
131
+ )
132
+ }
@@ -0,0 +1,29 @@
1
+ import type { HttpMethod } from "@/lib/api-types"
2
+
3
+ const METHOD_STYLES: Record<string, string> = {
4
+ get: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
5
+ post: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
6
+ put: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300",
7
+ patch: "bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300",
8
+ delete: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
9
+ head: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
10
+ options: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
11
+ }
12
+
13
+ type Props = {
14
+ method: HttpMethod
15
+ size?: "sm" | "md"
16
+ }
17
+
18
+ export function MethodBadge({ method, size = "md" }: Props) {
19
+ const styles = METHOD_STYLES[method] ?? METHOD_STYLES.get
20
+ const sizeClass = size === "sm"
21
+ ? "text-[10px] px-1.5 py-0.5 min-w-[38px]"
22
+ : "text-xs px-2 py-1 min-w-[46px]"
23
+
24
+ return (
25
+ <span className={`inline-flex items-center justify-center rounded font-mono font-bold uppercase tracking-wide shrink-0 ${sizeClass} ${styles}`}>
26
+ {method}
27
+ </span>
28
+ )
29
+ }
@@ -0,0 +1,70 @@
1
+ import type { ParsedParameter, SchemaObject } from "@/lib/api-types"
2
+ import { SchemaViewer } from "./SchemaViewer"
3
+
4
+ type Props = {
5
+ parameters: ParsedParameter[]
6
+ schemas: Record<string, SchemaObject>
7
+ group: "path" | "query" | "header"
8
+ }
9
+
10
+ const GROUP_LABELS: Record<string, string> = {
11
+ path: "Path Parameters",
12
+ query: "Query Parameters",
13
+ header: "Header Parameters",
14
+ }
15
+
16
+ function TypeTag({ schema }: { schema: SchemaObject }) {
17
+ if (schema.enum) {
18
+ return (
19
+ <span className="text-xs font-mono text-gray-500">
20
+ {schema.type ?? "string"}{" "}
21
+ <span className="text-gray-400 text-[11px]">
22
+ ({schema.enum.map((v) => JSON.stringify(v)).join(" | ")})
23
+ </span>
24
+ </span>
25
+ )
26
+ }
27
+ if (schema.format) {
28
+ return <span className="text-xs font-mono text-gray-500">{schema.type} ({schema.format})</span>
29
+ }
30
+ return <span className="text-xs font-mono text-gray-500">{schema.type ?? "string"}</span>
31
+ }
32
+
33
+ export function ParamsTable({ parameters, schemas, group }: Props) {
34
+ const filtered = parameters.filter((p) => p.in === group)
35
+ if (!filtered.length) return null
36
+
37
+ return (
38
+ <section className="mb-8">
39
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 mb-3">
40
+ {GROUP_LABELS[group]}
41
+ </h3>
42
+ <div className="rounded-lg border border-gray-200 dark:border-gray-800 divide-y divide-gray-100 dark:divide-gray-800">
43
+ {filtered.map((param) => (
44
+ <div key={param.name} className="px-4 py-3">
45
+ <div className="flex items-center gap-2 flex-wrap mb-0.5">
46
+ <code className="text-sm font-mono font-medium text-gray-900 dark:text-gray-100">
47
+ {param.name}
48
+ </code>
49
+ <TypeTag schema={param.schema} />
50
+ {param.required && (
51
+ <span className="text-[10px] font-medium text-red-500 uppercase tracking-wide">required</span>
52
+ )}
53
+ {param.deprecated && (
54
+ <span className="text-[10px] font-medium text-amber-500 uppercase tracking-wide">deprecated</span>
55
+ )}
56
+ </div>
57
+ {param.description && (
58
+ <p className="text-sm text-gray-500 dark:text-gray-400">{param.description}</p>
59
+ )}
60
+ {param.schema.default !== undefined && (
61
+ <p className="text-xs text-gray-400 mt-0.5">
62
+ Default: <code className="font-mono">{JSON.stringify(param.schema.default)}</code>
63
+ </p>
64
+ )}
65
+ </div>
66
+ ))}
67
+ </div>
68
+ </section>
69
+ )
70
+ }
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import { createContext, useContext, useState } from "react"
4
+
5
+ type ResponseContextValue = {
6
+ selectedCode: string | null
7
+ setSelectedCode: (code: string | null) => void
8
+ }
9
+
10
+ const ResponseContext = createContext<ResponseContextValue>({
11
+ selectedCode: null,
12
+ setSelectedCode: () => {},
13
+ })
14
+
15
+ export function ResponseProvider({
16
+ children,
17
+ defaultCode,
18
+ }: {
19
+ children: React.ReactNode
20
+ defaultCode: string | null
21
+ }) {
22
+ const [selectedCode, setSelectedCode] = useState<string | null>(defaultCode)
23
+ return (
24
+ <ResponseContext.Provider value={{ selectedCode, setSelectedCode }}>
25
+ {children}
26
+ </ResponseContext.Provider>
27
+ )
28
+ }
29
+
30
+ export function useResponseContext() {
31
+ return useContext(ResponseContext)
32
+ }
@@ -0,0 +1,135 @@
1
+ "use client"
2
+
3
+ import type { ParsedResponse, SchemaObject } from "@/lib/api-types"
4
+ import { SchemaViewer } from "./SchemaViewer"
5
+ import { useResponseContext } from "./ResponseContext"
6
+
7
+ function statusBadgeClass(code: string) {
8
+ const c = code[0]
9
+ if (c === "2") return "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300"
10
+ if (c === "3") return "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300"
11
+ return "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300"
12
+ }
13
+
14
+ type Props = {
15
+ responses: ParsedResponse[]
16
+ schemas: Record<string, SchemaObject>
17
+ }
18
+
19
+ function SuccessRow({ response, schemas }: { response: ParsedResponse; schemas: Record<string, SchemaObject> }) {
20
+ const { selectedCode, setSelectedCode } = useResponseContext()
21
+ const isSelected = selectedCode === response.statusCode
22
+
23
+ return (
24
+ <div
25
+ className={`rounded-lg border px-4 py-3 mb-2 cursor-pointer transition-colors ${
26
+ isSelected
27
+ ? "border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/10"
28
+ : "border-gray-200 dark:border-gray-800 hover:border-gray-300 dark:hover:border-gray-700"
29
+ }`}
30
+ onClick={() => setSelectedCode(response.statusCode)}
31
+ >
32
+ <div className="flex items-center gap-3 mb-2">
33
+ <span className={`text-xs font-mono font-bold px-2 py-0.5 rounded ${statusBadgeClass(response.statusCode)}`}>
34
+ {response.statusCode}
35
+ </span>
36
+ <span className="text-sm text-gray-700 dark:text-gray-300">{response.description}</span>
37
+ </div>
38
+ {response.schema && (
39
+ <div className="mt-2">
40
+ {response.contentType && (
41
+ <p className="text-xs font-mono text-gray-400 mb-2">{response.contentType}</p>
42
+ )}
43
+ <SchemaViewer schema={response.schema} schemas={schemas} />
44
+ </div>
45
+ )}
46
+ </div>
47
+ )
48
+ }
49
+
50
+ function ErrorRow({ response, schemas }: { response: ParsedResponse; schemas: Record<string, SchemaObject> }) {
51
+ const { selectedCode, setSelectedCode } = useResponseContext()
52
+ const isExpanded = selectedCode === response.statusCode
53
+
54
+ if (isExpanded) {
55
+ return (
56
+ <div className="border-b border-gray-100 dark:border-gray-800 last:border-0 bg-red-50/50 dark:bg-red-900/5">
57
+ {/* Expanded header */}
58
+ <div className="flex items-center gap-3 px-4 py-3">
59
+ <span className={`text-xs font-mono font-bold px-2 py-0.5 rounded ${statusBadgeClass(response.statusCode)}`}>
60
+ {response.statusCode}
61
+ </span>
62
+ <span className="text-sm text-gray-700 dark:text-gray-300 flex-1">{response.description}</span>
63
+ <button
64
+ onClick={() => setSelectedCode(null)}
65
+ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors p-0.5"
66
+ aria-label="Close"
67
+ >
68
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
69
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
70
+ </svg>
71
+ </button>
72
+ </div>
73
+
74
+ {/* Schema */}
75
+ {response.schema && (
76
+ <div className="px-4 pb-4">
77
+ {response.contentType && (
78
+ <p className="text-xs font-mono text-gray-400 mb-2">{response.contentType}</p>
79
+ )}
80
+ <SchemaViewer schema={response.schema} schemas={schemas} />
81
+ </div>
82
+ )}
83
+ </div>
84
+ )
85
+ }
86
+
87
+ return (
88
+ <button
89
+ onClick={() => setSelectedCode(response.statusCode)}
90
+ className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-900/40 transition-colors border-b border-gray-100 dark:border-gray-800 last:border-0"
91
+ >
92
+ <span className={`text-xs font-mono font-bold px-2 py-0.5 rounded ${statusBadgeClass(response.statusCode)}`}>
93
+ {response.statusCode}
94
+ </span>
95
+ <span className="text-sm text-gray-700 dark:text-gray-300">{response.description}</span>
96
+ </button>
97
+ )
98
+ }
99
+
100
+ export function ResponseDocs({ responses, schemas }: Props) {
101
+ if (!responses.length) return null
102
+
103
+ const successResponses = responses.filter((r) => r.statusCode.startsWith("2") || r.statusCode.startsWith("3"))
104
+ const errorResponses = responses.filter((r) => r.statusCode.startsWith("4") || r.statusCode.startsWith("5"))
105
+
106
+ return (
107
+ <section className="mb-8">
108
+ {successResponses.length > 0 && (
109
+ <>
110
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 mb-3">
111
+ Responses
112
+ </h3>
113
+ <div className="mb-6">
114
+ {successResponses.map((r) => (
115
+ <SuccessRow key={r.statusCode} response={r} schemas={schemas} />
116
+ ))}
117
+ </div>
118
+ </>
119
+ )}
120
+
121
+ {errorResponses.length > 0 && (
122
+ <>
123
+ <h3 className="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 mb-3">
124
+ Errors
125
+ </h3>
126
+ <div className="rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden">
127
+ {errorResponses.map((r) => (
128
+ <ErrorRow key={r.statusCode} response={r} schemas={schemas} />
129
+ ))}
130
+ </div>
131
+ </>
132
+ )}
133
+ </section>
134
+ )
135
+ }
@@ -0,0 +1,98 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useRef } from "react"
4
+ import type { ParsedResponse, SchemaObject } from "@/lib/api-types"
5
+ import { buildExample } from "@/lib/api-utils"
6
+ import { useResponseContext } from "./ResponseContext"
7
+ import { ExpandableCode } from "./ExpandableCode"
8
+
9
+ function statusBadgeClass(code: string) {
10
+ const c = code[0]
11
+ if (c === "2") return "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300"
12
+ if (c === "3") return "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300"
13
+ return "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300"
14
+ }
15
+
16
+ type Props = {
17
+ responses: ParsedResponse[]
18
+ schemas: Record<string, SchemaObject>
19
+ }
20
+
21
+ export function ResponseExample({ responses, schemas }: Props) {
22
+ const { selectedCode, setSelectedCode } = useResponseContext()
23
+ const [dropdownOpen, setDropdownOpen] = useState(false)
24
+ const dropdownRef = useRef<HTMLDivElement>(null)
25
+
26
+ const responsesWithSchema = responses.filter((r) => r.schema)
27
+ const current =
28
+ (selectedCode ? responsesWithSchema.find((r) => r.statusCode === selectedCode) : null) ??
29
+ responsesWithSchema[0]
30
+
31
+ const json = current?.schema
32
+ ? JSON.stringify(buildExample(current.schema, schemas), null, 2)
33
+ : null
34
+
35
+ useEffect(() => {
36
+ if (!dropdownOpen) return
37
+ function onClickOutside(e: MouseEvent) {
38
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
39
+ setDropdownOpen(false)
40
+ }
41
+ }
42
+ document.addEventListener("mousedown", onClickOutside)
43
+ return () => document.removeEventListener("mousedown", onClickOutside)
44
+ }, [dropdownOpen])
45
+
46
+ if (!current || !json) return null
47
+
48
+ return (
49
+ <div className="mt-6">
50
+ <p className="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 mb-3">
51
+ Response
52
+ </p>
53
+
54
+ {/* Status code selector */}
55
+ <div className="relative mb-3" ref={dropdownRef}>
56
+ <button
57
+ onClick={() => setDropdownOpen((o) => !o)}
58
+ className="flex items-center gap-2 px-3 py-2 w-full rounded-lg border border-gray-700 bg-[#1e2129] hover:bg-[#252830] transition-colors text-left"
59
+ >
60
+ <span className={`text-xs font-mono font-bold px-1.5 py-0.5 rounded ${statusBadgeClass(current.statusCode)}`}>
61
+ {current.statusCode}
62
+ </span>
63
+ <span className="text-sm text-gray-300 flex-1 truncate">{current.description}</span>
64
+ <svg
65
+ className={`w-4 h-4 text-gray-500 shrink-0 transition-transform ${dropdownOpen ? "rotate-180" : ""}`}
66
+ fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
67
+ >
68
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
69
+ </svg>
70
+ </button>
71
+
72
+ {dropdownOpen && (
73
+ <div className="absolute top-full left-0 right-0 mt-1 rounded-lg border border-gray-700 bg-[#1a1d23] shadow-xl z-10 overflow-hidden">
74
+ {responsesWithSchema.map((r) => (
75
+ <button
76
+ key={r.statusCode}
77
+ onClick={() => {
78
+ setSelectedCode(r.statusCode)
79
+ setDropdownOpen(false)
80
+ }}
81
+ className={`w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-white/5 transition-colors ${
82
+ r.statusCode === current.statusCode ? "bg-white/5" : ""
83
+ }`}
84
+ >
85
+ <span className={`text-xs font-mono font-bold px-1.5 py-0.5 rounded ${statusBadgeClass(r.statusCode)}`}>
86
+ {r.statusCode}
87
+ </span>
88
+ <span className="text-sm text-gray-300 truncate">{r.description}</span>
89
+ </button>
90
+ ))}
91
+ </div>
92
+ )}
93
+ </div>
94
+
95
+ <ExpandableCode code={json} />
96
+ </div>
97
+ )
98
+ }