@usecross/docs 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,269 @@
1
+ import type { GriffeDocstring, GriffeDocstringSection, GriffeDocstringElement, GriffeExpression } from '../../types'
2
+ import { Markdown } from '../Markdown'
3
+
4
+ /**
5
+ * Render a type annotation expression to string
6
+ */
7
+ function renderExpression(expr: GriffeExpression | string | undefined): string {
8
+ if (!expr) return ''
9
+ if (typeof expr === 'string') return expr
10
+ if (expr.str) return expr.str
11
+ if (expr.canonical) return expr.canonical
12
+
13
+ const exprAny = expr as any
14
+
15
+ // Handle ExprName with member reference
16
+ if (expr.name && typeof expr.name === 'string') return expr.name
17
+
18
+ // Handle ExprBoolOp (like `config or StrawberryConfig()`)
19
+ if (exprAny.cls === 'ExprBoolOp' && exprAny.operator && Array.isArray(exprAny.values)) {
20
+ return exprAny.values.map((v: any) => renderExpression(v)).join(` ${exprAny.operator} `)
21
+ }
22
+
23
+ // Handle ExprBinOp (like `type | None`)
24
+ if (exprAny.cls === 'ExprBinOp' && exprAny.left && exprAny.right) {
25
+ const left = renderExpression(exprAny.left)
26
+ const right = renderExpression(exprAny.right)
27
+ const op = exprAny.operator || '|'
28
+ return `${left} ${op} ${right}`
29
+ }
30
+
31
+ // Handle ExprCall (like `StrawberryConfig()`)
32
+ if (exprAny.cls === 'ExprCall' && exprAny.function) {
33
+ const funcName = renderExpression(exprAny.function)
34
+ const args = Array.isArray(exprAny.arguments)
35
+ ? exprAny.arguments.map((a: any) => renderExpression(a)).join(', ')
36
+ : ''
37
+ return `${funcName}(${args})`
38
+ }
39
+
40
+ // Handle ExprAttribute (like contextlib.asynccontextmanager)
41
+ if (exprAny.cls === 'ExprAttribute' && Array.isArray(exprAny.values)) {
42
+ return exprAny.values.map((v: any) => renderExpression(v)).join('.')
43
+ }
44
+
45
+ // Handle ExprList and ExprTuple
46
+ if ('elements' in exprAny && Array.isArray(exprAny.elements)) {
47
+ const inner = exprAny.elements.map((el: any) => renderExpression(el)).join(', ')
48
+ return exprAny.cls === 'ExprTuple' ? `(${inner})` : `[${inner}]`
49
+ }
50
+
51
+ // Handle ExprDict
52
+ if (exprAny.cls === 'ExprDict' && Array.isArray(exprAny.keys) && Array.isArray(exprAny.values)) {
53
+ const pairs = exprAny.keys.map((k: any, i: number) =>
54
+ `${renderExpression(k)}: ${renderExpression(exprAny.values[i])}`
55
+ ).join(', ')
56
+ return `{${pairs}}`
57
+ }
58
+
59
+ // Handle ExprSubscript (like Dict[str, int])
60
+ if (exprAny.left && exprAny.slice) {
61
+ const left = renderExpression(exprAny.left)
62
+ const slice = renderExpression(exprAny.slice)
63
+ return `${left}[${slice}]`
64
+ }
65
+
66
+ // Handle slice expressions
67
+ if ('slice' in exprAny && exprAny.slice && !exprAny.left) {
68
+ return renderExpression(exprAny.slice)
69
+ }
70
+
71
+ // Fallback for unknown expressions
72
+ if (typeof expr === 'object') {
73
+ return JSON.stringify(expr)
74
+ }
75
+
76
+ return String(expr)
77
+ }
78
+
79
+ interface DocstringSectionProps {
80
+ section: GriffeDocstringSection
81
+ }
82
+
83
+ function DocstringSection({ section }: DocstringSectionProps) {
84
+ switch (section.kind) {
85
+ case 'text':
86
+ return (
87
+ <div className="prose prose-sm dark:prose-invert max-w-none">
88
+ <Markdown content={section.value as string} />
89
+ </div>
90
+ )
91
+
92
+ case 'parameters':
93
+ return (
94
+ <div className="mt-4">
95
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Parameters</h4>
96
+ <dl className="space-y-2">
97
+ {(section.value as GriffeDocstringElement[])?.map((param) => (
98
+ <div key={param.name} className="grid grid-cols-[auto_1fr] gap-x-3">
99
+ <dt className="font-mono text-sm">
100
+ <span className="text-orange-600 dark:text-orange-400">{param.name}</span>
101
+ {param.annotation && (
102
+ <span className="text-gray-500 dark:text-gray-400">
103
+ {' '}({renderExpression(param.annotation)})
104
+ </span>
105
+ )}
106
+ </dt>
107
+ <dd className="text-sm text-gray-600 dark:text-gray-300">
108
+ {param.description}
109
+ </dd>
110
+ </div>
111
+ ))}
112
+ </dl>
113
+ </div>
114
+ )
115
+
116
+ case 'returns':
117
+ return (
118
+ <div className="mt-4">
119
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Returns</h4>
120
+ <div className="text-sm text-gray-600 dark:text-gray-300">
121
+ {Array.isArray(section.value) ? (
122
+ (section.value as GriffeDocstringElement[]).map((ret, i) => (
123
+ <div key={i}>
124
+ {ret.annotation && (
125
+ <span className="font-mono text-green-600 dark:text-green-400">
126
+ {renderExpression(ret.annotation)}
127
+ </span>
128
+ )}
129
+ {ret.description && <span> - {ret.description}</span>}
130
+ </div>
131
+ ))
132
+ ) : (
133
+ section.value
134
+ )}
135
+ </div>
136
+ </div>
137
+ )
138
+
139
+ case 'raises':
140
+ return (
141
+ <div className="mt-4">
142
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Raises</h4>
143
+ <dl className="space-y-2">
144
+ {(section.value as GriffeDocstringElement[])?.map((exc, i) => (
145
+ <div key={i} className="grid grid-cols-[auto_1fr] gap-x-3">
146
+ <dt className="font-mono text-sm text-red-600 dark:text-red-400">
147
+ {exc.annotation ? renderExpression(exc.annotation) : exc.name}
148
+ </dt>
149
+ <dd className="text-sm text-gray-600 dark:text-gray-300">
150
+ {exc.description}
151
+ </dd>
152
+ </div>
153
+ ))}
154
+ </dl>
155
+ </div>
156
+ )
157
+
158
+ case 'examples':
159
+ return (
160
+ <div className="mt-4">
161
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Examples</h4>
162
+ <pre className="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 overflow-x-auto text-sm">
163
+ <code>{section.value as string}</code>
164
+ </pre>
165
+ </div>
166
+ )
167
+
168
+ case 'attributes':
169
+ return (
170
+ <div className="mt-4">
171
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Attributes</h4>
172
+ <dl className="space-y-2">
173
+ {(section.value as GriffeDocstringElement[])?.map((attr) => (
174
+ <div key={attr.name} className="grid grid-cols-[auto_1fr] gap-x-3">
175
+ <dt className="font-mono text-sm">
176
+ <span className="text-orange-600 dark:text-orange-400">{attr.name}</span>
177
+ {attr.annotation && (
178
+ <span className="text-gray-500 dark:text-gray-400">
179
+ {' '}({renderExpression(attr.annotation)})
180
+ </span>
181
+ )}
182
+ </dt>
183
+ <dd className="text-sm text-gray-600 dark:text-gray-300">
184
+ {attr.description}
185
+ </dd>
186
+ </div>
187
+ ))}
188
+ </dl>
189
+ </div>
190
+ )
191
+
192
+ case 'deprecated':
193
+ return (
194
+ <div className="mt-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
195
+ <h4 className="text-sm font-semibold text-yellow-800 dark:text-yellow-200 mb-1">Deprecated</h4>
196
+ <p className="text-sm text-yellow-700 dark:text-yellow-300">{section.value as string}</p>
197
+ </div>
198
+ )
199
+
200
+ case 'admonition':
201
+ return (
202
+ <div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
203
+ {section.title && (
204
+ <h4 className="text-sm font-semibold text-blue-800 dark:text-blue-200 mb-1">{section.title}</h4>
205
+ )}
206
+ <p className="text-sm text-blue-700 dark:text-blue-300">{section.value as string}</p>
207
+ </div>
208
+ )
209
+
210
+ default:
211
+ if (section.title) {
212
+ return (
213
+ <div className="mt-4">
214
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">{section.title}</h4>
215
+ <div className="text-sm text-gray-600 dark:text-gray-300">
216
+ {typeof section.value === 'string' ? section.value : JSON.stringify(section.value)}
217
+ </div>
218
+ </div>
219
+ )
220
+ }
221
+ return null
222
+ }
223
+ }
224
+
225
+ interface DocstringProps {
226
+ docstring: GriffeDocstring | undefined
227
+ /** Show raw text instead of parsed sections */
228
+ raw?: boolean
229
+ /** Only show the text/description part, skip parameters/returns/etc */
230
+ showOnlyText?: boolean
231
+ /** Additional CSS class */
232
+ className?: string
233
+ }
234
+
235
+ /**
236
+ * Renders a parsed docstring with sections for parameters, returns, raises, etc.
237
+ */
238
+ export function Docstring({ docstring, raw = false, showOnlyText = false, className = '' }: DocstringProps) {
239
+ if (!docstring) return null
240
+
241
+ // If raw mode or no parsed sections, show raw value
242
+ if (raw || !docstring.parsed || docstring.parsed.length === 0) {
243
+ return (
244
+ <div className={`prose prose-sm dark:prose-invert max-w-none ${className}`}>
245
+ <p className="whitespace-pre-wrap text-gray-600 dark:text-gray-300">{docstring.value}</p>
246
+ </div>
247
+ )
248
+ }
249
+
250
+ // If showOnlyText, only render the first text section (the description)
251
+ if (showOnlyText) {
252
+ const firstTextSection = docstring.parsed.find(s => s.kind === 'text')
253
+ if (!firstTextSection) return null
254
+
255
+ return (
256
+ <div className={`prose prose-sm dark:prose-invert max-w-none ${className}`}>
257
+ <Markdown content={firstTextSection.value as string} />
258
+ </div>
259
+ )
260
+ }
261
+
262
+ return (
263
+ <div className={className}>
264
+ {docstring.parsed.map((section, i) => (
265
+ <DocstringSection key={`${section.kind}-${i}`} section={section} />
266
+ ))}
267
+ </div>
268
+ )
269
+ }
@@ -0,0 +1,130 @@
1
+ import type { GriffeFunction, GriffeDocstringElement } from '../../types'
2
+ import { Signature } from './Signature'
3
+ import { Docstring } from './Docstring'
4
+ import { ParameterTable } from './ParameterTable'
5
+ import { CodeSpan } from './CodeSpan'
6
+ import { Markdown } from '../Markdown'
7
+
8
+ interface FunctionDocProps {
9
+ fn: GriffeFunction
10
+ /** Whether this is a method (inside a class) */
11
+ isMethod?: boolean
12
+ /** Show function name as title */
13
+ showName?: boolean
14
+ /** GitHub repo URL for source links */
15
+ githubUrl?: string
16
+ /** Additional CSS class */
17
+ className?: string
18
+ /** Override display path (e.g., for aliases to show alias name instead of target path) */
19
+ displayPath?: string
20
+ }
21
+
22
+ /**
23
+ * Renders documentation for a function or method matching strawberry.rocks design.
24
+ */
25
+ export function FunctionDoc({ fn, isMethod = false, showName = true, githubUrl, className = '', displayPath }: FunctionDocProps) {
26
+ const hasParams = fn.parameters && fn.parameters.filter(p => p.name !== 'self').length > 0
27
+
28
+ // Get returns description from docstring
29
+ const returnsSection = fn.docstring?.parsed?.find(s => s.kind === 'returns')
30
+ const returnsValue = returnsSection?.value
31
+ const returnsDescription = Array.isArray(returnsValue)
32
+ ? (returnsValue[0] as GriffeDocstringElement)?.description
33
+ : undefined
34
+
35
+ // Get additional text sections (examples, notes, etc.) - all text sections after the first
36
+ const textSections = fn.docstring?.parsed?.filter(s => s.kind === 'text') || []
37
+ const additionalTextSections = textSections.slice(1) // Skip first (description)
38
+
39
+ // Get relative filepath for display (prefer package-relative path)
40
+ const relativeFilepath = fn.relative_package_filepath || fn.relative_filepath || fn.filepath
41
+
42
+ // Build GitHub URL for source link
43
+ const githubSourceUrl = githubUrl && relativeFilepath && fn.lineno
44
+ ? `${githubUrl}/blob/main/${relativeFilepath}#L${fn.lineno}-L${fn.endlineno || fn.lineno}`
45
+ : undefined
46
+
47
+ return (
48
+ <article id={fn.name} className={`scroll-mt-20 ${className}`}>
49
+ {/* Function name/title */}
50
+ {showName && (
51
+ <h1 className="font-mono text-2xl font-normal text-gray-900 dark:text-white mb-8">
52
+ {displayPath || fn.path || fn.name}
53
+ </h1>
54
+ )}
55
+
56
+ {/* Docstring - description text only */}
57
+ {fn.docstring && (
58
+ <div className="mb-6">
59
+ <Docstring docstring={fn.docstring} showOnlyText />
60
+ </div>
61
+ )}
62
+
63
+ {/* Returns section */}
64
+ {returnsDescription && (
65
+ <section className="mb-6">
66
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
67
+ Returns:
68
+ </h2>
69
+ <p className="text-gray-700 dark:text-gray-300">
70
+ {returnsDescription}
71
+ </p>
72
+ </section>
73
+ )}
74
+
75
+ {/* Signature section */}
76
+ <section className="mb-6">
77
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
78
+ Signature:
79
+ </h2>
80
+ <Signature fn={fn} />
81
+ </section>
82
+
83
+ {/* Parameters section */}
84
+ {hasParams && (
85
+ <section id="parameters" className="mb-6">
86
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
87
+ Parameters:
88
+ </h2>
89
+ <ParameterTable
90
+ parameters={fn.parameters!}
91
+ docstringSections={fn.docstring?.parsed}
92
+ />
93
+ </section>
94
+ )}
95
+
96
+ {/* Additional text sections (examples, notes, etc.) */}
97
+ {additionalTextSections.length > 0 && (
98
+ <section className="mb-6 prose prose-sm dark:prose-invert max-w-none">
99
+ {additionalTextSections.map((section, i) => (
100
+ <Markdown key={i} content={section.value as string} />
101
+ ))}
102
+ </section>
103
+ )}
104
+
105
+ {/* Footer with file path and GitHub link (only for top-level functions) */}
106
+ {!isMethod && relativeFilepath && (
107
+ <footer className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 space-y-4">
108
+ <p className="flex items-center gap-2">
109
+ <span className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
110
+ File path:
111
+ </span>
112
+ <CodeSpan allowCopy>{relativeFilepath}</CodeSpan>
113
+ </p>
114
+ {githubSourceUrl && (
115
+ <p>
116
+ <a
117
+ href={githubSourceUrl}
118
+ target="_blank"
119
+ rel="noopener noreferrer"
120
+ className="text-sm font-semibold text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 uppercase tracking-wide"
121
+ >
122
+ Open in GitHub
123
+ </a>
124
+ </p>
125
+ )}
126
+ </footer>
127
+ )}
128
+ </article>
129
+ )
130
+ }
@@ -0,0 +1,298 @@
1
+ import { Link } from '@inertiajs/react'
2
+ import type { GriffeModule, GriffeClass, GriffeFunction, GriffeAttribute, GriffeMember, GriffeExpression } from '../../types'
3
+ import { Docstring } from './Docstring'
4
+ import { ClassDoc } from './ClassDoc'
5
+ import { FunctionDoc } from './FunctionDoc'
6
+
7
+ /**
8
+ * Safely render a Griffe expression or value to a string
9
+ */
10
+ function renderValue(value: string | GriffeExpression | undefined): string {
11
+ if (!value) return ''
12
+ if (typeof value === 'string') return value
13
+ // Handle Griffe expression objects
14
+ if (value.str) return value.str
15
+ if (value.canonical) return value.canonical
16
+
17
+ const exprAny = value as any
18
+
19
+ // Handle ExprName with member reference
20
+ if (value.name && typeof value.name === 'string') return value.name
21
+
22
+ // Handle ExprBoolOp (like `config or StrawberryConfig()`)
23
+ if (exprAny.cls === 'ExprBoolOp' && exprAny.operator && Array.isArray(exprAny.values)) {
24
+ return exprAny.values.map((v: any) => renderValue(v)).join(` ${exprAny.operator} `)
25
+ }
26
+
27
+ // Handle ExprBinOp (like `type | None`)
28
+ if (exprAny.cls === 'ExprBinOp' && exprAny.left && exprAny.right) {
29
+ const left = renderValue(exprAny.left)
30
+ const right = renderValue(exprAny.right)
31
+ const op = exprAny.operator || '|'
32
+ return `${left} ${op} ${right}`
33
+ }
34
+
35
+ // Handle ExprCall (like `StrawberryConfig()`)
36
+ if (exprAny.cls === 'ExprCall' && exprAny.function) {
37
+ const funcName = renderValue(exprAny.function)
38
+ const args = Array.isArray(exprAny.arguments)
39
+ ? exprAny.arguments.map((a: any) => renderValue(a)).join(', ')
40
+ : ''
41
+ return `${funcName}(${args})`
42
+ }
43
+
44
+ // Handle ExprAttribute (like contextlib.asynccontextmanager)
45
+ if (exprAny.cls === 'ExprAttribute' && Array.isArray(exprAny.values)) {
46
+ return exprAny.values.map((v: any) => renderValue(v)).join('.')
47
+ }
48
+
49
+ // Handle ExprList and ExprTuple
50
+ if ('elements' in exprAny && Array.isArray(exprAny.elements)) {
51
+ const inner = exprAny.elements.map((el: any) => renderValue(el)).join(', ')
52
+ return exprAny.cls === 'ExprTuple' ? `(${inner})` : `[${inner}]`
53
+ }
54
+
55
+ // Handle ExprDict
56
+ if (exprAny.cls === 'ExprDict' && Array.isArray(exprAny.keys) && Array.isArray(exprAny.values)) {
57
+ const pairs = exprAny.keys.map((k: any, i: number) =>
58
+ `${renderValue(k)}: ${renderValue(exprAny.values[i])}`
59
+ ).join(', ')
60
+ return `{${pairs}}`
61
+ }
62
+
63
+ // Handle ExprSubscript (like Dict[str, int])
64
+ if (exprAny.left && exprAny.slice) {
65
+ const left = renderValue(exprAny.left)
66
+ const slice = renderValue(exprAny.slice)
67
+ return `${left}[${slice}]`
68
+ }
69
+
70
+ // Handle slice expressions
71
+ if ('slice' in exprAny && exprAny.slice && !exprAny.left) {
72
+ return renderValue(exprAny.slice)
73
+ }
74
+
75
+ // Fallback for unknown expressions
76
+ if (typeof value === 'object') {
77
+ return JSON.stringify(value)
78
+ }
79
+
80
+ return String(value)
81
+ }
82
+
83
+ interface ModuleDocProps {
84
+ module: GriffeModule
85
+ /** URL prefix for links */
86
+ prefix?: string
87
+ /** Show full module content or just summary */
88
+ showFull?: boolean
89
+ /** Additional CSS class */
90
+ className?: string
91
+ /** Override display path (e.g., for aliases to show alias name instead of target path) */
92
+ displayPath?: string
93
+ /** GitHub repository URL for "Open in GitHub" links */
94
+ githubUrl?: string
95
+ }
96
+
97
+ /**
98
+ * Renders documentation for a module including its classes, functions, and submodules.
99
+ */
100
+ export function ModuleDoc({ module, prefix = '/api', showFull = true, className = '', displayPath, githubUrl }: ModuleDocProps) {
101
+ const members = module.members ?? {}
102
+
103
+ // Separate members by type
104
+ const submodules: GriffeModule[] = []
105
+ const classes: GriffeClass[] = []
106
+ const functions: GriffeFunction[] = []
107
+ const attributes: GriffeAttribute[] = []
108
+
109
+ for (const member of Object.values(members)) {
110
+ switch (member.kind) {
111
+ case 'module':
112
+ submodules.push(member as GriffeModule)
113
+ break
114
+ case 'class':
115
+ classes.push(member as GriffeClass)
116
+ break
117
+ case 'function':
118
+ functions.push(member as GriffeFunction)
119
+ break
120
+ case 'attribute':
121
+ attributes.push(member as GriffeAttribute)
122
+ break
123
+ }
124
+ }
125
+
126
+ // Sort alphabetically
127
+ submodules.sort((a, b) => a.name.localeCompare(b.name))
128
+ classes.sort((a, b) => a.name.localeCompare(b.name))
129
+ functions.sort((a, b) => a.name.localeCompare(b.name))
130
+ attributes.sort((a, b) => a.name.localeCompare(b.name))
131
+
132
+ // Generate href for a member using dotted path
133
+ const memberHref = (member: GriffeMember) => {
134
+ const modulePath = module.path || module.name
135
+ return `${prefix}/${modulePath}.${member.name}`
136
+ }
137
+
138
+ return (
139
+ <div className={className}>
140
+ {/* Module header */}
141
+ <h1 id={module.name} className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
142
+ <span className="text-gray-500 dark:text-gray-400 font-normal">module </span>
143
+ {displayPath || module.path || module.name}
144
+ </h1>
145
+
146
+ {/* Docstring */}
147
+ {module.docstring && (
148
+ <div className="mb-8">
149
+ <Docstring docstring={module.docstring} />
150
+ </div>
151
+ )}
152
+
153
+ {/* Submodules */}
154
+ {submodules.length > 0 && (
155
+ <div className="mb-8">
156
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
157
+ Submodules
158
+ </h2>
159
+ <ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
160
+ {submodules.map((submodule) => (
161
+ <li key={submodule.name}>
162
+ <Link
163
+ href={`${prefix}/${submodule.path || submodule.name}`}
164
+ className="block p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-primary-300 dark:hover:border-primary-600 transition-colors"
165
+ >
166
+ <div className="font-mono text-sm text-primary-600 dark:text-primary-400">
167
+ {submodule.path || submodule.name}
168
+ </div>
169
+ {submodule.docstring && (
170
+ <div className="text-sm text-gray-600 dark:text-gray-300 mt-1 line-clamp-2">
171
+ {submodule.docstring.value.split('\n')[0]}
172
+ </div>
173
+ )}
174
+ </Link>
175
+ </li>
176
+ ))}
177
+ </ul>
178
+ </div>
179
+ )}
180
+
181
+ {/* Classes - summary or full */}
182
+ {classes.length > 0 && (
183
+ <div id="classes" className="mb-8">
184
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
185
+ Classes
186
+ </h2>
187
+ {showFull ? (
188
+ <div className="space-y-12">
189
+ {classes.map((cls) => (
190
+ <ClassDoc key={cls.name} cls={cls} prefix={prefix} githubUrl={githubUrl} />
191
+ ))}
192
+ </div>
193
+ ) : (
194
+ <ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
195
+ {classes.map((cls) => (
196
+ <li key={cls.name}>
197
+ <Link
198
+ href={memberHref(cls)}
199
+ className="block p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-primary-300 dark:hover:border-primary-600 transition-colors"
200
+ >
201
+ <div className="font-mono text-sm text-primary-600 dark:text-primary-400">
202
+ {cls.name}
203
+ </div>
204
+ {cls.docstring && (
205
+ <div className="text-sm text-gray-600 dark:text-gray-300 mt-1 line-clamp-2">
206
+ {cls.docstring.value.split('\n')[0]}
207
+ </div>
208
+ )}
209
+ </Link>
210
+ </li>
211
+ ))}
212
+ </ul>
213
+ )}
214
+ </div>
215
+ )}
216
+
217
+ {/* Functions - summary or full */}
218
+ {functions.length > 0 && (
219
+ <div id="functions" className="mb-8">
220
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
221
+ Functions
222
+ </h2>
223
+ {showFull ? (
224
+ <div className="space-y-8">
225
+ {functions.map((fn) => (
226
+ <FunctionDoc key={fn.name} fn={fn} githubUrl={githubUrl} />
227
+ ))}
228
+ </div>
229
+ ) : (
230
+ <ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
231
+ {functions.map((fn) => (
232
+ <li key={fn.name}>
233
+ <Link
234
+ href={memberHref(fn)}
235
+ className="block p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-primary-300 dark:hover:border-primary-600 transition-colors"
236
+ >
237
+ <div className="font-mono text-sm text-primary-600 dark:text-primary-400">
238
+ {fn.name}()
239
+ </div>
240
+ {fn.docstring && (
241
+ <div className="text-sm text-gray-600 dark:text-gray-300 mt-1 line-clamp-2">
242
+ {fn.docstring.value.split('\n')[0]}
243
+ </div>
244
+ )}
245
+ </Link>
246
+ </li>
247
+ ))}
248
+ </ul>
249
+ )}
250
+ </div>
251
+ )}
252
+
253
+ {/* Module-level attributes */}
254
+ {attributes.length > 0 && (
255
+ <div className="mb-8">
256
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
257
+ Attributes
258
+ </h2>
259
+ <dl className="space-y-3">
260
+ {attributes.map((attr) => (
261
+ <div key={attr.name} id={attr.name} className="scroll-mt-20">
262
+ <dt className="font-mono text-sm">
263
+ <span className="text-orange-600 dark:text-orange-400 font-semibold">{attr.name}</span>
264
+ {attr.annotation && (
265
+ <>
266
+ <span className="text-gray-600 dark:text-gray-400">: </span>
267
+ <span className="text-green-600 dark:text-green-400">
268
+ {typeof attr.annotation === 'string' ? attr.annotation : attr.annotation.str || attr.annotation.name}
269
+ </span>
270
+ </>
271
+ )}
272
+ {attr.value && (
273
+ <>
274
+ <span className="text-gray-600 dark:text-gray-400"> = </span>
275
+ <span className="text-cyan-600 dark:text-cyan-400">{renderValue(attr.value)}</span>
276
+ </>
277
+ )}
278
+ </dt>
279
+ {attr.docstring && (
280
+ <dd className="mt-1 text-sm text-gray-600 dark:text-gray-300 ml-4">
281
+ {attr.docstring.value}
282
+ </dd>
283
+ )}
284
+ </div>
285
+ ))}
286
+ </dl>
287
+ </div>
288
+ )}
289
+
290
+ {/* Source location */}
291
+ {(module.relative_package_filepath || module.filepath) && (
292
+ <div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
293
+ <span className="font-mono">{module.relative_package_filepath || module.filepath}</span>
294
+ </div>
295
+ )}
296
+ </div>
297
+ )
298
+ }