mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages (#3388)
* feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages * multiline curl * random improvements * cleanup * update docs copy * fix build * cast * fix builg --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai> Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
Lakee Sivaraya
Vikhyath Mondreti
Vikhyath Mondreti
parent
ee20e119de
commit
79bb4e5ad8
@@ -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 && (
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden transition-all duration-200 ease-in-out',
|
||||
open ? 'max-h-[10000px] opacity-100' : 'max-h-0 opacity-0'
|
||||
'grid transition-[grid-template-rows,opacity] duration-200 ease-in-out',
|
||||
open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
|
||||
)}
|
||||
>
|
||||
{/* Mobile: simple indent */}
|
||||
<div className='ml-4 flex flex-col gap-0.5 lg:hidden'>{children}</div>
|
||||
{/* Desktop: styled with border */}
|
||||
<ul className='mt-0.5 ml-2 hidden space-y-[0.0625rem] border-gray-200/60 border-l pl-2.5 lg:block dark:border-gray-700/60'>
|
||||
{children}
|
||||
</ul>
|
||||
<div className='overflow-hidden'>
|
||||
{/* Mobile: simple indent */}
|
||||
<div className='ml-4 flex flex-col gap-0.5 lg:hidden'>{children}</div>
|
||||
{/* Desktop: styled with border */}
|
||||
<ul className='mt-0.5 ml-2 hidden space-y-[0.0625rem] border-gray-200/60 border-l pl-2.5 lg:block dark:border-gray-700/60'>
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ArrowRight, ChevronRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export function TOCFooter() {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
return (
|
||||
<div className='sticky bottom-0 mt-6'>
|
||||
<div className='flex flex-col gap-2 rounded-lg border border-border bg-secondary p-6 text-sm'>
|
||||
@@ -21,18 +18,19 @@ export function TOCFooter() {
|
||||
href='https://sim.ai/signup'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className='group mt-2 inline-flex h-8 w-fit items-center justify-center gap-1 whitespace-nowrap rounded-[10px] border border-[#2AAD6C] bg-gradient-to-b from-[#3ED990] to-[#2AAD6C] px-3 pr-[10px] pl-[12px] font-medium text-sm text-white shadow-[inset_0_2px_4px_0_#5EE8A8] outline-none transition-all hover:shadow-lg focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50'
|
||||
aria-label='Get started with Sim - Sign up for free'
|
||||
>
|
||||
<span>Get started</span>
|
||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||
{isHovered ? (
|
||||
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
||||
) : (
|
||||
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
||||
)}
|
||||
<span className='relative inline-flex h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||
<ChevronRight
|
||||
className='absolute inset-0 h-4 w-4 transition-opacity duration-200 group-hover:opacity-0'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<ArrowRight
|
||||
className='absolute inset-0 h-4 w-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
'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'
|
||||
|
||||
export function Navbar() {
|
||||
const pathname = usePathname()
|
||||
const isApiReference = pathname.includes('/api-reference')
|
||||
|
||||
return (
|
||||
<nav className='sticky top-0 z-50 border-border/50 border-b bg-background/80 backdrop-blur-md backdrop-saturate-150'>
|
||||
{/* Desktop: Single row layout */}
|
||||
@@ -31,16 +36,30 @@ export function Navbar() {
|
||||
</div>
|
||||
|
||||
{/* Right cluster aligns with TOC edge */}
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Link
|
||||
href='/introduction'
|
||||
className={cn(
|
||||
'rounded-xl px-3 py-2 font-normal text-[0.9375rem] leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground',
|
||||
!isApiReference ? 'text-foreground' : 'text-foreground/60'
|
||||
)}
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href='/api-reference/getting-started'
|
||||
className={cn(
|
||||
'rounded-xl px-3 py-2 font-normal text-[0.9375rem] leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground',
|
||||
isApiReference ? 'text-foreground' : 'text-foreground/60'
|
||||
)}
|
||||
>
|
||||
API
|
||||
</Link>
|
||||
<Link
|
||||
href='https://sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='rounded-xl px-3 py-2 font-normal text-[0.9375rem] text-foreground/60 leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground'
|
||||
style={{
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
}}
|
||||
>
|
||||
Platform
|
||||
</Link>
|
||||
|
||||
@@ -25,8 +25,8 @@ export function StructuredData({
|
||||
headline: title,
|
||||
description: description,
|
||||
url: url,
|
||||
datePublished: dateModified || new Date().toISOString(),
|
||||
dateModified: dateModified || new Date().toISOString(),
|
||||
...(dateModified && { datePublished: dateModified }),
|
||||
...(dateModified && { dateModified }),
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Sim Team',
|
||||
@@ -91,12 +91,6 @@ export function StructuredData({
|
||||
inLanguage: ['en', 'es', 'fr', 'de', 'ja', 'zh'],
|
||||
}
|
||||
|
||||
const faqStructuredData = title.toLowerCase().includes('faq') && {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: [],
|
||||
}
|
||||
|
||||
const softwareStructuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
@@ -151,15 +145,6 @@ export function StructuredData({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{faqStructuredData && (
|
||||
<Script
|
||||
id='faq-structured-data'
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(faqStructuredData),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{url === baseUrl && (
|
||||
<Script
|
||||
id='software-structured-data'
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ResponseSectionProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ResponseSection({ children }: ResponseSectionProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [statusCodes, setStatusCodes] = useState<string[]>([])
|
||||
const [selectedCode, setSelectedCode] = useState<string>('')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
function getAccordionItems() {
|
||||
const root = containerRef.current?.querySelector('[data-orientation="vertical"]')
|
||||
if (!root) return []
|
||||
return Array.from(root.children).filter(
|
||||
(el) => el.getAttribute('data-state') !== null
|
||||
) as HTMLElement[]
|
||||
}
|
||||
|
||||
function showStatusCode(code: string) {
|
||||
const items = getAccordionItems()
|
||||
for (const item of items) {
|
||||
const triggerBtn = item.querySelector('h3 button') as HTMLButtonElement | null
|
||||
const text = triggerBtn?.textContent?.trim() ?? ''
|
||||
const itemCode = text.match(/^\d{3}/)?.[0]
|
||||
|
||||
if (itemCode === code) {
|
||||
item.style.display = ''
|
||||
if (item.getAttribute('data-state') === 'closed' && triggerBtn) {
|
||||
triggerBtn.click()
|
||||
}
|
||||
} else {
|
||||
item.style.display = 'none'
|
||||
if (item.getAttribute('data-state') === 'open' && triggerBtn) {
|
||||
triggerBtn.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect when the fumadocs accordion children mount via MutationObserver,
|
||||
* then extract status codes and show the first one.
|
||||
* Replaces the previous approach that used `children` as a dependency
|
||||
* (which triggered on every render since children is a new object each time).
|
||||
*/
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
const initialize = () => {
|
||||
const items = getAccordionItems()
|
||||
if (items.length === 0) return false
|
||||
|
||||
const codes: string[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
for (const item of items) {
|
||||
const triggerBtn = item.querySelector('h3 button')
|
||||
if (triggerBtn) {
|
||||
const text = triggerBtn.textContent?.trim() ?? ''
|
||||
const code = text.match(/^\d{3}/)?.[0]
|
||||
if (code && !seen.has(code)) {
|
||||
seen.add(code)
|
||||
codes.push(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (codes.length > 0) {
|
||||
setStatusCodes(codes)
|
||||
setSelectedCode(codes[0])
|
||||
showStatusCode(codes[0])
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (initialize()) return
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (initialize()) {
|
||||
observer.disconnect()
|
||||
}
|
||||
})
|
||||
observer.observe(container, { childList: true, subtree: true })
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
function handleSelectCode(code: string) {
|
||||
setSelectedCode(code)
|
||||
setIsOpen(false)
|
||||
showStatusCode(code)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='response-section-wrapper'>
|
||||
{statusCodes.length > 0 && (
|
||||
<div className='response-section-header'>
|
||||
<h2 className='response-section-title'>Response</h2>
|
||||
<div className='response-section-meta'>
|
||||
<div ref={dropdownRef} className='response-section-dropdown-wrapper'>
|
||||
<button
|
||||
type='button'
|
||||
className='response-section-dropdown-trigger'
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span>{selectedCode}</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'response-section-chevron',
|
||||
isOpen && 'response-section-chevron-open'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className='response-section-dropdown-menu'>
|
||||
{statusCodes.map((code) => (
|
||||
<button
|
||||
key={code}
|
||||
type='button'
|
||||
className={cn(
|
||||
'response-section-dropdown-item',
|
||||
code === selectedCode && 'response-section-dropdown-item-selected'
|
||||
)}
|
||||
onClick={() => handleSelectCode(code)}
|
||||
>
|
||||
<span>{code}</span>
|
||||
{code === selectedCode && (
|
||||
<svg
|
||||
className='response-section-check'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
>
|
||||
<polyline points='20 6 9 17 4 12' />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className='response-section-content-type'>application/json</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='response-section-content'>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user