feat (v1.0.0): initial refactor and redesign

This commit is contained in:
Aidan 2025-10-09 04:12:05 -04:00
parent 3058aa1ab4
commit fe9b50b30e
134 changed files with 17792 additions and 3670 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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
View 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>
)
}

View 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>
)
}