feat (v1.0.0): initial refactor and redesign
This commit is contained in:
parent
3058aa1ab4
commit
fe9b50b30e
134 changed files with 17792 additions and 3670 deletions
257
components/docs/APIEndpointDoc.tsx
Normal file
257
components/docs/APIEndpointDoc.tsx
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import { colors } from '@/lib/theme'
|
||||
import type { APIEndpoint } from '@/lib/docs/types'
|
||||
import CodeBlock from './CodeBlock'
|
||||
import { LuLock } from 'react-icons/lu'
|
||||
|
||||
interface APIEndpointDocProps {
|
||||
endpoint: APIEndpoint
|
||||
className?: string
|
||||
}
|
||||
|
||||
const methodStyles = {
|
||||
GET: {
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
color: colors.accents.success,
|
||||
borderColor: 'rgba(16, 185, 129, 0.3)',
|
||||
},
|
||||
POST: {
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
color: colors.accents.info,
|
||||
borderColor: 'rgba(59, 130, 246, 0.3)',
|
||||
},
|
||||
PUT: {
|
||||
backgroundColor: colors.accents.warningBg,
|
||||
color: colors.accents.warning,
|
||||
borderColor: 'rgba(245, 158, 11, 0.3)',
|
||||
},
|
||||
DELETE: {
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||||
color: colors.accents.error,
|
||||
borderColor: 'rgba(239, 68, 68, 0.3)',
|
||||
},
|
||||
PATCH: {
|
||||
backgroundColor: 'rgba(168, 85, 247, 0.1)',
|
||||
color: '#a855f7',
|
||||
borderColor: 'rgba(168, 85, 247, 0.3)',
|
||||
},
|
||||
} as const
|
||||
|
||||
export default function APIEndpointDoc({
|
||||
endpoint,
|
||||
className,
|
||||
}: APIEndpointDocProps) {
|
||||
return (
|
||||
<div id={endpoint.id} className={cn('scroll-mt-20', className)}>
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="rounded-md border px-3 py-1 text-sm font-bold"
|
||||
style={methodStyles[endpoint.method]}
|
||||
>
|
||||
{endpoint.method}
|
||||
</span>
|
||||
<code className="text-lg font-mono" style={{ color: colors.text.secondary }}>
|
||||
{endpoint.path}
|
||||
</code>
|
||||
</div>
|
||||
<p className="leading-relaxed" style={{ color: colors.text.body }}>{endpoint.description}</p>
|
||||
{endpoint.auth?.required && (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm"
|
||||
style={{
|
||||
borderColor: 'rgba(245, 158, 11, 0.3)',
|
||||
backgroundColor: colors.accents.warningBg,
|
||||
color: colors.accents.warning,
|
||||
}}
|
||||
>
|
||||
<LuLock className="h-4 w-4" />
|
||||
<span>
|
||||
Authentication required
|
||||
{endpoint.auth.type && `: ${endpoint.auth.type}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Query Parameters */}
|
||||
{endpoint.parameters?.query && endpoint.parameters.query.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.body }}>
|
||||
Query Parameters
|
||||
</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b" style={{ borderColor: colors.borders.default }}>
|
||||
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Name
|
||||
</th>
|
||||
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{endpoint.parameters.query.map((param, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b last:border-0"
|
||||
style={{ borderColor: colors.borders.subtle }}
|
||||
>
|
||||
<td className="px-4 py-3 font-mono" style={{ color: colors.text.secondary }}>
|
||||
{param.name}
|
||||
{!param.optional && (
|
||||
<span className="ml-1" style={{ color: colors.accents.error }}>*</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono" style={{ color: colors.text.muted }}>
|
||||
{param.type}
|
||||
</td>
|
||||
<td className="px-4 py-3" style={{ color: colors.text.body }}>
|
||||
{param.description}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Request Body */}
|
||||
{endpoint.parameters?.body && endpoint.parameters.body.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.body }}>Request Body</h4>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b" style={{ borderColor: colors.borders.default }}>
|
||||
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Field
|
||||
</th>
|
||||
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{endpoint.parameters.body.map((param, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b last:border-0"
|
||||
style={{ borderColor: colors.borders.subtle }}
|
||||
>
|
||||
<td className="px-4 py-3 font-mono" style={{ color: colors.text.secondary }}>
|
||||
{param.name}
|
||||
{!param.optional && (
|
||||
<span className="ml-1" style={{ color: colors.accents.error }}>*</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono" style={{ color: colors.text.muted }}>
|
||||
{param.type}
|
||||
</td>
|
||||
<td className="px-4 py-3" style={{ color: colors.text.body }}>
|
||||
{param.description}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Responses */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.body }}>Responses</h4>
|
||||
{endpoint.responses.map((response, index) => {
|
||||
const isSuccess = response.status >= 200 && response.status < 300
|
||||
const isError = response.status >= 400
|
||||
const statusStyle = isSuccess
|
||||
? { backgroundColor: 'rgba(16, 185, 129, 0.1)', color: colors.accents.success }
|
||||
: isError
|
||||
? { backgroundColor: 'rgba(239, 68, 68, 0.1)', color: colors.accents.error }
|
||||
: { backgroundColor: 'rgba(59, 130, 246, 0.1)', color: colors.accents.info }
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-2 rounded-lg border p-4"
|
||||
style={{
|
||||
borderColor: colors.borders.default,
|
||||
backgroundColor: colors.backgrounds.card,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="rounded px-2 py-1 text-sm font-mono font-semibold"
|
||||
style={statusStyle}
|
||||
>
|
||||
{response.status}
|
||||
</span>
|
||||
<span className="text-sm" style={{ color: colors.text.body }}>
|
||||
{response.description}
|
||||
</span>
|
||||
</div>
|
||||
{response.example && (
|
||||
<CodeBlock
|
||||
code={JSON.stringify(response.example, null, 2)}
|
||||
language="json"
|
||||
title="Example Response"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Examples */}
|
||||
{endpoint.examples && endpoint.examples.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.body }}>
|
||||
Request Examples
|
||||
</h4>
|
||||
{endpoint.examples.map((example, index) => (
|
||||
<div key={index} className="space-y-3">
|
||||
{example.title && (
|
||||
<h5 className="text-sm font-medium" style={{ color: colors.text.muted }}>
|
||||
{example.title}
|
||||
</h5>
|
||||
)}
|
||||
<div className="grid gap-3 lg:grid-cols-2">
|
||||
<CodeBlock
|
||||
code={
|
||||
typeof example.request === 'string'
|
||||
? example.request
|
||||
: JSON.stringify(example.request, null, 2)
|
||||
}
|
||||
language="bash"
|
||||
title="Request"
|
||||
/>
|
||||
<CodeBlock
|
||||
code={
|
||||
typeof example.response === 'string'
|
||||
? example.response
|
||||
: JSON.stringify(example.response, null, 2)
|
||||
}
|
||||
language="json"
|
||||
title="Response"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
198
components/docs/CodeBlock.tsx
Normal file
198
components/docs/CodeBlock.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { colors, effects } from '@/lib/theme'
|
||||
import { Copy, Check } from 'lucide-react'
|
||||
|
||||
/**
|
||||
* Supported syntax highlighting languages for code blocks.
|
||||
*
|
||||
* @remarks
|
||||
* This list includes the most commonly used languages in the codebase.
|
||||
* Languages are validated and normalized to ensure proper syntax highlighting.
|
||||
*/
|
||||
const SUPPORTED_LANGUAGES = [
|
||||
'typescript',
|
||||
'javascript',
|
||||
'tsx',
|
||||
'jsx',
|
||||
'ts',
|
||||
'js',
|
||||
'json',
|
||||
'bash',
|
||||
'shell',
|
||||
'css',
|
||||
'scss',
|
||||
'html',
|
||||
'markdown',
|
||||
'yaml',
|
||||
'sql',
|
||||
] as const
|
||||
|
||||
type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number]
|
||||
|
||||
/**
|
||||
* Normalizes language identifiers to their canonical forms.
|
||||
*
|
||||
* @param language - Raw language identifier from code fence
|
||||
* @returns Normalized language identifier for syntax highlighting
|
||||
*
|
||||
* @remarks
|
||||
* **Normalization rules:**
|
||||
* - 'ts' → 'typescript'
|
||||
* - 'js' → 'javascript'
|
||||
* - Invalid languages → 'typescript' (safe default)
|
||||
* - All other valid languages → unchanged
|
||||
*
|
||||
* This ensures consistent syntax highlighting even when JSDoc
|
||||
* examples use shorthand language identifiers.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* normalizeLanguage('ts') // Returns: 'typescript'
|
||||
* normalizeLanguage('tsx') // Returns: 'tsx'
|
||||
* normalizeLanguage('invalid') // Returns: 'typescript'
|
||||
* ```
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function normalizeLanguage(language: string): SupportedLanguage {
|
||||
const normalized = language.toLowerCase()
|
||||
|
||||
// Map common shorthands to full names
|
||||
if (normalized === 'ts') return 'typescript'
|
||||
if (normalized === 'js') return 'javascript'
|
||||
|
||||
// Validate against supported languages
|
||||
if (SUPPORTED_LANGUAGES.includes(normalized as SupportedLanguage)) {
|
||||
return normalized as SupportedLanguage
|
||||
}
|
||||
|
||||
// Default to typescript for unknown languages
|
||||
return 'typescript'
|
||||
}
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string
|
||||
language?: string
|
||||
title?: string
|
||||
showLineNumbers?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function CodeBlock({
|
||||
code,
|
||||
language = 'typescript',
|
||||
title,
|
||||
showLineNumbers = false,
|
||||
className,
|
||||
}: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const normalizedLanguage = normalizeLanguage(language)
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('group relative', className)}>
|
||||
{title && (
|
||||
<div
|
||||
className="flex items-center justify-between rounded-t-lg border-2 border-b-0 px-4 py-2.5"
|
||||
style={{
|
||||
borderColor: colors.borders.default,
|
||||
backgroundColor: colors.backgrounds.card
|
||||
}}
|
||||
>
|
||||
<span className="text-sm font-medium" style={{ color: colors.text.secondary }}>
|
||||
{title}
|
||||
</span>
|
||||
<span className="text-xs font-mono" style={{ color: colors.text.disabled }}>
|
||||
{normalizedLanguage}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-x-auto',
|
||||
title ? 'rounded-b-lg' : 'rounded-lg',
|
||||
'border-2'
|
||||
)}
|
||||
style={{
|
||||
borderColor: colors.borders.default,
|
||||
backgroundColor: colors.backgrounds.cardSolid
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
'absolute right-3 top-3 z-10',
|
||||
'rounded-md px-3 py-1.5',
|
||||
'text-xs font-medium',
|
||||
'flex items-center gap-1.5',
|
||||
'opacity-0 transition-all duration-200',
|
||||
'group-hover:opacity-100',
|
||||
copied && 'opacity-100',
|
||||
effects.transitions.all
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: colors.backgrounds.card,
|
||||
color: copied ? colors.accents.success : colors.text.muted,
|
||||
borderWidth: '2px',
|
||||
borderColor: copied ? colors.accents.success : colors.borders.default
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!copied) {
|
||||
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
|
||||
e.currentTarget.style.borderColor = colors.borders.hover
|
||||
e.currentTarget.style.color = colors.text.secondary
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!copied) {
|
||||
e.currentTarget.style.backgroundColor = colors.backgrounds.card
|
||||
e.currentTarget.style.borderColor = colors.borders.default
|
||||
e.currentTarget.style.color = colors.text.muted
|
||||
}
|
||||
}}
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<SyntaxHighlighter
|
||||
language={normalizedLanguage}
|
||||
style={vscDarkPlus}
|
||||
showLineNumbers={showLineNumbers}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1rem',
|
||||
fontSize: '0.875rem',
|
||||
background: 'transparent',
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
144
components/docs/DocsSearch.tsx
Normal file
144
components/docs/DocsSearch.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { colors, effects } from '@/lib/theme'
|
||||
import { Search, X } from 'lucide-react'
|
||||
import type { DocItem } from '@/lib/docs/types'
|
||||
|
||||
interface DocsSearchProps {
|
||||
items: DocItem[]
|
||||
onSearch: (query: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function DocsSearch({
|
||||
items,
|
||||
onSearch,
|
||||
className,
|
||||
}: DocsSearchProps) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Keyboard shortcut (Cmd/Ctrl + K)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
inputRef.current?.blur()
|
||||
setQuery('')
|
||||
onSearch('')
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [onSearch])
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setQuery(value)
|
||||
onSearch(value)
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setQuery('')
|
||||
onSearch('')
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center',
|
||||
'rounded-lg border-2',
|
||||
effects.transitions.colors
|
||||
)}
|
||||
style={{
|
||||
borderColor: isFocused ? colors.borders.hover : colors.borders.default,
|
||||
backgroundColor: colors.backgrounds.card
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isFocused) {
|
||||
e.currentTarget.style.borderColor = colors.borders.hover
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isFocused) {
|
||||
e.currentTarget.style.borderColor = colors.borders.default
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Search
|
||||
className="absolute left-3 h-5 w-5"
|
||||
style={{ color: colors.text.disabled }}
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder="Search documentation..."
|
||||
className={cn(
|
||||
'w-full bg-transparent px-10 py-3',
|
||||
'text-sm outline-none'
|
||||
)}
|
||||
style={{
|
||||
color: colors.text.primary,
|
||||
caretColor: colors.text.secondary
|
||||
}}
|
||||
/>
|
||||
{query ? (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className={cn(
|
||||
'absolute right-3 rounded p-1',
|
||||
effects.transitions.colors
|
||||
)}
|
||||
style={{ color: colors.text.disabled }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
|
||||
e.currentTarget.style.color = colors.text.secondary
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent'
|
||||
e.currentTarget.style.color = colors.text.disabled
|
||||
}}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<kbd
|
||||
className={cn(
|
||||
'absolute right-3',
|
||||
'rounded border px-2 py-1 text-xs font-mono'
|
||||
)}
|
||||
style={{
|
||||
borderColor: colors.borders.default,
|
||||
backgroundColor: colors.backgrounds.cardSolid,
|
||||
color: colors.text.disabled
|
||||
}}
|
||||
>
|
||||
⌘K
|
||||
</kbd>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{query && (
|
||||
<div
|
||||
className="mt-2 text-xs"
|
||||
style={{ color: colors.text.disabled }}
|
||||
>
|
||||
{items.length} result{items.length !== 1 ? 's' : ''} found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
210
components/docs/DocsSidebar.tsx
Normal file
210
components/docs/DocsSidebar.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { colors } from '@/lib/theme'
|
||||
import type { DocNavigation, DocCategory } from '@/lib/docs/types'
|
||||
import { Settings, Wrench, FileText, Palette, Globe, Package, ChevronDown, ChevronRight, X, Smartphone, Network, BookOpen } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
interface DocsSidebarProps {
|
||||
navigation: DocNavigation
|
||||
currentItemId?: string
|
||||
className?: string
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const categoryIcons: Record<DocCategory, LucideIcon> = {
|
||||
Services: Settings,
|
||||
Utils: Wrench,
|
||||
Types: FileText,
|
||||
Theme: Palette,
|
||||
Devices: Smartphone,
|
||||
Domains: Network,
|
||||
Docs: BookOpen,
|
||||
API: Globe,
|
||||
Other: Package,
|
||||
}
|
||||
|
||||
export default function DocsSidebar({
|
||||
navigation,
|
||||
currentItemId,
|
||||
className,
|
||||
onClose,
|
||||
}: DocsSidebarProps) {
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(
|
||||
new Set(navigation.sections.map((s) => s.title))
|
||||
)
|
||||
|
||||
const isMobileDrawer = !!onClose
|
||||
|
||||
const toggleSection = (title: string) => {
|
||||
const newExpanded = new Set(expandedSections)
|
||||
if (newExpanded.has(title)) {
|
||||
newExpanded.delete(title)
|
||||
} else {
|
||||
newExpanded.add(title)
|
||||
}
|
||||
setExpandedSections(newExpanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
isMobileDrawer
|
||||
? 'h-full w-full overflow-y-auto'
|
||||
: 'sticky top-20 h-[calc(100vh-8rem)] overflow-y-auto w-64',
|
||||
isMobileDrawer ? 'border-r-0' : 'border-r-2',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
borderColor: isMobileDrawer ? 'transparent' : colors.borders.default,
|
||||
backgroundColor: isMobileDrawer ? colors.backgrounds.cardSolid : 'transparent'
|
||||
}}
|
||||
>
|
||||
{/* Mobile Header with Close Button */}
|
||||
{isMobileDrawer && (
|
||||
<div
|
||||
className="sticky top-0 z-10 flex items-center justify-between p-4 border-b-2"
|
||||
style={{
|
||||
backgroundColor: colors.backgrounds.cardSolid,
|
||||
borderColor: colors.borders.default
|
||||
}}
|
||||
>
|
||||
<h2 className="text-lg font-semibold" style={{ color: colors.text.primary }}>
|
||||
Navigation
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-md p-2',
|
||||
'transition-colors duration-300'
|
||||
)}
|
||||
style={{ color: colors.text.muted }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
|
||||
e.currentTarget.style.color = colors.text.secondary
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent'
|
||||
e.currentTarget.style.color = colors.text.muted
|
||||
}}
|
||||
aria-label="Close navigation"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="p-4 space-y-2">
|
||||
{navigation.sections.map((section) => {
|
||||
const isExpanded = expandedSections.has(section.title)
|
||||
const Icon = categoryIcons[section.category]
|
||||
|
||||
return (
|
||||
<div key={section.title} className="space-y-1">
|
||||
<button
|
||||
onClick={() => toggleSection(section.title)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md px-3 py-2',
|
||||
'text-sm font-medium',
|
||||
'transition-colors duration-300'
|
||||
)}
|
||||
style={{
|
||||
color: colors.text.secondary,
|
||||
backgroundColor: isExpanded ? colors.backgrounds.hover : 'transparent',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isExpanded) {
|
||||
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isExpanded) {
|
||||
e.currentTarget.style.backgroundColor = 'transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 flex-shrink-0" />
|
||||
)}
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="flex-1">{section.title}</span>
|
||||
<span
|
||||
className="text-xs px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
color: colors.text.disabled,
|
||||
backgroundColor: colors.backgrounds.card
|
||||
}}
|
||||
>
|
||||
{section.items.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="ml-6 space-y-0.5">
|
||||
{section.items.map((item) => {
|
||||
const isActive = item.id === currentItemId
|
||||
|
||||
return (
|
||||
<a
|
||||
key={item.id}
|
||||
href={`#${item.id}`}
|
||||
onClick={isMobileDrawer ? onClose : undefined}
|
||||
className={cn(
|
||||
'block rounded-md px-3 py-1.5',
|
||||
'text-sm transition-colors duration-300'
|
||||
)}
|
||||
style={{
|
||||
color: isActive ? colors.text.primary : colors.text.muted,
|
||||
backgroundColor: isActive ? colors.backgrounds.hover : 'transparent',
|
||||
fontWeight: isActive ? 500 : 400
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
|
||||
e.currentTarget.style.color = colors.text.secondary
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.backgroundColor = 'transparent'
|
||||
e.currentTarget.style.color = colors.text.muted
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-mono px-1.5 py-0.5 rounded flex-shrink-0'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: colors.backgrounds.card,
|
||||
color: colors.text.disabled
|
||||
}}
|
||||
>
|
||||
{item.kind === 'function' && 'fn'}
|
||||
{item.kind === 'method' && 'fn'}
|
||||
{item.kind === 'class' && 'class'}
|
||||
{item.kind === 'interface' && 'interface'}
|
||||
{item.kind === 'type' && 'type'}
|
||||
{item.kind === 'variable' && 'const'}
|
||||
{item.kind === 'property' && 'prop'}
|
||||
{item.kind === 'enum' && 'enum'}
|
||||
</span>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
296
components/docs/FunctionDoc.tsx
Normal file
296
components/docs/FunctionDoc.tsx
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import { colors, surfaces, effects } from '@/lib/theme'
|
||||
import type { DocItem } from '@/lib/docs/types'
|
||||
import CodeBlock from './CodeBlock'
|
||||
import TypeLink from './TypeLink'
|
||||
import { ExternalLink, TriangleAlert } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
interface FunctionDocProps {
|
||||
item: DocItem
|
||||
className?: string
|
||||
availableTypeIds?: Set<string>
|
||||
}
|
||||
|
||||
export default function FunctionDoc({ item, className, availableTypeIds }: FunctionDocProps) {
|
||||
return (
|
||||
<div id={item.id} className={cn('scroll-mt-20', className)}>
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h3 className="text-2xl font-bold" style={{ color: colors.text.primary }}>
|
||||
{item.name}
|
||||
</h3>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-md px-2.5 py-1 text-xs font-medium'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: colors.backgrounds.card,
|
||||
color: colors.text.secondary
|
||||
}}
|
||||
>
|
||||
{item.kind}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-md px-2.5 py-1 text-xs font-medium'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: colors.accents.docsBg,
|
||||
color: colors.accents.docs,
|
||||
borderWidth: '1px',
|
||||
borderColor: colors.accents.docsBorder
|
||||
}}
|
||||
>
|
||||
{item.category}
|
||||
</span>
|
||||
{item.deprecated && (
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: colors.accents.warningBg,
|
||||
color: colors.accents.warning
|
||||
}}
|
||||
>
|
||||
<TriangleAlert className="h-3 w-3" />
|
||||
Deprecated
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<p className="leading-relaxed" style={{ color: colors.text.body }}>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{item.source && (
|
||||
<a
|
||||
href={`https://github.com/ihatenodejs/aidxnCC/blob/main/${item.source.file}#L${item.source.line}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-3 py-2',
|
||||
'text-xs border-2',
|
||||
effects.transitions.colors,
|
||||
'flex-shrink-0'
|
||||
)}
|
||||
style={{
|
||||
color: colors.text.muted,
|
||||
borderColor: colors.borders.default
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = colors.borders.hover
|
||||
e.currentTarget.style.color = colors.text.secondary
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = colors.borders.default
|
||||
e.currentTarget.style.color = colors.text.muted
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
Source
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remarks */}
|
||||
{item.remarks && (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border-l-4 pl-4 py-2',
|
||||
'space-y-2'
|
||||
)}
|
||||
style={{
|
||||
borderColor: colors.accents.ai,
|
||||
backgroundColor: colors.backgrounds.card
|
||||
}}
|
||||
>
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Remarks
|
||||
</h4>
|
||||
<div className="text-sm leading-relaxed prose prose-invert prose-sm max-w-none" style={{ color: colors.text.body }}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{item.remarks}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Signature */}
|
||||
{item.signature && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Signature
|
||||
</h4>
|
||||
<CodeBlock code={item.signature} language="typescript" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parameters */}
|
||||
{item.parameters && item.parameters.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Parameters
|
||||
</h4>
|
||||
<div className="overflow-x-auto rounded-lg border-2" style={{ borderColor: colors.borders.default }}>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b-2" style={{ borderColor: colors.borders.default, backgroundColor: colors.backgrounds.card }}>
|
||||
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Name
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.parameters.map((param, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b last:border-0"
|
||||
style={{ borderColor: colors.borders.subtle }}
|
||||
>
|
||||
<td className="px-4 py-3 font-mono" style={{ color: colors.text.secondary }}>
|
||||
{param.name}
|
||||
{param.optional && (
|
||||
<span style={{ color: colors.text.disabled }}>?</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<TypeLink type={param.type} className="text-sm" availableTypeIds={availableTypeIds} />
|
||||
</td>
|
||||
<td className="px-4 py-3" style={{ color: colors.text.body }}>
|
||||
{param.description || '—'}
|
||||
{param.defaultValue && (
|
||||
<div className="mt-1 text-xs" style={{ color: colors.text.disabled }}>
|
||||
Default: <code>{param.defaultValue}</code>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Returns */}
|
||||
{item.returns && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Returns
|
||||
</h4>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border-2 p-4',
|
||||
'space-y-2'
|
||||
)}
|
||||
style={{
|
||||
borderColor: colors.borders.default,
|
||||
backgroundColor: colors.backgrounds.card
|
||||
}}
|
||||
>
|
||||
<TypeLink type={item.returns.type} className="text-sm" availableTypeIds={availableTypeIds} />
|
||||
{item.returns.description && (
|
||||
<p className="text-sm" style={{ color: colors.text.body }}>
|
||||
{item.returns.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Throws */}
|
||||
{item.throws && item.throws.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Throws
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{item.throws.map((throwsDoc, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'rounded-lg border-2 p-4'
|
||||
)}
|
||||
style={{
|
||||
borderColor: colors.accents.warningBg,
|
||||
backgroundColor: colors.backgrounds.card
|
||||
}}
|
||||
>
|
||||
<p className="text-sm" style={{ color: colors.text.body }}>
|
||||
{throwsDoc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Examples */}
|
||||
{item.examples && item.examples.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Examples
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
{item.examples.map((example, index) => (
|
||||
<CodeBlock
|
||||
key={index}
|
||||
code={example.code}
|
||||
language={example.language}
|
||||
showLineNumbers
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={cn(surfaces.badge.muted)}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* See Also */}
|
||||
{item.see && item.see.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
See Also
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{item.see.map((ref, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm"
|
||||
style={{ color: colors.text.body }}
|
||||
>
|
||||
{ref}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
246
components/docs/TypeDoc.tsx
Normal file
246
components/docs/TypeDoc.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import { colors, surfaces, effects } from '@/lib/theme'
|
||||
import type { DocItem } from '@/lib/docs/types'
|
||||
import CodeBlock from './CodeBlock'
|
||||
import TypeLink from './TypeLink'
|
||||
import { ExternalLink, TriangleAlert } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
interface TypeDocProps {
|
||||
item: DocItem
|
||||
className?: string
|
||||
availableTypeIds?: Set<string>
|
||||
}
|
||||
|
||||
export default function TypeDoc({ item, className, availableTypeIds }: TypeDocProps) {
|
||||
return (
|
||||
<div id={item.id} className={cn('scroll-mt-20', className)}>
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h3 className="text-2xl font-bold" style={{ color: colors.text.primary }}>
|
||||
{item.name}
|
||||
</h3>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-md px-2.5 py-1 text-xs font-medium'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: colors.backgrounds.card,
|
||||
color: colors.text.secondary
|
||||
}}
|
||||
>
|
||||
{item.kind}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-md px-2.5 py-1 text-xs font-medium'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: colors.accents.docsBg,
|
||||
color: colors.accents.docs,
|
||||
borderWidth: '1px',
|
||||
borderColor: colors.accents.docsBorder
|
||||
}}
|
||||
>
|
||||
{item.category}
|
||||
</span>
|
||||
{item.deprecated && (
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: colors.accents.warningBg,
|
||||
color: colors.accents.warning
|
||||
}}
|
||||
>
|
||||
<TriangleAlert className="h-3 w-3" />
|
||||
Deprecated
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<p className="leading-relaxed" style={{ color: colors.text.body }}>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{item.source && (
|
||||
<a
|
||||
href={`https://github.com/ihatenodejs/aidxnCC/blob/main/${item.source.file}#L${item.source.line}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-3 py-2',
|
||||
'text-xs border-2',
|
||||
effects.transitions.colors,
|
||||
'flex-shrink-0'
|
||||
)}
|
||||
style={{
|
||||
color: colors.text.muted,
|
||||
borderColor: colors.borders.default
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = colors.borders.hover
|
||||
e.currentTarget.style.color = colors.text.secondary
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = colors.borders.default
|
||||
e.currentTarget.style.color = colors.text.muted
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
Source
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remarks */}
|
||||
{item.remarks && (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg border-l-4 pl-4 py-2',
|
||||
'space-y-2'
|
||||
)}
|
||||
style={{
|
||||
borderColor: colors.accents.ai,
|
||||
backgroundColor: colors.backgrounds.card
|
||||
}}
|
||||
>
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Remarks
|
||||
</h4>
|
||||
<div className="text-sm leading-relaxed prose prose-invert prose-sm max-w-none" style={{ color: colors.text.body }}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{item.remarks}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Type Definition */}
|
||||
{item.signature && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Definition
|
||||
</h4>
|
||||
<CodeBlock
|
||||
code={item.kind === 'interface' ? `interface ${item.name} ${item.signature}` : `${item.kind} ${item.name} = ${item.signature}`}
|
||||
language="typescript"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Interface Properties */}
|
||||
{item.kind === 'interface' && item.parameters && item.parameters.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Properties
|
||||
</h4>
|
||||
<div className="overflow-x-auto rounded-lg border-2" style={{ borderColor: colors.borders.default }}>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b-2" style={{ borderColor: colors.borders.default, backgroundColor: colors.backgrounds.card }}>
|
||||
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Property
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.parameters.map((prop, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b last:border-0"
|
||||
style={{ borderColor: colors.borders.subtle }}
|
||||
>
|
||||
<td className="px-4 py-3 font-mono" style={{ color: colors.text.secondary }}>
|
||||
{prop.name}
|
||||
{prop.optional && (
|
||||
<span style={{ color: colors.text.disabled }}>?</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<TypeLink type={prop.type} className="text-xs" availableTypeIds={availableTypeIds} />
|
||||
</td>
|
||||
<td className="px-4 py-3" style={{ color: colors.text.body }}>
|
||||
{prop.description || '—'}
|
||||
{prop.defaultValue && (
|
||||
<div className="mt-1 text-xs" style={{ color: colors.text.disabled }}>
|
||||
Default: <code>{prop.defaultValue}</code>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Examples */}
|
||||
{item.examples && item.examples.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
Examples
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
{item.examples.map((example, index) => (
|
||||
<CodeBlock
|
||||
key={index}
|
||||
code={example.code}
|
||||
language={example.language}
|
||||
showLineNumbers
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={cn(surfaces.badge.muted)}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* See Also */}
|
||||
{item.see && item.see.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
|
||||
See Also
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{item.see.map((ref, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm"
|
||||
style={{ color: colors.text.body }}
|
||||
>
|
||||
{ref}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
126
components/docs/TypeLink.tsx
Normal file
126
components/docs/TypeLink.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
'use client'
|
||||
|
||||
import { colors, effects } from '@/lib/theme'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface TypeLinkProps {
|
||||
type: string
|
||||
className?: string
|
||||
availableTypeIds?: Set<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a type string and converts type references into clickable links
|
||||
* that scroll to the corresponding type definition in the documentation.
|
||||
*
|
||||
* Supports:
|
||||
* - Simple types: Domain, User, etc.
|
||||
* - Generic types: Array<Domain>, Promise<User>
|
||||
* - Union types: string | number
|
||||
* - Complex types: Record<string, Domain>
|
||||
*/
|
||||
export default function TypeLink({ type, className, availableTypeIds }: TypeLinkProps) {
|
||||
const parseTypeString = (typeStr: string): React.ReactNode[] => {
|
||||
const parts: React.ReactNode[] = []
|
||||
let currentIndex = 0
|
||||
|
||||
const typeNamePattern = /\b([A-Z][a-zA-Z0-9]*)\b/g
|
||||
const builtInTypes = new Set([
|
||||
'string', 'number', 'boolean', 'void', 'null', 'undefined', 'any', 'unknown',
|
||||
'never', 'object', 'symbol', 'bigint', 'Array', 'Promise', 'Record', 'Partial',
|
||||
'Required', 'Readonly', 'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable',
|
||||
'ReturnType', 'InstanceType', 'ThisType', 'Parameters', 'ConstructorParameters',
|
||||
'Date', 'Error', 'RegExp', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Function',
|
||||
'ReadonlyArray', 'String', 'Number', 'Boolean', 'Symbol', 'Object'
|
||||
])
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = typeNamePattern.exec(typeStr)) !== null) {
|
||||
const typeName = match[1]
|
||||
const matchStart = match.index
|
||||
const matchEnd = typeNamePattern.lastIndex
|
||||
|
||||
if (matchStart > currentIndex) {
|
||||
parts.push(
|
||||
<span key={`text-${currentIndex}`}>
|
||||
{typeStr.substring(currentIndex, matchStart)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (builtInTypes.has(typeName)) {
|
||||
parts.push(
|
||||
<span key={`builtin-${matchStart}`}>
|
||||
{typeName}
|
||||
</span>
|
||||
)
|
||||
} else {
|
||||
// Check if this type exists in the documentation
|
||||
const typeExists = availableTypeIds?.has(typeName) ?? false
|
||||
|
||||
if (typeExists) {
|
||||
parts.push(
|
||||
<button
|
||||
key={`link-${matchStart}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
const targetId = typeName
|
||||
const element = document.getElementById(targetId)
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
|
||||
element.classList.add('ring-2', 'ring-blue-400', 'ring-offset-2', 'ring-offset-gray-900')
|
||||
setTimeout(() => {
|
||||
element.classList.remove('ring-2', 'ring-blue-400', 'ring-offset-2', 'ring-offset-gray-900')
|
||||
}, 2000)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'hover:underline cursor-pointer',
|
||||
effects.transitions.colors
|
||||
)}
|
||||
style={{
|
||||
color: colors.accents.link,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = colors.accents.linkHover
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = colors.accents.link
|
||||
}}
|
||||
>
|
||||
{typeName}
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
// Type doesn't exist in docs, render as plain text
|
||||
parts.push(
|
||||
<span key={`text-${matchStart}`}>
|
||||
{typeName}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
currentIndex = matchEnd
|
||||
}
|
||||
|
||||
if (currentIndex < typeStr.length) {
|
||||
parts.push(
|
||||
<span key={`text-${currentIndex}`}>
|
||||
{typeStr.substring(currentIndex)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={cn('font-mono', className)}>
|
||||
{parseTypeString(type)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue