@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.
@@ -1,13 +1,151 @@
1
+ import { useState } from 'react'
1
2
  import { Link } from '@inertiajs/react'
2
3
  import { cn } from '../lib/utils'
3
4
  import { DocSetSelector } from './DocSetSelector'
4
- import type { SidebarProps } from '../types'
5
+ import type { SidebarProps, NavSection } from '../types'
6
+
7
+ /**
8
+ * Chevron icon for collapsible sections
9
+ */
10
+ function ChevronIcon({ expanded, className }: { expanded: boolean; className?: string }) {
11
+ return (
12
+ <svg
13
+ className={cn('w-4 h-4 transition-transform duration-200', expanded && 'rotate-90', className)}
14
+ fill="none"
15
+ viewBox="0 0 24 24"
16
+ stroke="currentColor"
17
+ >
18
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
19
+ </svg>
20
+ )
21
+ }
22
+
23
+ /**
24
+ * Collapsible navigation section
25
+ */
26
+ function CollapsibleSection({
27
+ section,
28
+ currentPath,
29
+ defaultExpanded = true,
30
+ compact = false,
31
+ }: {
32
+ section: NavSection
33
+ currentPath: string
34
+ defaultExpanded?: boolean
35
+ compact?: boolean
36
+ }) {
37
+ // Check if current path is in this section
38
+ const isActive = section.items.some(
39
+ (item) => currentPath === item.href || currentPath + '/' === item.href
40
+ )
41
+ const [expanded, setExpanded] = useState(defaultExpanded || isActive)
42
+
43
+ return (
44
+ <div>
45
+ <button
46
+ onClick={() => setExpanded(!expanded)}
47
+ className="w-full flex items-center justify-between mb-2 group"
48
+ >
49
+ <h3 className={cn(
50
+ 'text-sm font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400',
51
+ 'group-hover:text-gray-700 dark:group-hover:text-gray-300 transition-colors'
52
+ )}>
53
+ {section.title}
54
+ </h3>
55
+ <ChevronIcon expanded={expanded} className="text-gray-400 dark:text-gray-500" />
56
+ </button>
57
+ {expanded && (
58
+ <ul className={cn(
59
+ 'border-l-2 border-gray-200 dark:border-gray-700',
60
+ compact ? 'space-y-0.5' : 'space-y-1.5'
61
+ )}>
62
+ {section.items.map((item) => (
63
+ <li key={item.href}>
64
+ <Link
65
+ href={item.href}
66
+ className={cn(
67
+ 'block border-l-2 py-1 pl-4 leading-snug transition-colors -ml-0.5',
68
+ compact ? 'text-sm' : 'text-[15px]',
69
+ currentPath === item.href || currentPath + '/' === item.href
70
+ ? 'border-primary-500 text-gray-900 dark:text-white font-semibold'
71
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:border-primary-300 dark:hover:border-primary-400 hover:text-gray-900 dark:hover:text-white'
72
+ )}
73
+ >
74
+ {item.title}
75
+ </Link>
76
+ </li>
77
+ ))}
78
+ </ul>
79
+ )}
80
+ </div>
81
+ )
82
+ }
83
+
84
+ /**
85
+ * Static navigation section (non-collapsible)
86
+ */
87
+ function StaticSection({
88
+ section,
89
+ currentPath,
90
+ compact = false,
91
+ }: {
92
+ section: NavSection
93
+ currentPath: string
94
+ compact?: boolean
95
+ }) {
96
+ return (
97
+ <div>
98
+ <h3 className="mb-3 text-sm font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400">
99
+ {section.title}
100
+ </h3>
101
+ <ul className={cn(
102
+ 'border-l-2 border-gray-200 dark:border-gray-700',
103
+ compact ? 'space-y-0.5' : 'space-y-1.5'
104
+ )}>
105
+ {section.items.map((item) => (
106
+ <li key={item.href}>
107
+ <Link
108
+ href={item.href}
109
+ className={cn(
110
+ 'block border-l-2 py-1 pl-4 leading-snug transition-colors -ml-0.5',
111
+ compact ? 'text-sm' : 'text-[15px]',
112
+ currentPath === item.href || currentPath + '/' === item.href
113
+ ? 'border-primary-500 text-gray-900 dark:text-white font-semibold'
114
+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:border-primary-300 dark:hover:border-primary-400 hover:text-gray-900 dark:hover:text-white'
115
+ )}
116
+ >
117
+ {item.title}
118
+ </Link>
119
+ </li>
120
+ ))}
121
+ </ul>
122
+ </div>
123
+ )
124
+ }
125
+
126
+ export interface ExtendedSidebarProps extends SidebarProps {
127
+ /** Use compact styling (smaller text) */
128
+ compact?: boolean
129
+ /** Make sections collapsible */
130
+ collapsible?: boolean
131
+ /** Collapse sections with more than N items by default */
132
+ collapseThreshold?: number
133
+ }
5
134
 
6
135
  /**
7
136
  * Documentation sidebar with section-based navigation.
8
- * In multi-docs mode, includes a dropdown selector at the top.
137
+ * Supports both docs and API navigation styles.
9
138
  */
10
- export function Sidebar({ nav, currentPath, className, docSets, currentDocSet }: SidebarProps) {
139
+ export function Sidebar({
140
+ nav,
141
+ currentPath,
142
+ className,
143
+ docSets,
144
+ currentDocSet,
145
+ compact = false,
146
+ collapsible = false,
147
+ collapseThreshold = 10,
148
+ }: ExtendedSidebarProps) {
11
149
  return (
12
150
  <nav className={cn('space-y-6', className)}>
13
151
  {/* Doc Set Selector - only shown in multi-docs mode */}
@@ -16,31 +154,36 @@ export function Sidebar({ nav, currentPath, className, docSets, currentDocSet }:
16
154
  )}
17
155
 
18
156
  {/* Navigation Sections */}
19
- <div className="space-y-8">
20
- {nav.map((section) => (
21
- <div key={section.title}>
22
- <h3 className="mb-3 text-xs font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400">
23
- {section.title}
24
- </h3>
25
- <ul className="space-y-1 border-l-2 border-gray-200 dark:border-gray-700">
26
- {section.items.map((item) => (
27
- <li key={item.href}>
28
- <Link
29
- href={item.href}
30
- className={cn(
31
- 'block border-l-2 py-1.5 pl-4 text-base transition-colors -ml-0.5',
32
- currentPath === item.href
33
- ? 'border-primary-500 text-gray-900 dark:text-white font-bold'
34
- : 'border-transparent text-gray-600 dark:text-gray-300 hover:border-primary-300 dark:hover:border-primary-400 hover:text-gray-900 dark:hover:text-white'
35
- )}
36
- >
37
- {item.title}
38
- </Link>
39
- </li>
40
- ))}
41
- </ul>
42
- </div>
43
- ))}
157
+ <div className={compact ? 'space-y-4' : 'space-y-6'}>
158
+ {nav.map((section) => {
159
+ // Determine if this section should be collapsible
160
+ const shouldCollapse = collapsible && section.items.length > collapseThreshold
161
+ // Check if current path is in this section
162
+ const isActive = section.items.some(
163
+ (item) => currentPath === item.href || currentPath + '/' === item.href
164
+ )
165
+
166
+ if (shouldCollapse) {
167
+ return (
168
+ <CollapsibleSection
169
+ key={section.title}
170
+ section={section}
171
+ currentPath={currentPath}
172
+ defaultExpanded={isActive}
173
+ compact={compact}
174
+ />
175
+ )
176
+ }
177
+
178
+ return (
179
+ <StaticSection
180
+ key={section.title}
181
+ section={section}
182
+ currentPath={currentPath}
183
+ compact={compact}
184
+ />
185
+ )
186
+ })}
44
187
  </div>
45
188
  </nav>
46
189
  )
@@ -5,7 +5,7 @@ import type { TableOfContentsProps } from '../types'
5
5
  * Table of contents component with scroll spy functionality.
6
6
  * Displays "ON THIS PAGE" sidebar with heading links.
7
7
  */
8
- export function TableOfContents({ items, className = '' }: TableOfContentsProps) {
8
+ export function TableOfContents({ items, className = '', ...props }: TableOfContentsProps) {
9
9
  const [activeId, setActiveId] = useState<string>(() => {
10
10
  // Initialize with hash from URL if present
11
11
  if (typeof window !== 'undefined' && window.location.hash) {
@@ -149,11 +149,11 @@ export function TableOfContents({ items, className = '' }: TableOfContentsProps)
149
149
  }
150
150
 
151
151
  return (
152
- <nav className={className}>
153
- <h3 className="mb-3 text-xs font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400">
152
+ <nav className={className} {...props}>
153
+ <h3 className="mb-3 text-sm font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400">
154
154
  On this page
155
155
  </h3>
156
- <ul className="space-y-1 border-l-2 border-gray-200 dark:border-gray-700">
156
+ <ul className="space-y-1.5 border-l-2 border-gray-200 dark:border-gray-700">
157
157
  {items.map((item) => {
158
158
  const isActive = activeId === item.id
159
159
  const indent = item.level === 3 ? 'pl-6' : 'pl-4'
@@ -163,13 +163,13 @@ export function TableOfContents({ items, className = '' }: TableOfContentsProps)
163
163
  <a
164
164
  href={`#${item.id}`}
165
165
  onClick={(e) => handleClick(e, item.id)}
166
- className={`block border-l-2 py-1.5 ${indent} -ml-0.5 text-sm transition-colors ${
166
+ className={`block border-l-2 py-1 ${indent} -ml-0.5 text-base leading-snug transition-colors ${
167
167
  isActive
168
168
  ? 'border-primary-500 text-gray-900 dark:text-white font-bold'
169
169
  : 'border-transparent text-gray-600 dark:text-gray-300 hover:border-primary-300 dark:hover:border-primary-400 hover:text-gray-900 dark:hover:text-white'
170
170
  }`}
171
171
  >
172
- {item.text}
172
+ {item.text || item.title}
173
173
  </a>
174
174
  </li>
175
175
  )
@@ -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
+ }