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.
- package/dist/index.js +4 -1
- package/package.json +4 -2
- package/template/app/[...slug]/page.tsx +27 -5
- package/template/app/api/feedback/route.ts +40 -0
- package/template/app/api-reference/[[...slug]]/page.tsx +165 -0
- package/template/app/home/page.tsx +6 -0
- package/template/components/ApiReference/ApiOverview.tsx +63 -0
- package/template/components/ApiReference/ApiSidebar.tsx +69 -0
- package/template/components/ApiReference/CodeSamples.tsx +186 -0
- package/template/components/ApiReference/EndpointPage.tsx +92 -0
- package/template/components/ApiReference/ExpandableCode.tsx +132 -0
- package/template/components/ApiReference/MethodBadge.tsx +29 -0
- package/template/components/ApiReference/ParamsTable.tsx +70 -0
- package/template/components/ApiReference/ResponseContext.tsx +32 -0
- package/template/components/ApiReference/ResponseDocs.tsx +135 -0
- package/template/components/ApiReference/ResponseExample.tsx +98 -0
- package/template/components/ApiReference/SchemaViewer.tsx +205 -0
- package/template/components/DocFeedback/DocFeedback.tsx +155 -0
- package/template/components/LastUpdated/LastUpdated.tsx +23 -0
- package/template/components/Nav/TopNav.tsx +14 -1
- package/template/components/PageNav/PageNav.tsx +2 -2
- package/template/lib/api-reference.ts +441 -0
- package/template/lib/api-types.ts +102 -0
- package/template/lib/api-utils.ts +62 -0
- package/template/lib/config-types.ts +16 -0
- package/template/lib/config.ts +12 -0
- package/template/lib/mdx.ts +27 -1
- package/template/lib/versions.ts +26 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { resolveRef } from "@/lib/api-utils"
|
|
5
|
+
import type { SchemaObject } from "@/lib/api-types"
|
|
6
|
+
|
|
7
|
+
// Correctly merges allOf items: shallow-merges scalar fields but additively merges
|
|
8
|
+
// `properties` and `required` so no allOf item's fields are lost.
|
|
9
|
+
function mergeAllOf(allOf: SchemaObject[], schemas: Record<string, SchemaObject>): SchemaObject {
|
|
10
|
+
const items = allOf.map((s) => (s.$ref ? (resolveRef(s.$ref, schemas) ?? s) : s))
|
|
11
|
+
const merged = Object.assign({} as SchemaObject, ...items)
|
|
12
|
+
const mergedProps: Record<string, SchemaObject> = {}
|
|
13
|
+
const mergedRequired: string[] = []
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
if (item.properties) Object.assign(mergedProps, item.properties)
|
|
16
|
+
if (item.required) mergedRequired.push(...item.required)
|
|
17
|
+
}
|
|
18
|
+
if (Object.keys(mergedProps).length) merged.properties = mergedProps
|
|
19
|
+
if (mergedRequired.length) merged.required = [...new Set(mergedRequired)]
|
|
20
|
+
return merged
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
schema: SchemaObject
|
|
25
|
+
schemas: Record<string, SchemaObject>
|
|
26
|
+
required?: boolean
|
|
27
|
+
name?: string
|
|
28
|
+
depth?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function TypeLabel({ schema }: { schema: SchemaObject }) {
|
|
32
|
+
if (schema.$ref) {
|
|
33
|
+
const refName = schema.$ref.replace("#/components/schemas/", "")
|
|
34
|
+
return <span className="text-xs font-mono text-purple-600 dark:text-purple-400">{refName}</span>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (schema.allOf?.length) {
|
|
38
|
+
return <span className="text-xs font-mono text-gray-500">object</span>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (schema.type === "array") {
|
|
42
|
+
const items = schema.items
|
|
43
|
+
if (items?.$ref) {
|
|
44
|
+
const refName = items.$ref.replace("#/components/schemas/", "")
|
|
45
|
+
return <span className="text-xs font-mono text-gray-500">array of <span className="text-purple-600 dark:text-purple-400">{refName}</span></span>
|
|
46
|
+
}
|
|
47
|
+
return <span className="text-xs font-mono text-gray-500">array{items?.type ? ` of ${items.type}` : ""}</span>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (schema.enum) {
|
|
51
|
+
return (
|
|
52
|
+
<span className="text-xs font-mono text-gray-500">
|
|
53
|
+
{schema.type ?? "string"}{" "}
|
|
54
|
+
<span className="text-gray-400">({schema.enum.map((v) => JSON.stringify(v)).join(" | ")})</span>
|
|
55
|
+
</span>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (schema.format) {
|
|
60
|
+
return <span className="text-xs font-mono text-gray-500">{schema.type} <span className="text-gray-400">({schema.format})</span></span>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return <span className="text-xs font-mono text-gray-500">{schema.type ?? "any"}</span>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function PropertyRow({
|
|
67
|
+
name,
|
|
68
|
+
schema,
|
|
69
|
+
required,
|
|
70
|
+
schemas,
|
|
71
|
+
depth,
|
|
72
|
+
}: {
|
|
73
|
+
name: string
|
|
74
|
+
schema: SchemaObject
|
|
75
|
+
required: boolean
|
|
76
|
+
schemas: Record<string, SchemaObject>
|
|
77
|
+
depth: number
|
|
78
|
+
}) {
|
|
79
|
+
const [expanded, setExpanded] = useState(depth < 1)
|
|
80
|
+
|
|
81
|
+
const resolved: SchemaObject = schema.$ref ? (resolveRef(schema.$ref, schemas) ?? schema) : schema
|
|
82
|
+
const mergedSchema: SchemaObject = resolved.allOf?.length
|
|
83
|
+
? mergeAllOf(resolved.allOf, schemas)
|
|
84
|
+
: resolved
|
|
85
|
+
|
|
86
|
+
const itemSchema = mergedSchema.type === "array" && mergedSchema.items
|
|
87
|
+
? (mergedSchema.items.$ref ? (resolveRef(mergedSchema.items.$ref, schemas) ?? mergedSchema.items) : mergedSchema.items)
|
|
88
|
+
: null
|
|
89
|
+
|
|
90
|
+
const childProperties =
|
|
91
|
+
mergedSchema.type === "object" || mergedSchema.properties
|
|
92
|
+
? mergedSchema.properties
|
|
93
|
+
: itemSchema?.type === "object" || itemSchema?.properties
|
|
94
|
+
? itemSchema.properties
|
|
95
|
+
: null
|
|
96
|
+
|
|
97
|
+
const hasChildren = depth < 4 && !!childProperties && Object.keys(childProperties).length > 0
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="border-b border-gray-100 dark:border-gray-800 last:border-0">
|
|
101
|
+
<div className="flex items-start gap-3 py-3 px-0">
|
|
102
|
+
<div className="flex-1 min-w-0">
|
|
103
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
104
|
+
<code className="text-sm font-mono font-medium text-gray-900 dark:text-gray-100">{name}</code>
|
|
105
|
+
<TypeLabel schema={schema} />
|
|
106
|
+
{required && (
|
|
107
|
+
<span className="text-[10px] font-medium text-red-500 uppercase tracking-wide">required</span>
|
|
108
|
+
)}
|
|
109
|
+
{(resolved.readOnly || mergedSchema.readOnly) && (
|
|
110
|
+
<span className="text-[10px] font-medium text-gray-400 uppercase tracking-wide">read-only</span>
|
|
111
|
+
)}
|
|
112
|
+
{(schema.deprecated || resolved.deprecated) && (
|
|
113
|
+
<span className="text-[10px] font-medium text-amber-500 uppercase tracking-wide">deprecated</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
{(resolved.description || mergedSchema.description) && (
|
|
117
|
+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
118
|
+
{resolved.description ?? mergedSchema.description}
|
|
119
|
+
</p>
|
|
120
|
+
)}
|
|
121
|
+
{resolved.default !== undefined && (
|
|
122
|
+
<p className="text-xs text-gray-400 mt-0.5">
|
|
123
|
+
Default: <code className="font-mono">{JSON.stringify(resolved.default)}</code>
|
|
124
|
+
</p>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{hasChildren && (
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => setExpanded((e) => !e)}
|
|
131
|
+
className="shrink-0 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 flex items-center gap-1 mt-0.5"
|
|
132
|
+
>
|
|
133
|
+
{expanded ? "Hide" : "Show"} fields
|
|
134
|
+
<svg
|
|
135
|
+
className={`w-3 h-3 transition-transform ${expanded ? "rotate-0" : "rotate-180"}`}
|
|
136
|
+
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}
|
|
137
|
+
>
|
|
138
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
|
|
139
|
+
</svg>
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{hasChildren && expanded && (
|
|
145
|
+
<div className="ml-4 border-l-2 border-gray-100 dark:border-gray-800 pl-4 mb-3">
|
|
146
|
+
{(Object.entries(childProperties!) as [string, SchemaObject][]).map(([childName, childSchema]) => (
|
|
147
|
+
<PropertyRow
|
|
148
|
+
key={childName}
|
|
149
|
+
name={childName}
|
|
150
|
+
schema={childSchema}
|
|
151
|
+
required={(mergedSchema.required ?? itemSchema?.required ?? []).includes(childName)}
|
|
152
|
+
schemas={schemas}
|
|
153
|
+
depth={depth + 1}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function SchemaViewer({ schema, schemas, required, name, depth = 0 }: Props) {
|
|
163
|
+
const resolved: SchemaObject = schema.$ref ? (resolveRef(schema.$ref, schemas) ?? schema) : schema
|
|
164
|
+
const mergedSchema: SchemaObject = resolved.allOf?.length
|
|
165
|
+
? mergeAllOf(resolved.allOf, schemas)
|
|
166
|
+
: resolved
|
|
167
|
+
|
|
168
|
+
const properties = mergedSchema.properties
|
|
169
|
+
const requiredFields = mergedSchema.required ?? []
|
|
170
|
+
|
|
171
|
+
if (name !== undefined) {
|
|
172
|
+
return (
|
|
173
|
+
<PropertyRow
|
|
174
|
+
name={name}
|
|
175
|
+
schema={schema}
|
|
176
|
+
required={required ?? false}
|
|
177
|
+
schemas={schemas}
|
|
178
|
+
depth={depth}
|
|
179
|
+
/>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!properties || Object.keys(properties).length === 0) {
|
|
184
|
+
return (
|
|
185
|
+
<div className="text-sm text-gray-500 dark:text-gray-400 py-2">
|
|
186
|
+
<TypeLabel schema={schema} />
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
|
193
|
+
{(Object.entries(properties) as [string, SchemaObject][]).map(([propName, propSchema]) => (
|
|
194
|
+
<PropertyRow
|
|
195
|
+
key={propName}
|
|
196
|
+
name={propName}
|
|
197
|
+
schema={propSchema}
|
|
198
|
+
required={requiredFields.includes(propName)}
|
|
199
|
+
schemas={schemas}
|
|
200
|
+
depth={depth}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { ThumbsUp, ThumbsDown } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
type Step = "idle" | "yes" | "no" | "yes-done" | "no-done"
|
|
7
|
+
|
|
8
|
+
const YES_REASONS = [
|
|
9
|
+
{ value: "accurate", label: "Accurate", description: "Accurately describes the product or feature." },
|
|
10
|
+
{ value: "solved", label: "Solved my issue", description: "Helped me resolve an issue." },
|
|
11
|
+
{ value: "clear", label: "Easy to understand", description: "Easy to follow and comprehend." },
|
|
12
|
+
{ value: "decided", label: "Helped me decide to use the product", description: "Convinced me to adopt the product or feature." },
|
|
13
|
+
{ value: "other", label: "Another reason", description: "" },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const NO_REASONS = [
|
|
17
|
+
{ value: "inaccurate", label: "Inaccurate", description: "Doesn't accurately describe the product or feature." },
|
|
18
|
+
{ value: "missing", label: "Couldn't find what I was looking for", description: "Missing important information." },
|
|
19
|
+
{ value: "unclear", label: "Hard to understand", description: "Too complicated or unclear." },
|
|
20
|
+
{ value: "code", label: "Code sample errors", description: "One or more code samples are incorrect." },
|
|
21
|
+
{ value: "other", label: "Another reason", description: "" },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
pageTitle: string
|
|
26
|
+
pageSlug: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DocFeedback({ pageTitle, pageSlug }: Props) {
|
|
30
|
+
const [step, setStep] = useState<Step>("idle")
|
|
31
|
+
const [reason, setReason] = useState("")
|
|
32
|
+
const [allowFollowUp, setAllowFollowUp] = useState(false)
|
|
33
|
+
const [submitting, setSubmitting] = useState(false)
|
|
34
|
+
|
|
35
|
+
function handleYes() {
|
|
36
|
+
if (step === "yes") { setStep("idle"); setReason(""); return }
|
|
37
|
+
setStep("yes")
|
|
38
|
+
setReason("")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleNo() {
|
|
42
|
+
if (step === "no") { setStep("idle"); setReason(""); return }
|
|
43
|
+
setStep("no")
|
|
44
|
+
setReason("")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function handleSubmit() {
|
|
48
|
+
setSubmitting(true)
|
|
49
|
+
try {
|
|
50
|
+
await fetch("/api/feedback", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
rating: step === "yes" ? "positive" : "negative",
|
|
55
|
+
reason,
|
|
56
|
+
allowFollowUp,
|
|
57
|
+
pageTitle,
|
|
58
|
+
pageSlug,
|
|
59
|
+
}),
|
|
60
|
+
})
|
|
61
|
+
} finally {
|
|
62
|
+
setStep(step === "yes" ? "yes-done" : "no-done")
|
|
63
|
+
setSubmitting(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const showPanel = step === "yes" || step === "no"
|
|
68
|
+
const reasons = step === "yes" ? YES_REASONS : NO_REASONS
|
|
69
|
+
const panelTitle = step === "yes" ? "What did you like?" : "What went wrong?"
|
|
70
|
+
|
|
71
|
+
const yesActive = step === "yes" || step === "yes-done"
|
|
72
|
+
const noActive = step === "no" || step === "no-done"
|
|
73
|
+
const done = step === "yes-done" || step === "no-done"
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="relative flex flex-col items-end gap-3">
|
|
77
|
+
{showPanel && (
|
|
78
|
+
<div className="absolute bottom-full right-0 mb-2 w-80 bg-white border border-gray-200 dark:bg-[#1a1d23] dark:border-gray-700 rounded-lg p-5 shadow-lg z-50">
|
|
79
|
+
<p className="text-sm font-semibold text-gray-900 dark:text-white mb-4">{panelTitle}</p>
|
|
80
|
+
<div className="flex flex-col gap-3">
|
|
81
|
+
{reasons.map((r) => (
|
|
82
|
+
<label key={r.value} className="flex items-start gap-3 cursor-pointer">
|
|
83
|
+
<input
|
|
84
|
+
type="radio"
|
|
85
|
+
name="feedback-reason"
|
|
86
|
+
value={r.value}
|
|
87
|
+
checked={reason === r.value}
|
|
88
|
+
onChange={() => setReason(r.value)}
|
|
89
|
+
className="mt-0.5 accent-blue-500"
|
|
90
|
+
/>
|
|
91
|
+
<div>
|
|
92
|
+
<span className="text-sm font-medium text-gray-800 dark:text-white">{r.label}</span>
|
|
93
|
+
{r.description && (
|
|
94
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{r.description}</p>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</label>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
101
|
+
<label className="flex items-start gap-2 cursor-pointer">
|
|
102
|
+
<input
|
|
103
|
+
type="checkbox"
|
|
104
|
+
checked={allowFollowUp}
|
|
105
|
+
onChange={(e) => setAllowFollowUp(e.target.checked)}
|
|
106
|
+
className="mt-0.5 accent-blue-500"
|
|
107
|
+
/>
|
|
108
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
109
|
+
Yes, it's okay to follow up by email.
|
|
110
|
+
</span>
|
|
111
|
+
</label>
|
|
112
|
+
</div>
|
|
113
|
+
<button
|
|
114
|
+
onClick={handleSubmit}
|
|
115
|
+
disabled={!reason || submitting}
|
|
116
|
+
className="mt-4 w-full py-2 text-sm text-gray-600 bg-gray-100 rounded-md disabled:opacity-40 disabled:cursor-not-allowed hover:enabled:bg-gray-200 hover:enabled:text-gray-900 dark:text-gray-400 dark:bg-gray-800 dark:hover:enabled:bg-gray-700 dark:hover:enabled:text-white transition-colors"
|
|
117
|
+
>
|
|
118
|
+
{submitting ? "Sending…" : "Feedback"}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<div className="flex items-center gap-3">
|
|
124
|
+
{done && (
|
|
125
|
+
<span className="text-sm text-gray-500 dark:text-gray-400">Thanks for your feedback!</span>
|
|
126
|
+
)}
|
|
127
|
+
{!done && <span className="text-sm text-gray-500 dark:text-gray-400">Was this page helpful?</span>}
|
|
128
|
+
<button
|
|
129
|
+
onClick={handleYes}
|
|
130
|
+
disabled={done}
|
|
131
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm border transition-colors disabled:cursor-default ${
|
|
132
|
+
yesActive
|
|
133
|
+
? "bg-green-50 border-green-400 text-green-600 dark:bg-green-900/40 dark:border-green-700 dark:text-green-400"
|
|
134
|
+
: "border-gray-300 text-gray-500 hover:border-gray-500 hover:text-gray-700 dark:border-gray-600 dark:text-gray-400 dark:hover:border-gray-400 dark:hover:text-gray-200"
|
|
135
|
+
}`}
|
|
136
|
+
>
|
|
137
|
+
<ThumbsUp size={14} strokeWidth={1.75} />
|
|
138
|
+
Yes
|
|
139
|
+
</button>
|
|
140
|
+
<button
|
|
141
|
+
onClick={handleNo}
|
|
142
|
+
disabled={done}
|
|
143
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm border transition-colors disabled:cursor-default ${
|
|
144
|
+
noActive
|
|
145
|
+
? "bg-red-50 border-red-400 text-red-600 dark:bg-red-900/40 dark:border-red-700 dark:text-red-400"
|
|
146
|
+
: "border-gray-300 text-gray-500 hover:border-gray-500 hover:text-gray-700 dark:border-gray-600 dark:text-gray-400 dark:hover:border-gray-400 dark:hover:text-gray-200"
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
<ThumbsDown size={14} strokeWidth={1.75} />
|
|
150
|
+
No
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Clock } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
date: Date
|
|
5
|
+
author?: string | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function LastUpdated({ date, author }: Props) {
|
|
9
|
+
const formatted = date.toLocaleDateString("en-US", {
|
|
10
|
+
month: "long",
|
|
11
|
+
day: "numeric",
|
|
12
|
+
year: "numeric",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
|
17
|
+
<Clock size={14} strokeWidth={1.75} />
|
|
18
|
+
<span>
|
|
19
|
+
{author ? `Last updated by ${author} on ${formatted}` : formatted}
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -19,9 +19,10 @@ type Props = {
|
|
|
19
19
|
currentVersionId: string | null
|
|
20
20
|
currentSlug: string
|
|
21
21
|
versionSlugs: Record<string, string[]>
|
|
22
|
+
apiRef?: { label: string; href: string; roles: string[] } | null
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export function TopNav({ nav, userRoles, userName, authEnabled = false, versions, currentVersionId, currentSlug, versionSlugs }: Props) {
|
|
25
|
+
export function TopNav({ nav, userRoles, userName, authEnabled = false, versions, currentVersionId, currentSlug, versionSlugs, apiRef }: Props) {
|
|
25
26
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null)
|
|
26
27
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
27
28
|
const navRef = useRef<HTMLElement>(null)
|
|
@@ -108,6 +109,18 @@ export function TopNav({ nav, userRoles, userName, authEnabled = false, versions
|
|
|
108
109
|
})}
|
|
109
110
|
</div>
|
|
110
111
|
|
|
112
|
+
{/* API Reference link */}
|
|
113
|
+
{apiRef && (apiRef.roles.length === 0 || apiRef.roles.some((r) => userRoles.includes(r))) && (
|
|
114
|
+
<Link
|
|
115
|
+
href={apiRef.href}
|
|
116
|
+
className={`hidden md:block text-sm font-medium transition-colors ${
|
|
117
|
+
currentSlug.startsWith("/api-reference") ? "text-[var(--cm-oasis-teal)]" : "text-[var(--cm-parchment)] hover:text-[var(--cm-oasis-teal)]"
|
|
118
|
+
}`}
|
|
119
|
+
>
|
|
120
|
+
{apiRef.label}
|
|
121
|
+
</Link>
|
|
122
|
+
)}
|
|
123
|
+
|
|
111
124
|
{/* Right side */}
|
|
112
125
|
<div className="ml-auto flex items-center gap-3">
|
|
113
126
|
<SearchTrigger />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Link from "next/link"
|
|
2
|
-
import type { NavGroup
|
|
2
|
+
import type { NavGroup } from "@/lib/nav-types"
|
|
3
3
|
|
|
4
4
|
type PageEntry = { label: string; slug: string }
|
|
5
5
|
|
|
@@ -39,7 +39,7 @@ export function PageNav({ activeGroup, currentSlug }: Props) {
|
|
|
39
39
|
if (!prev && !next) return null
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
|
-
<div className="flex justify-between items-center
|
|
42
|
+
<div className="flex justify-between items-center pt-6 border-t border-gray-200 dark:border-gray-800">
|
|
43
43
|
{prev ? (
|
|
44
44
|
<Link
|
|
45
45
|
href={prev.slug}
|