@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,216 @@
1
+ import type { APIPageProps, GriffeModule, GriffeClass, GriffeFunction, GriffeMember, GriffeAlias } from '../../types'
2
+ import { APILayout } from './APILayout'
3
+ import { ModuleDoc } from './ModuleDoc'
4
+ import { ClassDoc } from './ClassDoc'
5
+ import { FunctionDoc } from './FunctionDoc'
6
+ import { TableOfContents, generateClassToc, type TocItem } from './TableOfContents'
7
+
8
+ /**
9
+ * Resolve an alias to its target in the API data
10
+ */
11
+ function resolveAlias(alias: GriffeAlias, apiData: GriffeModule): GriffeMember | null {
12
+ const targetPath = alias.target_path
13
+ if (!targetPath) return null
14
+
15
+ // Split the target path into parts
16
+ const parts = targetPath.split('.')
17
+ const packageName = apiData.name
18
+
19
+ let current: GriffeModule | GriffeClass = apiData
20
+ for (let i = 0; i < parts.length; i++) {
21
+ const part = parts[i]
22
+
23
+ // Skip the package name if it matches
24
+ if (i === 0 && part === packageName) continue
25
+
26
+ // Look in members
27
+ if (current.members) {
28
+ const member: GriffeMember | undefined = current.members[part]
29
+ if (member) {
30
+ // Only modules and classes have members to recurse into
31
+ if (member.kind === 'module' || member.kind === 'class') {
32
+ current = member as GriffeModule | GriffeClass
33
+ } else {
34
+ // Found a function, attribute, or alias - return it
35
+ return member
36
+ }
37
+ } else {
38
+ return null
39
+ }
40
+ } else {
41
+ return null
42
+ }
43
+ }
44
+
45
+ return current
46
+ }
47
+
48
+ /**
49
+ * Generate TOC items based on item type
50
+ */
51
+ function generateTocItems(item: GriffeMember, apiData: GriffeModule): TocItem[] {
52
+ // Resolve aliases first
53
+ if (item.kind === 'alias') {
54
+ const resolved = resolveAlias(item as GriffeAlias, apiData)
55
+ if (resolved) {
56
+ return generateTocItems(resolved, apiData)
57
+ }
58
+ return []
59
+ }
60
+
61
+ if (item.kind === 'class') {
62
+ return generateClassToc(item as GriffeClass)
63
+ }
64
+ if (item.kind === 'function') {
65
+ const fn = item as GriffeFunction
66
+ const items: TocItem[] = [{ id: fn.name, title: fn.name, level: 1 }]
67
+ if (fn.parameters && fn.parameters.length > 0) {
68
+ items.push({ id: 'parameters', title: 'Parameters', level: 2 })
69
+ }
70
+ return items
71
+ }
72
+ if (item.kind === 'module') {
73
+ const mod = item as GriffeModule
74
+ const items: TocItem[] = [{ id: mod.name, title: mod.name, level: 1 }]
75
+ if (mod.members) {
76
+ const members = Object.values(mod.members)
77
+ const classes = members.filter(m => m.kind === 'class')
78
+ const functions = members.filter(m => m.kind === 'function')
79
+
80
+ if (classes.length > 0) {
81
+ items.push({ id: 'classes', title: 'Classes', level: 2 })
82
+ }
83
+ if (functions.length > 0) {
84
+ items.push({ id: 'functions', title: 'Functions', level: 2 })
85
+ }
86
+ }
87
+ return items
88
+ }
89
+ return []
90
+ }
91
+
92
+ /**
93
+ * Determine what kind of content to render based on the current item
94
+ */
95
+ function APIContent({
96
+ item,
97
+ prefix,
98
+ currentPath,
99
+ apiData,
100
+ displayPath,
101
+ githubUrl,
102
+ }: {
103
+ item: GriffeMember
104
+ prefix: string
105
+ currentPath: string
106
+ apiData: GriffeModule
107
+ /** Override the display path (used for aliases to show alias name instead of target) */
108
+ displayPath?: string
109
+ /** GitHub repository URL for "Open in GitHub" links */
110
+ githubUrl?: string
111
+ }) {
112
+ // Handle aliases by resolving to target
113
+ if (item.kind === 'alias') {
114
+ const alias = item as GriffeAlias
115
+ const resolved = resolveAlias(alias, apiData)
116
+ if (resolved) {
117
+ // Pass the alias path as displayPath so title shows "strawberry.enum" not "strawberry.types.enum.enum"
118
+ const aliasDisplayPath = alias.path || `${apiData.name}.${alias.name}`
119
+ return <APIContent item={resolved} prefix={prefix} currentPath={currentPath} apiData={apiData} displayPath={aliasDisplayPath} githubUrl={githubUrl} />
120
+ }
121
+ // Could not resolve alias
122
+ return (
123
+ <div className="text-gray-600 dark:text-gray-300">
124
+ <p>Could not resolve alias: {alias.target_path}</p>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ switch (item.kind) {
130
+ case 'module':
131
+ return <ModuleDoc module={item as GriffeModule} prefix={prefix} showFull displayPath={displayPath} githubUrl={githubUrl} />
132
+
133
+ case 'class':
134
+ return <ClassDoc cls={item as GriffeClass} prefix={prefix} currentPath={currentPath} displayPath={displayPath} githubUrl={githubUrl} />
135
+
136
+ case 'function':
137
+ return <FunctionDoc fn={item as GriffeFunction} displayPath={displayPath} githubUrl={githubUrl} />
138
+
139
+ default:
140
+ return (
141
+ <div className="text-gray-600 dark:text-gray-300">
142
+ <p>Unknown item type: {item.kind}</p>
143
+ <pre className="mt-4 text-xs bg-gray-100 dark:bg-gray-800 p-4 rounded overflow-auto">
144
+ {JSON.stringify(item, null, 2)}
145
+ </pre>
146
+ </div>
147
+ )
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Main API documentation page component.
153
+ *
154
+ * Renders API documentation with a separate sidebar navigation.
155
+ * Automatically handles different item types (modules, classes, functions).
156
+ *
157
+ * @example
158
+ * // In your pages configuration:
159
+ * createDocsApp({
160
+ * pages: {
161
+ * 'api/APIPage': APIPage,
162
+ * // ...
163
+ * }
164
+ * })
165
+ */
166
+ export function APIPage({
167
+ apiData,
168
+ currentItem,
169
+ currentPath,
170
+ currentModule,
171
+ apiNav,
172
+ prefix,
173
+ logoUrl,
174
+ logoInvertedUrl,
175
+ footerLogoUrl,
176
+ footerLogoInvertedUrl,
177
+ githubUrl,
178
+ navLinks,
179
+ header,
180
+ headerHeight,
181
+ footer,
182
+ }: APIPageProps) {
183
+ // Determine what to render
184
+ const itemToRender = currentItem || apiData
185
+
186
+ // Determine the title
187
+ let title = 'API Reference'
188
+ if (itemToRender) {
189
+ const name = itemToRender.name || currentModule
190
+ const kind = itemToRender.kind
191
+ title = `${name} (${kind}) - API Reference`
192
+ }
193
+
194
+ // Generate table of contents
195
+ const tocItems = itemToRender ? generateTocItems(itemToRender, apiData) : []
196
+
197
+ return (
198
+ <APILayout
199
+ title={title}
200
+ apiNav={apiNav}
201
+ currentPath={currentPath}
202
+ logoUrl={logoUrl}
203
+ logoInvertedUrl={logoInvertedUrl}
204
+ footerLogoUrl={footerLogoUrl}
205
+ footerLogoInvertedUrl={footerLogoInvertedUrl}
206
+ githubUrl={githubUrl}
207
+ navLinks={navLinks}
208
+ rightSidebar={tocItems.length > 0 ? <TableOfContents items={tocItems} /> : undefined}
209
+ header={header}
210
+ headerHeight={headerHeight}
211
+ footer={footer}
212
+ >
213
+ <APIContent item={itemToRender} prefix={prefix} currentPath={currentPath} apiData={apiData} githubUrl={githubUrl} />
214
+ </APILayout>
215
+ )
216
+ }
@@ -0,0 +1,98 @@
1
+ import { Link } from '@inertiajs/react'
2
+
3
+ interface BreadcrumbItem {
4
+ label: string
5
+ href?: string
6
+ }
7
+
8
+ interface BreadcrumbProps {
9
+ items: BreadcrumbItem[]
10
+ className?: string
11
+ }
12
+
13
+ /**
14
+ * Breadcrumb navigation for API documentation pages.
15
+ * Shows the path: API > Module > Class
16
+ */
17
+ export function Breadcrumb({ items, className = '' }: BreadcrumbProps) {
18
+ if (items.length === 0) return null
19
+
20
+ return (
21
+ <nav className={`flex items-center gap-2 text-sm mb-4 ${className}`}>
22
+ {items.map((item, index) => (
23
+ <span key={index} className="flex items-center gap-2">
24
+ {index > 0 && (
25
+ <ChevronIcon className="text-gray-400 dark:text-gray-500" />
26
+ )}
27
+ {item.href ? (
28
+ <Link
29
+ href={item.href}
30
+ className="text-gray-500 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
31
+ >
32
+ {item.label}
33
+ </Link>
34
+ ) : (
35
+ <span className="text-gray-900 dark:text-white font-medium">
36
+ {item.label}
37
+ </span>
38
+ )}
39
+ </span>
40
+ ))}
41
+ </nav>
42
+ )
43
+ }
44
+
45
+ function ChevronIcon({ className }: { className?: string }) {
46
+ return (
47
+ <svg
48
+ className={`w-4 h-4 ${className}`}
49
+ fill="none"
50
+ viewBox="0 0 24 24"
51
+ stroke="currentColor"
52
+ >
53
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
54
+ </svg>
55
+ )
56
+ }
57
+
58
+ /**
59
+ * Generate breadcrumb items from a path like "/api/strawberry/schema/schema/Schema"
60
+ * Combines module path into dotted notation: API > strawberry.schema.schema > Schema
61
+ */
62
+ export function generateBreadcrumb(currentPath: string, prefix: string = '/api'): BreadcrumbItem[] {
63
+ const items: BreadcrumbItem[] = [{ label: 'API', href: prefix }]
64
+
65
+ // Remove prefix from path
66
+ const relativePath = currentPath.startsWith(prefix)
67
+ ? currentPath.slice(prefix.length)
68
+ : currentPath
69
+
70
+ // Split path into parts
71
+ const parts = relativePath.split('/').filter(Boolean)
72
+
73
+ if (parts.length === 0) return items
74
+
75
+ // If we have parts, combine all but the last into a module path
76
+ if (parts.length === 1) {
77
+ // Just one part - show it as the current item
78
+ items.push({ label: parts[0] })
79
+ } else {
80
+ // Multiple parts: combine all but last into module path
81
+ const moduleParts = parts.slice(0, -1)
82
+ const finalItem = parts[parts.length - 1]
83
+
84
+ // Build href for module path (link to the parent)
85
+ const moduleHref = prefix + '/' + moduleParts.join('/')
86
+
87
+ // Add module path as dotted notation
88
+ items.push({
89
+ label: moduleParts.join('.'),
90
+ href: moduleHref,
91
+ })
92
+
93
+ // Add final item (class/function name)
94
+ items.push({ label: finalItem })
95
+ }
96
+
97
+ return items
98
+ }
@@ -0,0 +1,257 @@
1
+ import { useState } from 'react'
2
+ import type { GriffeClass, GriffeFunction, GriffeAttribute, GriffeExpression } from '../../types'
3
+ import { Docstring } from './Docstring'
4
+ import { FunctionDoc } from './FunctionDoc'
5
+ import { CodeSpan } from './CodeSpan'
6
+
7
+ /**
8
+ * Render a type annotation expression or value to string
9
+ */
10
+ function renderExpression(expr: GriffeExpression | string | undefined): string {
11
+ if (!expr) return ''
12
+ if (typeof expr === 'string') return expr
13
+ if (expr.str) return expr.str
14
+ if (expr.canonical) return expr.canonical
15
+
16
+ const exprAny = expr as any
17
+
18
+ // Handle ExprName with member reference
19
+ if (expr.name && typeof expr.name === 'string') return expr.name
20
+
21
+ // Handle ExprBoolOp (like `config or StrawberryConfig()`)
22
+ if (exprAny.cls === 'ExprBoolOp' && exprAny.operator && Array.isArray(exprAny.values)) {
23
+ return exprAny.values.map((v: any) => renderExpression(v)).join(` ${exprAny.operator} `)
24
+ }
25
+
26
+ // Handle ExprBinOp (like `type | None`)
27
+ if (exprAny.cls === 'ExprBinOp' && exprAny.left && exprAny.right) {
28
+ const left = renderExpression(exprAny.left)
29
+ const right = renderExpression(exprAny.right)
30
+ const op = exprAny.operator || '|'
31
+ return `${left} ${op} ${right}`
32
+ }
33
+
34
+ // Handle ExprCall (like `StrawberryConfig()`)
35
+ if (exprAny.cls === 'ExprCall' && exprAny.function) {
36
+ const funcName = renderExpression(exprAny.function)
37
+ const args = Array.isArray(exprAny.arguments)
38
+ ? exprAny.arguments.map((a: any) => renderExpression(a)).join(', ')
39
+ : ''
40
+ return `${funcName}(${args})`
41
+ }
42
+
43
+ // Handle ExprAttribute (like contextlib.asynccontextmanager)
44
+ if (exprAny.cls === 'ExprAttribute' && Array.isArray(exprAny.values)) {
45
+ return exprAny.values.map((v: any) => renderExpression(v)).join('.')
46
+ }
47
+
48
+ // Handle ExprList and ExprTuple
49
+ if ('elements' in exprAny && Array.isArray(exprAny.elements)) {
50
+ const inner = exprAny.elements.map((el: any) => renderExpression(el)).join(', ')
51
+ return exprAny.cls === 'ExprTuple' ? `(${inner})` : `[${inner}]`
52
+ }
53
+
54
+ // Handle ExprDict
55
+ if (exprAny.cls === 'ExprDict' && Array.isArray(exprAny.keys) && Array.isArray(exprAny.values)) {
56
+ const pairs = exprAny.keys.map((k: any, i: number) =>
57
+ `${renderExpression(k)}: ${renderExpression(exprAny.values[i])}`
58
+ ).join(', ')
59
+ return `{${pairs}}`
60
+ }
61
+
62
+ // Handle ExprSubscript (like Dict[str, int])
63
+ if (exprAny.left && exprAny.slice) {
64
+ const left = renderExpression(exprAny.left)
65
+ const slice = renderExpression(exprAny.slice)
66
+ return `${left}[${slice}]`
67
+ }
68
+
69
+ // Handle slice expressions
70
+ if ('slice' in exprAny && exprAny.slice && !exprAny.left) {
71
+ return renderExpression(exprAny.slice)
72
+ }
73
+
74
+ // Fallback for unknown expressions - try to avoid [object Object]
75
+ if (typeof expr === 'object') {
76
+ return JSON.stringify(expr)
77
+ }
78
+
79
+ return String(expr)
80
+ }
81
+
82
+ /**
83
+ * Collapsible method item with arrow indicator (strawberry.rocks style)
84
+ */
85
+ function CollapsibleMethod({ method }: { method: GriffeFunction }) {
86
+ const [expanded, setExpanded] = useState(false)
87
+
88
+ return (
89
+ <div className="border-b border-gray-200 dark:border-gray-700 last:border-b-0">
90
+ <button
91
+ onClick={() => setExpanded(!expanded)}
92
+ className="w-full flex items-center gap-2 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
93
+ >
94
+ <span className="font-mono text-base font-semibold text-gray-900 dark:text-white">
95
+ {method.name}
96
+ </span>
97
+ <span className="text-gray-400 text-sm">
98
+ {expanded ? '▲' : '▼'}
99
+ </span>
100
+ </button>
101
+ {expanded && (
102
+ <div className="pb-6">
103
+ <FunctionDoc fn={method} isMethod showName={false} />
104
+ </div>
105
+ )}
106
+ </div>
107
+ )
108
+ }
109
+
110
+ interface ClassDocProps {
111
+ cls: GriffeClass
112
+ /** URL prefix for links */
113
+ prefix?: string
114
+ /** Current path for breadcrumb */
115
+ currentPath?: string
116
+ /** GitHub repo URL for source links */
117
+ githubUrl?: string
118
+ /** Additional CSS class */
119
+ className?: string
120
+ /** Override display path (e.g., for aliases to show alias name instead of target path) */
121
+ displayPath?: string
122
+ }
123
+
124
+ /**
125
+ * Renders documentation for a class matching strawberry.rocks design.
126
+ * Includes: Title, Constructor, Methods (collapsible), Attributes, and footer.
127
+ */
128
+ export function ClassDoc({ cls, prefix: _prefix = '/api', currentPath: _currentPath, githubUrl, className = '', displayPath }: ClassDocProps) {
129
+ const members = cls.members ?? {}
130
+
131
+ // Separate members by type
132
+ const methods: GriffeFunction[] = []
133
+ const attributes: GriffeAttribute[] = []
134
+
135
+ for (const member of Object.values(members)) {
136
+ // Skip private members
137
+ if (member.name.startsWith('_') && !member.name.startsWith('__')) continue
138
+
139
+ if (member.kind === 'function') {
140
+ methods.push(member as GriffeFunction)
141
+ } else if (member.kind === 'attribute') {
142
+ attributes.push(member as GriffeAttribute)
143
+ }
144
+ }
145
+
146
+ // Sort methods: __init__ first, then public methods alphabetically, skip other dunders
147
+ const initMethod = methods.find(m => m.name === '__init__')
148
+ const publicMethods = methods
149
+ .filter(m => m.name !== '__init__' && !m.name.startsWith('_'))
150
+ .sort((a, b) => a.name.localeCompare(b.name))
151
+
152
+ // Sort attributes alphabetically, skip private ones
153
+ const publicAttributes = attributes
154
+ .filter(a => !a.name.startsWith('_'))
155
+ .sort((a, b) => a.name.localeCompare(b.name))
156
+
157
+ // Get relative filepath for display (prefer package-relative path)
158
+ const relativeFilepath = cls.relative_package_filepath || cls.relative_filepath || cls.filepath
159
+
160
+ // Build GitHub URL for source link
161
+ const githubSourceUrl = githubUrl && relativeFilepath && cls.lineno
162
+ ? `${githubUrl}/blob/main/${relativeFilepath}#L${cls.lineno}-L${cls.endlineno || cls.lineno}`
163
+ : undefined
164
+
165
+ return (
166
+ <div className={className}>
167
+ {/* Title - monospace like strawberry.rocks */}
168
+ <h1 id={cls.name} className="font-mono text-2xl font-normal text-gray-900 dark:text-white mb-8">
169
+ {displayPath || cls.path || cls.name}
170
+ </h1>
171
+
172
+ {/* Constructor section */}
173
+ {initMethod && (
174
+ <section id="constructor" className="mb-8">
175
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
176
+ Constructor:
177
+ </h2>
178
+
179
+ {/* Docstring description */}
180
+ {initMethod.docstring && (
181
+ <div className="mb-6">
182
+ <Docstring docstring={initMethod.docstring} showOnlyText />
183
+ </div>
184
+ )}
185
+
186
+ <FunctionDoc fn={initMethod} isMethod showName={false} />
187
+ </section>
188
+ )}
189
+
190
+ {/* Class docstring if no __init__ */}
191
+ {!initMethod && cls.docstring && (
192
+ <section className="mb-8">
193
+ <Docstring docstring={cls.docstring} />
194
+ </section>
195
+ )}
196
+
197
+ {/* Methods section - collapsible */}
198
+ {publicMethods.length > 0 && (
199
+ <section id="methods" className="mb-8">
200
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
201
+ Methods:
202
+ </h2>
203
+ <div>
204
+ {publicMethods.map((method) => (
205
+ <CollapsibleMethod key={method.name} method={method} />
206
+ ))}
207
+ </div>
208
+ </section>
209
+ )}
210
+
211
+ {/* Attributes section */}
212
+ {publicAttributes.length > 0 && (
213
+ <section id="attributes" className="mb-8">
214
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
215
+ Attributes:
216
+ </h2>
217
+ <div className="space-y-2">
218
+ {publicAttributes.map((attr) => (
219
+ <div key={attr.name} className="flex items-baseline gap-2">
220
+ <CodeSpan>{attr.name}:</CodeSpan>
221
+ {attr.annotation && (
222
+ <span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
223
+ {renderExpression(attr.annotation)}
224
+ </span>
225
+ )}
226
+ </div>
227
+ ))}
228
+ </div>
229
+ </section>
230
+ )}
231
+
232
+ {/* Footer with file path and GitHub link */}
233
+ {relativeFilepath && (
234
+ <footer className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 space-y-4">
235
+ <p className="flex items-center gap-2">
236
+ <span className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
237
+ File path:
238
+ </span>
239
+ <CodeSpan allowCopy>{relativeFilepath}</CodeSpan>
240
+ </p>
241
+ {githubSourceUrl && (
242
+ <p>
243
+ <a
244
+ href={githubSourceUrl}
245
+ target="_blank"
246
+ rel="noopener noreferrer"
247
+ className="text-sm font-semibold text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 uppercase tracking-wide"
248
+ >
249
+ Open in GitHub
250
+ </a>
251
+ </p>
252
+ )}
253
+ </footer>
254
+ )}
255
+ </div>
256
+ )
257
+ }
@@ -0,0 +1,53 @@
1
+ import { cn } from '../../lib/utils'
2
+
3
+ interface CodeSpanProps {
4
+ children: React.ReactNode
5
+ /** Visual variant */
6
+ variant?: 'default' | 'simple'
7
+ /** Allow copy on click */
8
+ allowCopy?: boolean
9
+ /** Additional CSS class */
10
+ className?: string
11
+ }
12
+
13
+ /**
14
+ * Styled code span component matching strawberry.rocks design.
15
+ * Uses coral/pink color for code badges.
16
+ */
17
+ export function CodeSpan({ children, variant = 'default', allowCopy = false, className }: CodeSpanProps) {
18
+ const handleCopy = () => {
19
+ if (allowCopy && typeof children === 'string') {
20
+ navigator.clipboard.writeText(children)
21
+ }
22
+ }
23
+
24
+ if (variant === 'simple') {
25
+ return (
26
+ <code
27
+ onClick={allowCopy ? handleCopy : undefined}
28
+ className={cn(
29
+ 'font-mono text-[0.9em] font-semibold text-gray-900 dark:text-white',
30
+ allowCopy && 'cursor-pointer hover:text-primary-600 dark:hover:text-primary-400',
31
+ className
32
+ )}
33
+ >
34
+ {children}
35
+ </code>
36
+ )
37
+ }
38
+
39
+ return (
40
+ <code
41
+ onClick={allowCopy ? handleCopy : undefined}
42
+ className={cn(
43
+ 'inline-flex items-center px-2 py-0.5 rounded font-mono text-sm',
44
+ 'bg-red-50 text-red-600 border border-red-200',
45
+ 'dark:bg-red-900/20 dark:text-red-400 dark:border-red-800/50',
46
+ allowCopy && 'cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/30',
47
+ className
48
+ )}
49
+ >
50
+ {children}
51
+ </code>
52
+ )
53
+ }