@usecross/docs 0.11.0 → 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,231 @@
1
+ import { Head, Link, usePage } from '@inertiajs/react'
2
+ import { useState } from 'react'
3
+ import { ThemeToggle } from '../ThemeToggle'
4
+ import { useTheme } from '../ThemeProvider'
5
+ import { MobileMenuButton } from '../DocsLayout'
6
+ import { Sidebar } from '../Sidebar'
7
+ import type { NavSection, SharedProps } from '../../types'
8
+
9
+ interface APILayoutProps {
10
+ children: React.ReactNode
11
+ title: string
12
+ apiNav: NavSection[]
13
+ currentPath: string
14
+ logoUrl?: string
15
+ logoInvertedUrl?: string
16
+ footerLogoUrl?: string
17
+ footerLogoInvertedUrl?: string
18
+ githubUrl?: string
19
+ navLinks?: Array<{ label: string; href: string }>
20
+ /** Right sidebar content (e.g., table of contents) */
21
+ rightSidebar?: React.ReactNode
22
+ /** Custom header component (replaces entire header). Can be a ReactNode or a function that receives mobile menu props. */
23
+ header?: React.ReactNode | ((props: { mobileMenuOpen: boolean; toggleMobileMenu: () => void }) => React.ReactNode)
24
+ /** Header height in pixels. Used to calculate content offset. Defaults to 64 (h-16). */
25
+ headerHeight?: number
26
+ /** Custom footer component */
27
+ footer?: React.ReactNode
28
+ }
29
+
30
+ /** Shared props type for API pages */
31
+ interface APISharedProps extends SharedProps {
32
+ apiNav?: NavSection[]
33
+ }
34
+
35
+ function GitHubIcon() {
36
+ return (
37
+ <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
38
+ <path
39
+ fillRule="evenodd"
40
+ d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
41
+ clipRule="evenodd"
42
+ />
43
+ </svg>
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Layout component for API documentation pages.
49
+ * Uses the shared Sidebar component with compact styling and collapsible sections.
50
+ */
51
+ export function APILayout({
52
+ children,
53
+ title,
54
+ apiNav,
55
+ currentPath,
56
+ logoUrl: propLogoUrl,
57
+ logoInvertedUrl: propLogoInvertedUrl,
58
+ footerLogoUrl: propFooterLogoUrl,
59
+ footerLogoInvertedUrl: propFooterLogoInvertedUrl,
60
+ githubUrl: propGithubUrl,
61
+ navLinks: propNavLinks,
62
+ rightSidebar,
63
+ header,
64
+ headerHeight: propHeaderHeight = 64,
65
+ footer,
66
+ }: APILayoutProps) {
67
+ const sharedProps = usePage<{ props: APISharedProps }>().props as unknown as APISharedProps
68
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
69
+ const { resolvedTheme } = useTheme()
70
+ const headerHeight = propHeaderHeight
71
+
72
+ // Merge props - component props take precedence over shared props from Python
73
+ const logoUrl = propLogoUrl ?? sharedProps.logoUrl
74
+ const logoInvertedUrl = propLogoInvertedUrl ?? sharedProps.logoInvertedUrl
75
+ const githubUrl = propGithubUrl ?? sharedProps.githubUrl
76
+ const navLinks = propNavLinks ?? sharedProps.navLinks ?? []
77
+
78
+ const headerLogo = logoInvertedUrl ? (
79
+ <img src={logoInvertedUrl} alt="Logo" className="h-8" />
80
+ ) : logoUrl ? (
81
+ <img src={logoUrl} alt="Logo" className="h-8" />
82
+ ) : null
83
+
84
+ const footerLogoUrl = propFooterLogoUrl || sharedProps.footerLogoUrl || logoUrl
85
+ const footerLogoInvertedUrl = propFooterLogoInvertedUrl || sharedProps.footerLogoInvertedUrl || logoInvertedUrl
86
+ const currentFooterLogoUrl = resolvedTheme === 'dark' ? (footerLogoInvertedUrl || footerLogoUrl) : footerLogoUrl
87
+ const footerLogo = currentFooterLogoUrl ? (
88
+ <img src={currentFooterLogoUrl} alt="Logo" className="h-6" />
89
+ ) : null
90
+
91
+ return (
92
+ <div className="min-h-screen bg-white dark:bg-[#0f0f0f] flex flex-col transition-colors duration-200">
93
+ <Head title={title} />
94
+
95
+ {/* Fixed navigation */}
96
+ {(typeof header === 'function'
97
+ ? header({ mobileMenuOpen, toggleMobileMenu: () => setMobileMenuOpen(!mobileMenuOpen) })
98
+ : header) || (
99
+ <nav className="fixed w-full z-50 bg-white/95 dark:bg-[#0f0f0f]/95 backdrop-blur-sm border-b border-gray-200 dark:border-gray-800 transition-colors">
100
+ <div className="px-4 lg:px-10">
101
+ <div className="flex justify-between h-16 items-center">
102
+ <div className="flex items-center gap-2">
103
+ <MobileMenuButton onClick={() => setMobileMenuOpen(!mobileMenuOpen)} isOpen={mobileMenuOpen} />
104
+ {headerLogo ? (
105
+ <Link href="/" className="flex items-center">
106
+ {headerLogo}
107
+ </Link>
108
+ ) : (
109
+ <Link href="/" className="font-bold text-lg text-gray-900 dark:text-white">
110
+ Docs
111
+ </Link>
112
+ )}
113
+ </div>
114
+ <div className="flex items-center gap-6">
115
+ <div className="-mr-2">
116
+ <ThemeToggle size="sm" />
117
+ </div>
118
+ {navLinks.map((link) => (
119
+ <Link
120
+ key={link.href}
121
+ href={link.href}
122
+ className="hidden sm:block text-gray-700 dark:text-gray-300 font-medium hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
123
+ >
124
+ {link.label}
125
+ </Link>
126
+ ))}
127
+ {githubUrl && (
128
+ <a
129
+ href={githubUrl}
130
+ target="_blank"
131
+ rel="noopener noreferrer"
132
+ className="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
133
+ >
134
+ <GitHubIcon />
135
+ </a>
136
+ )}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </nav>
141
+ )}
142
+
143
+ {/* Mobile sidebar */}
144
+ {mobileMenuOpen && (
145
+ <div className="fixed inset-0 z-40 lg:hidden">
146
+ <div className="fixed inset-0 bg-black/50 dark:bg-black/70" onClick={() => setMobileMenuOpen(false)} />
147
+ <div
148
+ className="fixed inset-y-0 left-0 w-72 overflow-y-auto bg-white dark:bg-[#0f0f0f] px-4 py-6 border-r border-gray-200 dark:border-gray-800 transition-colors"
149
+ style={{ paddingTop: headerHeight + 16 }}
150
+ >
151
+ <Sidebar
152
+ nav={apiNav}
153
+ currentPath={currentPath}
154
+ />
155
+ </div>
156
+ </div>
157
+ )}
158
+
159
+ {/* Main content area */}
160
+ <div className="bg-white dark:bg-[#0f0f0f] w-full flex-1 transition-colors" style={{ paddingTop: headerHeight }}>
161
+ <div className="flex">
162
+ {/* Desktop sidebar */}
163
+ <aside
164
+ className="hidden lg:block w-64 shrink-0 border-r border-gray-200 dark:border-gray-800 transition-colors"
165
+ style={{ minHeight: `calc(100vh - ${headerHeight}px)` }}
166
+ >
167
+ <div
168
+ className="sticky px-6 py-6 overflow-y-auto"
169
+ style={{ top: headerHeight, maxHeight: `calc(100vh - ${headerHeight}px)` }}
170
+ >
171
+ <Sidebar
172
+ nav={apiNav}
173
+ currentPath={currentPath}
174
+ />
175
+ </div>
176
+ </aside>
177
+
178
+ {/* Right section: content + TOC + footer */}
179
+ <div className="flex-1 min-w-0 flex flex-col">
180
+ <div className="flex-1 p-4 lg:px-10 lg:py-6">
181
+ <div className="flex gap-5">
182
+ {/* Main content */}
183
+ <main className="min-w-0 w-full max-w-4xl">
184
+ {children}
185
+ </main>
186
+
187
+ {/* Table of Contents - desktop only */}
188
+ {rightSidebar && (
189
+ <aside className="hidden xl:block w-56 shrink-0 transition-colors">
190
+ <div
191
+ className="sticky overflow-y-auto"
192
+ style={{ top: headerHeight + 24, maxHeight: `calc(100vh - ${headerHeight + 24}px)` }}
193
+ >
194
+ {rightSidebar}
195
+ </div>
196
+ </aside>
197
+ )}
198
+ </div>
199
+ </div>
200
+
201
+ {/* Footer */}
202
+ {footer || (
203
+ <footer className="border-t border-gray-200 dark:border-gray-800 py-8 px-4 lg:px-10 transition-colors">
204
+ <div className="flex flex-col md:flex-row justify-between items-center gap-6">
205
+ {footerLogo && <Link href="/">{footerLogo}</Link>}
206
+ <div className="flex gap-8 text-sm text-gray-600 dark:text-gray-400">
207
+ {navLinks.map((link) => (
208
+ <Link key={link.href} href={link.href} className="hover:text-black dark:hover:text-white transition-colors">
209
+ {link.label}
210
+ </Link>
211
+ ))}
212
+ {githubUrl && (
213
+ <a
214
+ href={githubUrl}
215
+ target="_blank"
216
+ rel="noopener noreferrer"
217
+ className="hover:text-black dark:hover:text-white transition-colors"
218
+ >
219
+ GitHub
220
+ </a>
221
+ )}
222
+ </div>
223
+ </div>
224
+ </footer>
225
+ )}
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ )
231
+ }
@@ -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
+ }