From d81055d98a27dc62182b47990e27484723423afb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Feb 2026 19:58:23 -0800 Subject: [PATCH 1/8] feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages --- apps/docs/app/[lang]/[[...slug]]/page.tsx | 135 +- apps/docs/app/[lang]/layout.tsx | 6 +- apps/docs/app/global.css | 679 +++++++ .../docs-layout/sidebar-components.tsx | 35 +- apps/docs/components/navbar/navbar.tsx | 37 +- apps/docs/components/ui/response-section.tsx | 169 ++ .../docs/de/api-reference/authentication.mdx | 95 + .../docs/de/api-reference/getting-started.mdx | 210 ++ .../content/docs/de/api-reference/meta.json | 16 + .../content/docs/de/api-reference/python.mdx | 766 +++++++ .../docs/de/api-reference/typescript.mdx | 1052 ++++++++++ apps/docs/content/docs/de/meta.json | 24 + .../docs/en/api-reference/authentication.mdx | 95 + .../docs/en/api-reference/getting-started.mdx | 210 ++ .../content/docs/en/api-reference/meta.json | 16 + .../content/docs/en/api-reference/python.mdx | 761 +++++++ .../docs/en/api-reference/typescript.mdx | 1035 ++++++++++ apps/docs/content/docs/en/meta.json | 1 - .../docs/es/api-reference/authentication.mdx | 95 + .../docs/es/api-reference/getting-started.mdx | 210 ++ .../content/docs/es/api-reference/meta.json | 16 + .../content/docs/es/api-reference/python.mdx | 766 +++++++ .../docs/es/api-reference/typescript.mdx | 1052 ++++++++++ apps/docs/content/docs/es/meta.json | 24 + .../docs/fr/api-reference/authentication.mdx | 95 + .../docs/fr/api-reference/getting-started.mdx | 210 ++ .../content/docs/fr/api-reference/meta.json | 16 + .../content/docs/fr/api-reference/python.mdx | 766 +++++++ .../docs/fr/api-reference/typescript.mdx | 1052 ++++++++++ apps/docs/content/docs/fr/meta.json | 24 + .../docs/ja/api-reference/authentication.mdx | 95 + .../docs/ja/api-reference/getting-started.mdx | 210 ++ .../content/docs/ja/api-reference/meta.json | 16 + .../content/docs/ja/api-reference/python.mdx | 766 +++++++ .../docs/ja/api-reference/typescript.mdx | 1052 ++++++++++ apps/docs/content/docs/ja/meta.json | 24 + .../docs/zh/api-reference/authentication.mdx | 95 + .../docs/zh/api-reference/getting-started.mdx | 210 ++ .../content/docs/zh/api-reference/meta.json | 16 + .../content/docs/zh/api-reference/python.mdx | 766 +++++++ .../docs/zh/api-reference/typescript.mdx | 1052 ++++++++++ apps/docs/content/docs/zh/meta.json | 24 + apps/docs/lib/openapi.ts | 135 ++ apps/docs/lib/source.ts | 92 +- apps/docs/openapi.json | 1806 +++++++++++++++++ apps/docs/package.json | 8 +- bun.lock | 164 +- 47 files changed, 16130 insertions(+), 69 deletions(-) create mode 100644 apps/docs/components/ui/response-section.tsx create mode 100644 apps/docs/content/docs/de/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/de/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/de/api-reference/meta.json create mode 100644 apps/docs/content/docs/de/api-reference/python.mdx create mode 100644 apps/docs/content/docs/de/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/de/meta.json create mode 100644 apps/docs/content/docs/en/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/en/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/en/api-reference/meta.json create mode 100644 apps/docs/content/docs/en/api-reference/python.mdx create mode 100644 apps/docs/content/docs/en/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/es/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/es/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/es/api-reference/meta.json create mode 100644 apps/docs/content/docs/es/api-reference/python.mdx create mode 100644 apps/docs/content/docs/es/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/es/meta.json create mode 100644 apps/docs/content/docs/fr/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/fr/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/fr/api-reference/meta.json create mode 100644 apps/docs/content/docs/fr/api-reference/python.mdx create mode 100644 apps/docs/content/docs/fr/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/fr/meta.json create mode 100644 apps/docs/content/docs/ja/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/ja/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/ja/api-reference/meta.json create mode 100644 apps/docs/content/docs/ja/api-reference/python.mdx create mode 100644 apps/docs/content/docs/ja/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/ja/meta.json create mode 100644 apps/docs/content/docs/zh/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/zh/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/zh/api-reference/meta.json create mode 100644 apps/docs/content/docs/zh/api-reference/python.mdx create mode 100644 apps/docs/content/docs/zh/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/zh/meta.json create mode 100644 apps/docs/lib/openapi.ts create mode 100644 apps/docs/openapi.json diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx index 0a4afc9818..cda16f502b 100644 --- a/apps/docs/app/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx @@ -1,5 +1,6 @@ import type React from 'react' import { findNeighbour } from 'fumadocs-core/page-tree' +import { createAPIPage } from 'fumadocs-openapi/ui' import { Pre } from 'fumadocs-ui/components/codeblock' import defaultMdxComponents from 'fumadocs-ui/mdx' import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page' @@ -12,22 +13,65 @@ import { LLMCopyButton } from '@/components/page-actions' import { StructuredData } from '@/components/structured-data' import { CodeBlock } from '@/components/ui/code-block' import { Heading } from '@/components/ui/heading' +import { ResponseSection } from '@/components/ui/response-section' +import { getApiSpecContent, openapi } from '@/lib/openapi' import { type PageData, source } from '@/lib/source' +const SUPPORTED_LANGUAGES = new Set(['en', 'es', 'fr', 'de', 'ja', 'zh']) + +const APIPage = createAPIPage(openapi, { + playground: { enabled: false }, + content: { + renderOperationLayout: async (slots) => { + return ( +
+
+ {slots.header} + {slots.apiPlayground} + {slots.description} + {slots.authSchemes &&
{slots.authSchemes}
} + {slots.paremeters} + {slots.body &&
{slots.body}
} + {slots.responses} + {slots.callbacks} +
+
+ {slots.apiExample} +
+
+ ) + }, + }, +}) + export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) { const params = await props.params - const page = source.getPage(params.slug, params.lang) + const isValidLang = SUPPORTED_LANGUAGES.has(params.lang) + const lang = isValidLang ? params.lang : 'en' + const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])] + const page = source.getPage(slug, lang) if (!page) notFound() - const data = page.data as PageData - const MDX = data.body + const data = page.data as PageData & { + _openapi?: { method?: string } + getAPIPageProps?: () => any + } + const isOpenAPI = '_openapi' in data && data._openapi != null + const isApiReference = slug?.some((s) => s === 'api-reference') ?? false const baseUrl = 'https://docs.sim.ai' - const markdownContent = await data.getText('processed') const pageTreeRecord = source.pageTree as Record const pageTree = pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0] - const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null + const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null + const neighbours = isApiReference + ? { + previous: rawNeighbours?.previous?.url.includes('/api-reference/') + ? rawNeighbours.previous + : undefined, + next: rawNeighbours?.next?.url.includes('/api-reference/') ? rawNeighbours.next : undefined, + } + : rawNeighbours const generateBreadcrumbs = () => { const breadcrumbs: Array<{ name: string; url: string }> = [ @@ -169,6 +213,62 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l ) + if (isOpenAPI && data.getAPIPageProps) { + const apiProps = data.getAPIPageProps() + const apiPageContent = getApiSpecContent( + data.title, + data.description, + apiProps.operations ?? [] + ) + + return ( + <> + + , + }} + > +
+
+
+ +
+ +
+ {data.title} + {data.description} +
+ + + +
+ + ) + } + + const MDX = (data as PageData).body + const markdownContent = await (data as PageData).getText('processed') + return ( <> }) { const params = await props.params - const page = source.getPage(params.slug, params.lang) + const isValidLang = SUPPORTED_LANGUAGES.has(params.lang) + const lang = isValidLang ? params.lang : 'en' + const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])] + const page = source.getPage(slug, lang) if (!page) notFound() const data = page.data as PageData @@ -286,10 +389,10 @@ export async function generateMetadata(props: { url: fullUrl, siteName: 'Sim Documentation', type: 'article', - locale: params.lang === 'en' ? 'en_US' : `${params.lang}_${params.lang.toUpperCase()}`, + locale: lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`, alternateLocale: ['en', 'es', 'fr', 'de', 'ja', 'zh'] - .filter((lang) => lang !== params.lang) - .map((lang) => (lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`)), + .filter((l) => l !== lang) + .map((l) => (l === 'en' ? 'en_US' : `${l}_${l.toUpperCase()}`)), images: [ { url: ogImageUrl, @@ -323,13 +426,13 @@ export async function generateMetadata(props: { alternates: { canonical: fullUrl, languages: { - 'x-default': `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`, - en: `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`, - es: `${baseUrl}/es${page.url.replace(`/${params.lang}`, '')}`, - fr: `${baseUrl}/fr${page.url.replace(`/${params.lang}`, '')}`, - de: `${baseUrl}/de${page.url.replace(`/${params.lang}`, '')}`, - ja: `${baseUrl}/ja${page.url.replace(`/${params.lang}`, '')}`, - zh: `${baseUrl}/zh${page.url.replace(`/${params.lang}`, '')}`, + 'x-default': `${baseUrl}${page.url.replace(`/${lang}`, '')}`, + en: `${baseUrl}${page.url.replace(`/${lang}`, '')}`, + es: `${baseUrl}/es${page.url.replace(`/${lang}`, '')}`, + fr: `${baseUrl}/fr${page.url.replace(`/${lang}`, '')}`, + de: `${baseUrl}/de${page.url.replace(`/${lang}`, '')}`, + ja: `${baseUrl}/ja${page.url.replace(`/${lang}`, '')}`, + zh: `${baseUrl}/zh${page.url.replace(`/${lang}`, '')}`, }, }, } diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx index d76a11f103..ac177505a5 100644 --- a/apps/docs/app/[lang]/layout.tsx +++ b/apps/docs/app/[lang]/layout.tsx @@ -55,8 +55,11 @@ type LayoutProps = { params: Promise<{ lang: string }> } +const SUPPORTED_LANGUAGES = new Set(['en', 'es', 'fr', 'de', 'ja', 'zh']) + export default async function Layout({ children, params }: LayoutProps) { - const { lang } = await params + const { lang: rawLang } = await params + const lang = SUPPORTED_LANGUAGES.has(rawLang) ? rawLang : 'en' const structuredData = { '@context': 'https://schema.org', @@ -107,6 +110,7 @@ export default async function Layout({ children, params }: LayoutProps) { title: , }} sidebar={{ + tabs: false, defaultOpenLevel: 0, collapsible: false, footer: null, diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index 70ec578bf9..030cc2ccb0 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -1,6 +1,7 @@ @import "tailwindcss"; @import "fumadocs-ui/css/neutral.css"; @import "fumadocs-ui/css/preset.css"; +@import "fumadocs-openapi/css/preset.css"; /* Prevent overscroll bounce effect on the page */ html, @@ -8,6 +9,11 @@ body { overscroll-behavior: none; } +/* Reserve scrollbar space to prevent layout jitter between pages */ +html { + scrollbar-gutter: stable; +} + @theme { --color-fd-primary: #33c482; /* Green from Sim logo */ --font-geist-sans: var(--font-geist-sans); @@ -576,6 +582,679 @@ video { border-style: solid !important; } +/* ============================================================ + API Reference Pages — Mintlify-style overrides + ============================================================ */ + +/* OpenAPI pages: span main + TOC grid columns for wide two-column layout. + The grid has columns: spacer | sidebar | main | toc | spacer. + By spanning columns 3-4, the article fills both main and toc areas, + while the grid structure stays identical to non-OpenAPI pages (no jitter). */ +#nd-page:has(.api-page-header) { + grid-column: 3 / span 2 !important; + max-width: 1400px !important; +} + +/* Hide the empty TOC aside on OpenAPI pages so it doesn't overlay content */ +#nd-docs-layout:has(#nd-page .api-page-header) #nd-toc { + display: none; +} + +/* Hide the default "Response Body" heading rendered by fumadocs-openapi */ +.response-section-wrapper > .response-section-content > h2, +.response-section-wrapper > .response-section-content > h3 { + display: none !important; +} + +/* Hide default accordion triggers (status code rows) — we show our own dropdown */ +.response-section-wrapper [data-orientation="vertical"] > [data-state] > h3 { + display: none !important; +} + +/* Ensure API reference pages use the same font as the rest of the docs */ +#nd-page:has(.api-page-header), +#nd-page:has(.api-page-header) h2, +#nd-page:has(.api-page-header) h3, +#nd-page:has(.api-page-header) h4, +#nd-page:has(.api-page-header) p, +#nd-page:has(.api-page-header) span, +#nd-page:has(.api-page-header) div, +#nd-page:has(.api-page-header) label, +#nd-page:has(.api-page-header) button { + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +/* Method badge pills in page content — colored background pills */ +#nd-page span.font-mono.font-medium[class*="text-green"] { + background-color: rgb(220 252 231 / 0.6); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; +} +html.dark #nd-page span.font-mono.font-medium[class*="text-green"] { + background-color: rgb(34 197 94 / 0.15); +} + +#nd-page span.font-mono.font-medium[class*="text-blue"] { + background-color: rgb(219 234 254 / 0.6); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; +} +html.dark #nd-page span.font-mono.font-medium[class*="text-blue"] { + background-color: rgb(59 130 246 / 0.15); +} + +#nd-page span.font-mono.font-medium[class*="text-orange"] { + background-color: rgb(255 237 213 / 0.6); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; +} +html.dark #nd-page span.font-mono.font-medium[class*="text-orange"] { + background-color: rgb(249 115 22 / 0.15); +} + +#nd-page span.font-mono.font-medium[class*="text-red"] { + background-color: rgb(254 226 226 / 0.6); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; +} +html.dark #nd-page span.font-mono.font-medium[class*="text-red"] { + background-color: rgb(239 68 68 / 0.15); +} + +/* Sidebar links with method badges — flex for vertical centering */ +#nd-sidebar a:has(span.font-mono.font-medium) { + display: flex !important; + align-items: center !important; + gap: 6px; +} + +/* Sidebar method badges — ensure proper inline flex display */ +#nd-sidebar a span.font-mono.font-medium { + display: inline-flex; + align-items: center; + font-size: 10px !important; + line-height: 1 !important; + padding: 2.5px 4px; + border-radius: 3px; + flex-shrink: 0; +} + +/* Sidebar GET badges */ +#nd-sidebar a span.font-mono.font-medium[class*="text-green"] { + background-color: rgb(220 252 231 / 0.6); +} +html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-green"] { + background-color: rgb(34 197 94 / 0.15); +} + +/* Sidebar POST badges */ +#nd-sidebar a span.font-mono.font-medium[class*="text-blue"] { + background-color: rgb(219 234 254 / 0.6); +} +html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-blue"] { + background-color: rgb(59 130 246 / 0.15); +} + +/* Sidebar PUT badges */ +#nd-sidebar a span.font-mono.font-medium[class*="text-orange"] { + background-color: rgb(255 237 213 / 0.6); +} +html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-orange"] { + background-color: rgb(249 115 22 / 0.15); +} + +/* Sidebar DELETE badges */ +#nd-sidebar a span.font-mono.font-medium[class*="text-red"] { + background-color: rgb(254 226 226 / 0.6); +} +html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-red"] { + background-color: rgb(239 68 68 / 0.15); +} + +/* Code block containers — match regular docs styling */ +#nd-page:has(.api-page-header) figure.shiki { + border-radius: 0.75rem !important; + background-color: var(--color-fd-card) !important; +} + +/* Hide "Filter Properties" search bar everywhere — main page and popovers */ +input[placeholder="Filter Properties"] { + display: none !important; +} +div:has(> input[placeholder="Filter Properties"]) { + display: none !important; +} +/* Remove top border on first visible property after hidden Filter Properties */ +div:has(> input[placeholder="Filter Properties"]) + .text-sm.border-t { + border-top: none !important; +} + +/* Hide "TypeScript Definitions" copy panel on API pages */ +#nd-page:has(.api-page-header) div.not-prose.rounded-xl.border.p-3.mb-4 { + display: none !important; +} +#nd-page:has(.api-page-header) div.not-prose.rounded-xl.border.p-3:has(> div > p.font-medium) { + display: none !important; +} + +/* Hide info tags (Format, Default, etc.) everywhere — main page and popovers */ +div.flex.flex-row.gap-2.flex-wrap.not-prose:has(> div.bg-fd-secondary) { + display: none !important; +} +div.flex.flex-row.items-start.bg-fd-secondary.border.rounded-lg.text-xs { + display: none !important; +} + +/* Method+path bar — cleaner, lighter styling like Gumloop. + Override bg-fd-card CSS variable directly for reliability. */ +#nd-page:has(.api-page-header) div.flex.flex-row.items-center.rounded-xl.border.not-prose { + --color-fd-card: rgb(249 250 251) !important; + background-color: rgb(249 250 251) !important; + border-color: rgb(229 231 235) !important; +} +html.dark + #nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose { + --color-fd-card: rgb(24 24 27) !important; + background-color: rgb(24 24 27) !important; + border-color: rgb(63 63 70) !important; +} +/* Method badge inside path bar — cleaner sans-serif, softer colors */ +#nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium { + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif !important; + font-weight: 600 !important; + font-size: 0.6875rem !important; + letter-spacing: 0.025em; + text-transform: uppercase; +} +/* POST — softer blue */ +#nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium[class*="text-blue"] { + color: rgb(37 99 235) !important; + background-color: rgb(219 234 254 / 0.7) !important; +} +html.dark + #nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium[class*="text-blue"] { + color: rgb(96 165 250) !important; + background-color: rgb(59 130 246 / 0.15) !important; +} +/* GET — softer green */ +#nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium[class*="text-green"] { + color: rgb(22 163 74) !important; + background-color: rgb(220 252 231 / 0.7) !important; +} +html.dark + #nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium[class*="text-green"] { + color: rgb(74 222 128) !important; + background-color: rgb(34 197 94 / 0.15) !important; +} + +/* Path text inside method+path bar — monospace, bright like Gumloop */ +#nd-page:has(.api-page-header) div.flex.flex-row.items-center.rounded-xl.border.not-prose code { + color: rgb(55 65 81) !important; + background: none !important; + border: none !important; + padding: 0 !important; + font-size: 0.8125rem !important; +} +html.dark + #nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + code { + color: rgb(229 231 235) !important; +} + +/* Inline code in API pages — neutral color instead of red. + Exclude code inside the method+path bar (handled above). */ +#nd-page:has(.api-page-header) .prose :not(pre) > code { + color: rgb(79 70 229) !important; +} +html.dark #nd-page:has(.api-page-header) .prose :not(pre) > code { + color: rgb(165 180 252) !important; +} + +/* Response Section — custom dropdown-based rendering (Mintlify style) */ + +/* Hide divider lines between accordion items */ +.response-section-wrapper [data-orientation="vertical"].divide-y > * { + border-top-width: 0 !important; + border-bottom-width: 0 !important; +} +.response-section-wrapper [data-orientation="vertical"].divide-y { + border-top: none !important; +} + +/* Remove content type labels inside accordion items (we show one in the header) */ +.response-section-wrapper [data-orientation="vertical"] p.not-prose:has(code.text-xs) { + display: none !important; +} + +/* Hide the top-level response description (e.g. "Execution was successfully cancelled.") + but NOT field descriptions inside Schema which also use prose-no-margin. + The response description is a direct child of AccordionContent (role=region) with mb-2. */ +.response-section-wrapper [data-orientation="vertical"] [role="region"] > .prose-no-margin.mb-2, +.response-section-wrapper + [data-orientation="vertical"] + [role="region"] + > div + > .prose-no-margin.mb-2 { + display: none !important; +} + +/* Remove left padding on accordion content so it aligns with Path Parameters */ +.response-section-wrapper [data-orientation="vertical"] [role="region"] { + padding-inline-start: 0 !important; +} + +/* Response section header */ +.response-section-header { + display: flex; + align-items: center; + gap: 1rem; + margin-top: 1.75rem; + margin-bottom: 0.5rem; +} + +.response-section-title { + font-size: 1.5rem; + font-weight: 600; + margin: 0; + color: var(--color-fd-foreground); + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, sans-serif; +} + +.response-section-meta { + display: flex; + align-items: center; + gap: 0.75rem; + margin-left: auto; +} + +/* Status code dropdown */ +.response-section-dropdown-wrapper { + position: relative; +} + +.response-section-dropdown-trigger { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.125rem 0.25rem; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-fd-muted-foreground); + background: none; + border: none; + cursor: pointer; + border-radius: 0.25rem; + transition: color 0.15s; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +.response-section-dropdown-trigger:hover { + color: var(--color-fd-foreground); +} + +.response-section-chevron { + width: 0.75rem; + height: 0.75rem; + transition: transform 0.15s; +} +.response-section-chevron-open { + transform: rotate(180deg); +} + +.response-section-dropdown-menu { + position: absolute; + top: calc(100% + 0.25rem); + left: 0; + z-index: 50; + min-width: 5rem; + background-color: white; + border: 1px solid rgb(229 231 235); + border-radius: 0.5rem; + box-shadow: + 0 4px 6px -1px rgb(0 0 0 / 0.1), + 0 2px 4px -2px rgb(0 0 0 / 0.1); + padding: 0.25rem; + overflow: hidden; +} +html.dark .response-section-dropdown-menu { + background-color: rgb(24 24 27); + border-color: rgb(63 63 70); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3); +} + +.response-section-dropdown-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0.375rem 0.5rem; + font-size: 0.875rem; + color: var(--color-fd-muted-foreground); + background: none; + border: none; + cursor: pointer; + border-radius: 0.25rem; + transition: + background-color 0.1s, + color 0.1s; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +.response-section-dropdown-item:hover { + background-color: rgb(243 244 246); + color: var(--color-fd-foreground); +} +html.dark .response-section-dropdown-item:hover { + background-color: rgb(39 39 42); +} +.response-section-dropdown-item-selected { + color: var(--color-fd-foreground); +} + +.response-section-check { + width: 0.875rem; + height: 0.875rem; +} + +.response-section-content-type { + font-size: 0.875rem; + color: var(--color-fd-muted-foreground); + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} + +/* Response schema container — remove border to match Path Parameters style */ +.response-section-wrapper [data-orientation="vertical"] .border.px-3.py-2.rounded-lg { + border: none !important; + padding: 0 !important; + border-radius: 0 !important; + background-color: transparent; +} + +/* Property row — reorder: name (1) → type badge (2) → required badge (3) */ +#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose { + display: flex; + flex-wrap: wrap; + align-items: center; +} + +/* Name span — order 1 */ +#nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose + > span.font-medium.font-mono.text-fd-primary { + order: 1; +} + +/* Type badge — order 2, grey pill like Mintlify */ +#nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground { + order: 2; + background-color: rgb(235 235 238); + color: rgb(90 90 99); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark + #nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground { + background-color: rgb(39 39 42); + color: rgb(212 212 216); +} + +/* Hide the "*" inside the name span — we'll add "required" as a ::after on the flex row */ +#nd-page:has(.api-page-header) span.font-medium.font-mono.text-fd-primary > span.text-red-400 { + display: none; +} + +/* Required badge — order 3, added as ::after on the flex row when it contains a required field */ +#nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after { + content: "required"; + order: 3; + display: inline-flex; + align-items: center; + background-color: rgb(254 226 226); + color: rgb(220 38 38); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark + #nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after { + background-color: rgb(127 29 29 / 0.3); + color: rgb(252 165 165); +} + +/* Optional "?" indicator — hide it */ +#nd-page:has(.api-page-header) + span.font-medium.font-mono.text-fd-primary + > span.text-fd-muted-foreground { + display: none; +} + +/* Hide the auth scheme type label (e.g. "apiKey") next to Authorization heading */ +#nd-page:has(.api-page-header) .flex.items-start.justify-between.gap-2 > div.not-prose { + display: none !important; +} + +/* Auth property — replace "" with "string" badge, add "header" and "required" badges. + Auth properties use my-4 (vs py-4 for regular properties). */ + +/* Auth property flex row — name: order 1, type: order 2, ::before "header": order 3, ::after "required": order 4 */ +#nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose + > span.font-medium.font-mono.text-fd-primary { + order: 1; +} +#nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground { + order: 2; + font-size: 0; + padding: 0 !important; + background: none !important; + line-height: 0; +} +#nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground::after { + content: "string"; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + background-color: rgb(235 235 238); + color: rgb(90 90 99); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + display: inline-flex; + align-items: center; +} +html.dark + #nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground::after { + background-color: rgb(39 39 42); + color: rgb(212 212 216); +} + +/* "header" badge via ::before on the auth flex row */ +#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::before { + content: "header"; + order: 3; + display: inline-flex; + align-items: center; + background-color: rgb(235 235 238); + color: rgb(90 90 99); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark + #nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose::before { + background-color: rgb(39 39 42); + color: rgb(212 212 216); +} + +/* "required" badge via ::after on the auth flex row */ +#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::after { + content: "required"; + order: 4; + display: inline-flex; + align-items: center; + background-color: rgb(254 226 226); + color: rgb(220 38 38); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark + #nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose::after { + background-color: rgb(127 29 29 / 0.3); + color: rgb(252 165 165); +} + +/* Hide "In: header" text below auth property — redundant with the header badge */ +#nd-page:has(.api-page-header) div.my-4 .prose-no-margin p:has(> code) { + display: none !important; +} + +/* Section dividers — bottom border after Authorization and Body sections. */ +.api-section-divider { + padding-bottom: 0.5rem; + border-bottom: 1px solid rgb(229 231 235 / 0.6); +} +html.dark .api-section-divider { + border-bottom-color: rgb(255 255 255 / 0.07); +} + +/* Property rows — breathing room like Mintlify. + Regular properties use border-t py-4; auth properties use border-t my-4. */ +#nd-page:has(.api-page-header) .text-sm.border-t.py-4 { + padding-top: 1.25rem !important; + padding-bottom: 1.25rem !important; +} +#nd-page:has(.api-page-header) .text-sm.border-t.my-4 { + margin-top: 1.25rem !important; + margin-bottom: 1.25rem !important; + padding-top: 1.25rem; +} + +/* Divider lines between fields — very subtle like Mintlify */ +#nd-page:has(.api-page-header) .text-sm.border-t { + border-color: rgb(229 231 235 / 0.6); +} +html.dark #nd-page:has(.api-page-header) .text-sm.border-t { + border-color: rgb(255 255 255 / 0.07); +} + +/* Body/Callback section "application/json" label — remove inline code styling */ +#nd-page:has(.api-page-header) .flex.gap-2.items-center.justify-between p.not-prose code.text-xs, +#nd-page:has(.api-page-header) .flex.justify-between.gap-2.items-end p.not-prose code.text-xs { + background: none !important; + border: none !important; + padding: 0 !important; + color: var(--color-fd-muted-foreground) !important; + font-size: 0.875rem !important; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif !important; +} + +/* Object/array type triggers in property rows — order 2 + badge chip styling */ +#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > button, +#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > span:has(> button) { + order: 2; + background-color: rgb(235 235 238); + color: rgb(90 90 99); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark #nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > button, +html.dark + #nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose + > span:has(> button) { + background-color: rgb(39 39 42); + color: rgb(212 212 216); +} + +/* Section headings (Authorization, Path Parameters, etc.) — consistent top spacing */ +#nd-page:has(.api-page-header) .min-w-0.flex-1 h2 { + margin-top: 1.75rem !important; + margin-bottom: 0.25rem !important; +} + +/* Code examples in right column — wrap long lines instead of horizontal scroll */ +#nd-page:has(.api-page-header) pre { + white-space: pre-wrap !important; + word-break: break-all !important; +} +#nd-page:has(.api-page-header) pre code { + width: 100% !important; + word-break: break-all !important; + overflow-wrap: break-word !important; +} + +/* API page header — constrain title/copy-page to left content column, not full width. + Only applies on OpenAPI pages (which have the two-column layout). */ +@media (min-width: 1280px) { + .api-page-header { + max-width: calc(100% - 400px - 1.5rem); + } +} + +/* Footer navigation — constrain to left content column on OpenAPI pages only. + Target pages that contain the two-column layout via :has() selector. */ +#nd-page:has(.api-page-header) > div:last-child { + max-width: calc(100% - 400px - 1.5rem); +} +@media (max-width: 1024px) { + #nd-page:has(.api-page-header) > div:last-child { + max-width: 100%; + } +} + /* Tailwind v4 content sources */ @source '../app/**/*.{js,ts,jsx,tsx,mdx}'; @source '../components/**/*.{js,ts,jsx,tsx,mdx}'; diff --git a/apps/docs/components/docs-layout/sidebar-components.tsx b/apps/docs/components/docs-layout/sidebar-components.tsx index e6fbe18cd1..7bd6039f8d 100644 --- a/apps/docs/components/docs-layout/sidebar-components.tsx +++ b/apps/docs/components/docs-layout/sidebar-components.tsx @@ -52,15 +52,26 @@ export function SidebarItem({ item }: { item: Item }) { ) } +function isApiReferenceFolder(node: Folder): boolean { + if (node.index?.url.includes('/api-reference/')) return true + for (const child of node.children) { + if (child.type === 'page' && child.url.includes('/api-reference/')) return true + if (child.type === 'folder' && isApiReferenceFolder(child)) return true + } + return false +} + export function SidebarFolder({ item, children }: { item: Folder; children: ReactNode }) { const pathname = usePathname() const hasActiveChild = checkHasActiveChild(item, pathname) + const isApiRef = isApiReferenceFolder(item) + const isOnApiRefPage = stripLangPrefix(pathname).startsWith('/api-reference') const hasChildren = item.children.length > 0 - const [open, setOpen] = useState(hasActiveChild) + const [open, setOpen] = useState(hasActiveChild || (isApiRef && isOnApiRefPage)) useEffect(() => { - setOpen(hasActiveChild) - }, [hasActiveChild]) + setOpen(hasActiveChild || (isApiRef && isOnApiRefPage)) + }, [hasActiveChild, isApiRef, isOnApiRefPage]) const active = item.index ? isActive(item.index.url, pathname, false) : false @@ -157,16 +168,18 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac {hasChildren && (
- {/* Mobile: simple indent */} -
{children}
- {/* Desktop: styled with border */} -
    - {children} -
+
+ {/* Mobile: simple indent */} +
{children}
+ {/* Desktop: styled with border */} +
    + {children} +
+
)} diff --git a/apps/docs/components/navbar/navbar.tsx b/apps/docs/components/navbar/navbar.tsx index db82c69062..b8edd5ec55 100644 --- a/apps/docs/components/navbar/navbar.tsx +++ b/apps/docs/components/navbar/navbar.tsx @@ -1,12 +1,22 @@ 'use client' import Link from 'next/link' +import { usePathname } from 'next/navigation' import { LanguageDropdown } from '@/components/ui/language-dropdown' import { SearchTrigger } from '@/components/ui/search-trigger' import { SimLogoFull } from '@/components/ui/sim-logo' import { ThemeToggle } from '@/components/ui/theme-toggle' +import { cn } from '@/lib/utils' + +const navLinkStyle = { + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', +} export function Navbar() { + const pathname = usePathname() + const isApiReference = pathname.startsWith('/api-reference') + return (