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,212 @@
|
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
import { extractTableOfContents, getAdjacentDocs, isCategoryPage } from "../lib/mdx"
|
|
3
|
+
import { getCachedVersions, getCachedAllDocs, getCachedDocBySlug } from "../lib/mdx-cache"
|
|
4
|
+
import { DocLayout } from "../components/docs/doc-layout"
|
|
5
|
+
import { TableOfContents } from "../components/docs/table-of-contents"
|
|
6
|
+
import { Header } from "../components/docs/header"
|
|
7
|
+
import { DocLayoutWrapper } from "../components/docs/doc-layout-wrapper"
|
|
8
|
+
import { HotReloadIndicator } from "../components/docs/hot-reload-indicator"
|
|
9
|
+
import { DevModeBadge } from "../components/docs/dev-mode-badge"
|
|
10
|
+
import { MdxHotReload } from "../components/docs/mdx-hot-reload"
|
|
11
|
+
import { CategoryIndex } from "../components/docs/category-index"
|
|
12
|
+
import { NotFoundContent } from "../components/docs/not-found-content"
|
|
13
|
+
import { getConfig } from "../lib/config"
|
|
14
|
+
import { Suspense } from "react"
|
|
15
|
+
import { DocLoading } from "../components/docs/doc-loading"
|
|
16
|
+
|
|
17
|
+
interface PageProps {
|
|
18
|
+
params: Promise<{
|
|
19
|
+
version: string
|
|
20
|
+
slug: string[]
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate metadata for documentation pages
|
|
26
|
+
* This is exported so users can re-export it from their page.tsx
|
|
27
|
+
*/
|
|
28
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
29
|
+
const { version, slug: slugArray } = await params
|
|
30
|
+
const slug = slugArray.join("/")
|
|
31
|
+
|
|
32
|
+
const doc = await getCachedDocBySlug(slug, version)
|
|
33
|
+
|
|
34
|
+
if (!doc) {
|
|
35
|
+
return {
|
|
36
|
+
title: "Page Not Found",
|
|
37
|
+
description: "The requested documentation page could not be found.",
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const title = doc.meta.title || doc.title
|
|
42
|
+
const description = doc.meta.description || `Documentation for ${title}`
|
|
43
|
+
const url = `/docs/${version}/${slug}`
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
title: `${title}`,
|
|
47
|
+
description,
|
|
48
|
+
openGraph: {
|
|
49
|
+
title,
|
|
50
|
+
description,
|
|
51
|
+
url,
|
|
52
|
+
siteName: "Documentation Platform",
|
|
53
|
+
type: "article",
|
|
54
|
+
locale: "en_US",
|
|
55
|
+
},
|
|
56
|
+
twitter: {
|
|
57
|
+
card: "summary_large_image",
|
|
58
|
+
title,
|
|
59
|
+
description,
|
|
60
|
+
},
|
|
61
|
+
alternates: {
|
|
62
|
+
canonical: url,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate static params for all documentation pages
|
|
69
|
+
* This enables static generation at build time
|
|
70
|
+
*/
|
|
71
|
+
export async function generateStaticParams() {
|
|
72
|
+
const versions = getCachedVersions()
|
|
73
|
+
const params = []
|
|
74
|
+
|
|
75
|
+
for (const version of versions) {
|
|
76
|
+
const docs = await getCachedAllDocs(version)
|
|
77
|
+
for (const doc of docs) {
|
|
78
|
+
// Add the custom slug path
|
|
79
|
+
params.push({
|
|
80
|
+
version,
|
|
81
|
+
slug: doc.slug.split("/").filter(Boolean),
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return params
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Documentation page component
|
|
91
|
+
* Handles:
|
|
92
|
+
* - Regular documentation pages
|
|
93
|
+
* - Category index pages (with or without content)
|
|
94
|
+
* - 404 pages (when doc not found)
|
|
95
|
+
*/
|
|
96
|
+
export default async function DocPage({ params }: PageProps) {
|
|
97
|
+
const { version, slug: slugArray } = await params
|
|
98
|
+
const slug = slugArray.join("/")
|
|
99
|
+
|
|
100
|
+
const allDocs = await getCachedAllDocs(version)
|
|
101
|
+
const versions = getCachedVersions()
|
|
102
|
+
const config = getConfig()
|
|
103
|
+
const isCategory = isCategoryPage(slug, allDocs)
|
|
104
|
+
|
|
105
|
+
// Try to get the doc (might be index.mdx or regular .mdx)
|
|
106
|
+
const doc = await getCachedDocBySlug(slug, version)
|
|
107
|
+
|
|
108
|
+
// If no doc found and it's a category, show category index
|
|
109
|
+
if (!doc && isCategory) {
|
|
110
|
+
// Find a doc in this category to get the tab group
|
|
111
|
+
const categoryDoc = allDocs.find((d) => d.slug.startsWith(slug + "/"))
|
|
112
|
+
const categoryTabGroup = categoryDoc?.meta?.tab_group || categoryDoc?.categoryTabGroup
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<DocLayoutWrapper
|
|
117
|
+
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
118
|
+
docs={allDocs}
|
|
119
|
+
version={version}
|
|
120
|
+
content={
|
|
121
|
+
<CategoryIndex
|
|
122
|
+
categoryPath={slug}
|
|
123
|
+
version={version}
|
|
124
|
+
allDocs={allDocs}
|
|
125
|
+
title={slug.split("/").pop()?.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()) || "Category"}
|
|
126
|
+
description="Browse the documentation in this section."
|
|
127
|
+
config={config}
|
|
128
|
+
/>
|
|
129
|
+
}
|
|
130
|
+
toc={<div />}
|
|
131
|
+
config={config}
|
|
132
|
+
currentPageTabGroup={categoryTabGroup}
|
|
133
|
+
/>
|
|
134
|
+
<MdxHotReload />
|
|
135
|
+
<HotReloadIndicator />
|
|
136
|
+
<DevModeBadge />
|
|
137
|
+
</>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If no doc found, render 404 content within the layout (keeps sidebar visible)
|
|
142
|
+
if (!doc) {
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<Suspense fallback={<DocLoading />}>
|
|
146
|
+
<DocLayoutWrapper
|
|
147
|
+
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
148
|
+
docs={allDocs}
|
|
149
|
+
version={version}
|
|
150
|
+
content={<NotFoundContent version={version} />}
|
|
151
|
+
toc={<div />}
|
|
152
|
+
config={config}
|
|
153
|
+
currentPageTabGroup={undefined}
|
|
154
|
+
/>
|
|
155
|
+
<MdxHotReload />
|
|
156
|
+
<HotReloadIndicator />
|
|
157
|
+
<DevModeBadge />
|
|
158
|
+
</Suspense>
|
|
159
|
+
</>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const toc = extractTableOfContents(doc.content)
|
|
164
|
+
const { previous, next } = getAdjacentDocs(slug, allDocs)
|
|
165
|
+
|
|
166
|
+
// If doc exists but is also a category, show both content and children
|
|
167
|
+
const showCategoryIndex = isCategory && doc
|
|
168
|
+
|
|
169
|
+
// Get current page's tab group from doc metadata or category
|
|
170
|
+
const currentPageTabGroup = doc.meta?.tab_group || doc.categoryTabGroup
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<>
|
|
174
|
+
<Suspense fallback={<DocLoading />}>
|
|
175
|
+
<DocLayoutWrapper
|
|
176
|
+
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
177
|
+
docs={allDocs}
|
|
178
|
+
version={version}
|
|
179
|
+
content={
|
|
180
|
+
showCategoryIndex ? (
|
|
181
|
+
<CategoryIndex
|
|
182
|
+
categoryPath={slug}
|
|
183
|
+
version={version}
|
|
184
|
+
allDocs={allDocs}
|
|
185
|
+
title={doc.meta.title}
|
|
186
|
+
description={doc.meta.description}
|
|
187
|
+
content={doc.content}
|
|
188
|
+
config={config}
|
|
189
|
+
/>
|
|
190
|
+
) : (
|
|
191
|
+
<DocLayout
|
|
192
|
+
meta={doc.meta}
|
|
193
|
+
content={doc.content}
|
|
194
|
+
previousDoc={previous ? { title: previous.meta.title, slug: previous.slug } : undefined}
|
|
195
|
+
nextDoc={next ? { title: next.meta.title, slug: next.slug } : undefined}
|
|
196
|
+
version={version}
|
|
197
|
+
slug={slug}
|
|
198
|
+
config={config}
|
|
199
|
+
/>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
toc={showCategoryIndex ? <div /> : <TableOfContents items={toc} config={config} />}
|
|
203
|
+
config={config}
|
|
204
|
+
currentPageTabGroup={currentPageTabGroup}
|
|
205
|
+
/>
|
|
206
|
+
<MdxHotReload />
|
|
207
|
+
<HotReloadIndicator />
|
|
208
|
+
<DevModeBadge />
|
|
209
|
+
</Suspense>
|
|
210
|
+
</>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
import type { Metadata } from "next"
|
|
3
|
+
import { Geist, Geist_Mono } from "next/font/google"
|
|
4
|
+
import { getConfig } from "../lib/config"
|
|
5
|
+
import { getAssetPath } from "../lib/utils"
|
|
6
|
+
import { TabProvider } from "../components/docs/tab-context"
|
|
7
|
+
import "../styles/globals.css"
|
|
8
|
+
|
|
9
|
+
const geist = Geist({ subsets: ["latin"] })
|
|
10
|
+
const geistMono = Geist_Mono({ subsets: ["latin"] })
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate metadata for the root layout
|
|
14
|
+
* This can be imported and used by the user's app/layout.tsx
|
|
15
|
+
*/
|
|
16
|
+
export function generateMetadata(): Metadata {
|
|
17
|
+
const config = getConfig()
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
title: {
|
|
21
|
+
default: config.site.title,
|
|
22
|
+
template: `%s | ${config.site.title}`,
|
|
23
|
+
},
|
|
24
|
+
description: config.site.description || "Modern documentation platform",
|
|
25
|
+
generator: "Specra Documentation",
|
|
26
|
+
metadataBase: config.site.url ? new URL(config.site.url) : undefined,
|
|
27
|
+
icons: {
|
|
28
|
+
icon: getAssetPath(config.site.favicon ?? "") ? [
|
|
29
|
+
{
|
|
30
|
+
url: getAssetPath(config.site.favicon ?? ""),
|
|
31
|
+
},
|
|
32
|
+
] : [],
|
|
33
|
+
apple: getAssetPath("/apple-icon.png"),
|
|
34
|
+
},
|
|
35
|
+
openGraph: {
|
|
36
|
+
title: config.site.title,
|
|
37
|
+
description: config.site.description,
|
|
38
|
+
url: config.site.url,
|
|
39
|
+
siteName: config.site.title,
|
|
40
|
+
locale: config.site.language || "en",
|
|
41
|
+
type: "website",
|
|
42
|
+
},
|
|
43
|
+
twitter: {
|
|
44
|
+
card: "summary_large_image",
|
|
45
|
+
title: config.site.title,
|
|
46
|
+
description: config.site.description,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const metadata: Metadata = generateMetadata()
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Root layout component for Specra documentation sites
|
|
55
|
+
* This provides the HTML structure and global providers
|
|
56
|
+
*/
|
|
57
|
+
export default function RootLayout({
|
|
58
|
+
children,
|
|
59
|
+
}: Readonly<{
|
|
60
|
+
children: React.ReactNode
|
|
61
|
+
}>) {
|
|
62
|
+
const config = getConfig()
|
|
63
|
+
const defaultTab = config.navigation?.tabGroups?.[0]?.id || ""
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<html lang={config.site.language || "en"} suppressHydrationWarning>
|
|
67
|
+
<body className={`${geist.className} font-sans antialiased`}>
|
|
68
|
+
<TabProvider defaultTab={defaultTab}>
|
|
69
|
+
{children}
|
|
70
|
+
</TabProvider>
|
|
71
|
+
</body>
|
|
72
|
+
</html>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ChevronDown } from "lucide-react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
|
|
6
|
+
interface AccordionItemProps {
|
|
7
|
+
title: string | React.ReactNode
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
defaultOpen?: boolean
|
|
10
|
+
value?: string // For compatibility with radix-ui style API
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AccordionItem({ title, children, defaultOpen = false }: AccordionItemProps) {
|
|
14
|
+
const [isOpen, setIsOpen] = useState(defaultOpen)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="border border-border rounded-xl overflow-hidden mb-2">
|
|
18
|
+
<button
|
|
19
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
20
|
+
className="w-full flex items-center justify-between p-4 text-left bg-muted/30 hover:bg-muted/50 transition-colors"
|
|
21
|
+
>
|
|
22
|
+
<span className="font-medium text-foreground">{title}</span>
|
|
23
|
+
<ChevronDown
|
|
24
|
+
className={`h-5 w-5 text-muted-foreground transition-transform ${
|
|
25
|
+
isOpen ? "rotate-180" : ""
|
|
26
|
+
}`}
|
|
27
|
+
/>
|
|
28
|
+
</button>
|
|
29
|
+
{isOpen && (
|
|
30
|
+
<div className="p-4 border-t border-border bg-background">
|
|
31
|
+
<div className="prose prose-sm dark:prose-invert max-w-none [&>*:last-child]:mb-0">
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface AccordionProps {
|
|
41
|
+
children: React.ReactNode
|
|
42
|
+
type?: "single" | "multiple"
|
|
43
|
+
collapsible?: boolean // For compatibility with radix-ui style API
|
|
44
|
+
className?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Accordion({ children, type = "multiple", className }: AccordionProps) {
|
|
48
|
+
return (
|
|
49
|
+
<div className={className || "my-6 space-y-2"}>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, useState } from "react"
|
|
4
|
+
import { ChevronDown } from "lucide-react"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
interface ApiEndpointProps {
|
|
8
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
|
|
9
|
+
path: string
|
|
10
|
+
summary?: string
|
|
11
|
+
children?: ReactNode
|
|
12
|
+
defaultOpen?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const methodColors = {
|
|
16
|
+
GET: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
|
|
17
|
+
POST: "bg-green-500/10 text-green-600 dark:text-green-400",
|
|
18
|
+
PUT: "bg-orange-500/10 text-orange-600 dark:text-orange-400",
|
|
19
|
+
PATCH: "bg-purple-500/10 text-purple-600 dark:text-purple-400",
|
|
20
|
+
DELETE: "bg-red-500/10 text-red-600 dark:text-red-400",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ApiEndpoint({ method, path, summary, children, defaultOpen = false }: ApiEndpointProps) {
|
|
24
|
+
const [isOpen, setIsOpen] = useState(defaultOpen)
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="not-prose mb-4 rounded-xl border border-border overflow-hidden">
|
|
28
|
+
{/* Accordion Header */}
|
|
29
|
+
<button
|
|
30
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
31
|
+
className="w-full flex items-center gap-3 px-4 py-3 text-left bg-muted/30 hover:bg-muted/50 transition-colors"
|
|
32
|
+
>
|
|
33
|
+
<span
|
|
34
|
+
className={cn(
|
|
35
|
+
"text-xs font-semibold px-2 py-0.5 rounded",
|
|
36
|
+
methodColors[method]
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
{method}
|
|
40
|
+
</span>
|
|
41
|
+
<code className="text-sm font-mono">{path}</code>
|
|
42
|
+
{summary && <span className="text-sm text-muted-foreground ml-auto mr-2">{summary}</span>}
|
|
43
|
+
<ChevronDown
|
|
44
|
+
className={cn(
|
|
45
|
+
"h-5 w-5 text-muted-foreground transition-transform flex-shrink-0",
|
|
46
|
+
isOpen ? "rotate-180" : ""
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
{/* Accordion Content */}
|
|
52
|
+
{isOpen && children && (
|
|
53
|
+
<div className="border-t border-border bg-background">
|
|
54
|
+
<div className="px-4 py-4 space-y-6">{children}</div>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
interface ApiParam {
|
|
2
|
+
name: string
|
|
3
|
+
type: string
|
|
4
|
+
required?: boolean
|
|
5
|
+
description?: string
|
|
6
|
+
default?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ApiParamsProps {
|
|
10
|
+
title?: string
|
|
11
|
+
params: ApiParam[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ApiParams({ title = "Parameters", params }: ApiParamsProps) {
|
|
15
|
+
if (!params || params.length === 0) return null
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="mb-6">
|
|
19
|
+
<h4 className="text-sm font-semibold text-foreground mb-3">{title}</h4>
|
|
20
|
+
<div className="space-y-3">
|
|
21
|
+
{params.map((param) => (
|
|
22
|
+
<div key={param.name} className="flex flex-col gap-1">
|
|
23
|
+
<div className="flex items-center gap-2">
|
|
24
|
+
<code className="text-sm font-mono text-foreground">{param.name}</code>
|
|
25
|
+
<span className="text-xs text-muted-foreground">{param.type}</span>
|
|
26
|
+
{param.required && (
|
|
27
|
+
<span className="text-xs text-red-600 dark:text-red-400">required</span>
|
|
28
|
+
)}
|
|
29
|
+
{param.default && (
|
|
30
|
+
<span className="text-xs text-muted-foreground">
|
|
31
|
+
default: <code className="text-xs">{param.default}</code>
|
|
32
|
+
</span>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
{param.description && (
|
|
36
|
+
<p className="text-sm text-muted-foreground">{param.description}</p>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react"
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { Input } from "@/components/ui/input"
|
|
6
|
+
import { Textarea } from "@/components/ui/textarea"
|
|
7
|
+
import { Badge } from "@/components/ui/badge"
|
|
8
|
+
import { CodeBlock } from "../code-block"
|
|
9
|
+
import { Play, Loader2 } from "lucide-react"
|
|
10
|
+
|
|
11
|
+
interface PathParam {
|
|
12
|
+
name: string
|
|
13
|
+
type: string
|
|
14
|
+
example?: any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ApiPlaygroundProps {
|
|
18
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
|
|
19
|
+
path: string
|
|
20
|
+
baseUrl?: string
|
|
21
|
+
headers?: Record<string, string>
|
|
22
|
+
defaultBody?: string
|
|
23
|
+
pathParams?: PathParam[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ApiPlayground({
|
|
27
|
+
method,
|
|
28
|
+
path,
|
|
29
|
+
baseUrl = "",
|
|
30
|
+
headers = {},
|
|
31
|
+
defaultBody,
|
|
32
|
+
pathParams = []
|
|
33
|
+
}: ApiPlaygroundProps) {
|
|
34
|
+
const [loading, setLoading] = useState(false)
|
|
35
|
+
const [response, setResponse] = useState<any>(null)
|
|
36
|
+
const [error, setError] = useState<string | null>(null)
|
|
37
|
+
const [requestBody, setRequestBody] = useState(defaultBody || "")
|
|
38
|
+
|
|
39
|
+
// Initialize headers with empty strings if not provided
|
|
40
|
+
const initialHeaders = useMemo(() => {
|
|
41
|
+
const cleanHeaders: Record<string, string> = {}
|
|
42
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
43
|
+
cleanHeaders[key] = value || ""
|
|
44
|
+
})
|
|
45
|
+
return cleanHeaders
|
|
46
|
+
}, [headers])
|
|
47
|
+
|
|
48
|
+
const [requestHeaders, setRequestHeaders] = useState(JSON.stringify(initialHeaders, null, 2))
|
|
49
|
+
|
|
50
|
+
// Extract path parameters and initialize with defaults
|
|
51
|
+
const extractedParams = useMemo(() => {
|
|
52
|
+
const params: Record<string, string> = {}
|
|
53
|
+
const pathParamPattern = /:(\w+)/g
|
|
54
|
+
let match
|
|
55
|
+
|
|
56
|
+
while ((match = pathParamPattern.exec(path)) !== null) {
|
|
57
|
+
const paramName = match[1]
|
|
58
|
+
const paramConfig = pathParams.find(p => p.name === paramName)
|
|
59
|
+
|
|
60
|
+
// Set default value based on example or type
|
|
61
|
+
if (paramConfig?.example !== undefined) {
|
|
62
|
+
params[paramName] = String(paramConfig.example)
|
|
63
|
+
} else if (paramConfig?.type === "number") {
|
|
64
|
+
params[paramName] = "1"
|
|
65
|
+
} else {
|
|
66
|
+
params[paramName] = ""
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return params
|
|
71
|
+
}, [path, pathParams])
|
|
72
|
+
|
|
73
|
+
const [pathParamValues, setPathParamValues] = useState<Record<string, string>>(extractedParams)
|
|
74
|
+
|
|
75
|
+
// Build the final URL with path params replaced
|
|
76
|
+
const buildUrl = () => {
|
|
77
|
+
let finalPath = path
|
|
78
|
+
Object.entries(pathParamValues).forEach(([key, value]) => {
|
|
79
|
+
finalPath = finalPath.replace(`:${key}`, value)
|
|
80
|
+
})
|
|
81
|
+
return `${baseUrl}${finalPath}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const handleSend = async () => {
|
|
85
|
+
setLoading(true)
|
|
86
|
+
setError(null)
|
|
87
|
+
setResponse(null)
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const url = buildUrl()
|
|
91
|
+
const parsedHeaders = JSON.parse(requestHeaders)
|
|
92
|
+
|
|
93
|
+
const options: RequestInit = {
|
|
94
|
+
method,
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
...parsedHeaders,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (method !== "GET" && method !== "DELETE" && requestBody) {
|
|
102
|
+
options.body = requestBody
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const res = await fetch(url, options)
|
|
106
|
+
const data = await res.json()
|
|
107
|
+
|
|
108
|
+
setResponse({
|
|
109
|
+
status: res.status,
|
|
110
|
+
statusText: res.statusText,
|
|
111
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
112
|
+
body: data,
|
|
113
|
+
})
|
|
114
|
+
} catch (err) {
|
|
115
|
+
setError(err instanceof Error ? err.message : "An error occurred")
|
|
116
|
+
} finally {
|
|
117
|
+
setLoading(false)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="not-prose border border-border rounded-lg overflow-hidden bg-card/30">
|
|
123
|
+
<div className="bg-muted/50 px-4 py-2 border-b border-border">
|
|
124
|
+
<h4 className="text-sm font-semibold text-foreground">API Playground</h4>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="p-4 space-y-4">
|
|
128
|
+
{/* Path Parameters */}
|
|
129
|
+
{Object.keys(pathParamValues).length > 0 && (
|
|
130
|
+
<div>
|
|
131
|
+
<label className="text-xs font-semibold text-muted-foreground mb-2 block">
|
|
132
|
+
Path Parameters
|
|
133
|
+
</label>
|
|
134
|
+
<div className="space-y-2">
|
|
135
|
+
{Object.entries(pathParamValues).map(([paramName, paramValue]) => {
|
|
136
|
+
const paramConfig = pathParams.find(p => p.name === paramName)
|
|
137
|
+
return (
|
|
138
|
+
<div key={paramName} className="flex items-center gap-2">
|
|
139
|
+
<span className="text-xs text-muted-foreground min-w-[80px]">
|
|
140
|
+
:{paramName}
|
|
141
|
+
</span>
|
|
142
|
+
<Input
|
|
143
|
+
value={paramValue}
|
|
144
|
+
onChange={(e) =>
|
|
145
|
+
setPathParamValues((prev) => ({ ...prev, [paramName]: e.target.value }))
|
|
146
|
+
}
|
|
147
|
+
placeholder={paramConfig?.example || paramConfig?.type || "value"}
|
|
148
|
+
className="font-mono text-sm"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* URL */}
|
|
158
|
+
<div>
|
|
159
|
+
<label className="text-xs font-semibold text-muted-foreground mb-2 block">
|
|
160
|
+
Request URL
|
|
161
|
+
</label>
|
|
162
|
+
<div className="flex items-center gap-2">
|
|
163
|
+
<Badge variant="outline" className="font-mono">
|
|
164
|
+
{method}
|
|
165
|
+
</Badge>
|
|
166
|
+
<Input value={buildUrl()} readOnly className="font-mono text-sm" />
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Headers */}
|
|
171
|
+
<div>
|
|
172
|
+
<label className="text-xs font-semibold text-muted-foreground mb-2 block">
|
|
173
|
+
Headers (JSON)
|
|
174
|
+
</label>
|
|
175
|
+
<Textarea
|
|
176
|
+
value={requestHeaders}
|
|
177
|
+
onChange={(e) => setRequestHeaders(e.target.value)}
|
|
178
|
+
className="font-mono text-sm"
|
|
179
|
+
rows={4}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Body (for POST, PUT, PATCH) */}
|
|
184
|
+
{method !== "GET" && method !== "DELETE" && (
|
|
185
|
+
<div>
|
|
186
|
+
<label className="text-xs font-semibold text-muted-foreground mb-2 block">
|
|
187
|
+
Request Body (JSON)
|
|
188
|
+
</label>
|
|
189
|
+
<Textarea
|
|
190
|
+
value={requestBody}
|
|
191
|
+
onChange={(e) => setRequestBody(e.target.value)}
|
|
192
|
+
className="font-mono text-sm"
|
|
193
|
+
rows={6}
|
|
194
|
+
placeholder='{\n "key": "value"\n}'
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* Send Button */}
|
|
200
|
+
<Button onClick={handleSend} disabled={loading} className="w-full">
|
|
201
|
+
{loading ? (
|
|
202
|
+
<>
|
|
203
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
204
|
+
Sending...
|
|
205
|
+
</>
|
|
206
|
+
) : (
|
|
207
|
+
<>
|
|
208
|
+
<Play className="mr-2 h-4 w-4" />
|
|
209
|
+
Send Request
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
</Button>
|
|
213
|
+
|
|
214
|
+
{/* Response */}
|
|
215
|
+
{response && (
|
|
216
|
+
<div className="mt-4">
|
|
217
|
+
<label className="text-xs font-semibold text-muted-foreground mb-2 block">
|
|
218
|
+
Response ({response.status} {response.statusText})
|
|
219
|
+
</label>
|
|
220
|
+
<CodeBlock code={JSON.stringify(response.body, null, 2)} language="json" />
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Error */}
|
|
225
|
+
{error && (
|
|
226
|
+
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/20 rounded-md">
|
|
227
|
+
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
}
|