@usecross/docs 0.6.0 → 0.7.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,239 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import { router } from '@inertiajs/react'
3
+ import { cn } from '../lib/utils'
4
+ import type { DocSetMeta } from '../types'
5
+
6
+ interface DocSetSelectorProps {
7
+ docSets: DocSetMeta[]
8
+ currentDocSet: string
9
+ className?: string
10
+ }
11
+
12
+ // Chevron icon with up/down indicators like Fumadocs
13
+ const ChevronUpDownIcon = ({ className }: { className?: string }) => (
14
+ <svg
15
+ className={className}
16
+ viewBox="0 0 16 16"
17
+ fill="none"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ >
20
+ <path
21
+ d="M5 6l3-3 3 3M5 10l3 3 3-3"
22
+ stroke="currentColor"
23
+ strokeWidth="1.5"
24
+ strokeLinecap="round"
25
+ strokeLinejoin="round"
26
+ />
27
+ </svg>
28
+ )
29
+
30
+ // Checkmark for selected state
31
+ const CheckIcon = ({ className }: { className?: string }) => (
32
+ <svg
33
+ className={className}
34
+ viewBox="0 0 16 16"
35
+ fill="none"
36
+ xmlns="http://www.w3.org/2000/svg"
37
+ >
38
+ <path
39
+ d="M3.5 8.5l3 3 6-6.5"
40
+ stroke="currentColor"
41
+ strokeWidth="1.75"
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ />
45
+ </svg>
46
+ )
47
+
48
+ // Default package/docs icon when no iconUrl is provided
49
+ const PackageIcon = ({ className }: { className?: string }) => (
50
+ <svg
51
+ className={className}
52
+ viewBox="0 0 20 20"
53
+ fill="none"
54
+ xmlns="http://www.w3.org/2000/svg"
55
+ >
56
+ <path
57
+ d="M10 2L17 6v8l-7 4-7-4V6l7-4z"
58
+ stroke="currentColor"
59
+ strokeWidth="1.5"
60
+ strokeLinejoin="round"
61
+ />
62
+ <path
63
+ d="M10 10v8M10 10l7-4M10 10L3 6"
64
+ stroke="currentColor"
65
+ strokeWidth="1.5"
66
+ strokeLinecap="round"
67
+ strokeLinejoin="round"
68
+ />
69
+ </svg>
70
+ )
71
+
72
+ /**
73
+ * Dropdown selector for switching between documentation sets.
74
+ * Inspired by Fumadocs design - clean and minimal.
75
+ */
76
+ export function DocSetSelector({ docSets, currentDocSet, className }: DocSetSelectorProps) {
77
+ const [isOpen, setIsOpen] = useState(false)
78
+ const dropdownRef = useRef<HTMLDivElement>(null)
79
+
80
+ const current = docSets.find((ds) => ds.slug === currentDocSet) || docSets[0]
81
+
82
+ // Close dropdown when clicking outside
83
+ useEffect(() => {
84
+ const handleClickOutside = (event: MouseEvent) => {
85
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
86
+ setIsOpen(false)
87
+ }
88
+ }
89
+
90
+ if (isOpen) {
91
+ document.addEventListener('mousedown', handleClickOutside)
92
+ return () => document.removeEventListener('mousedown', handleClickOutside)
93
+ }
94
+ }, [isOpen])
95
+
96
+ // Close on escape key
97
+ useEffect(() => {
98
+ const handleEscape = (event: KeyboardEvent) => {
99
+ if (event.key === 'Escape') setIsOpen(false)
100
+ }
101
+
102
+ if (isOpen) {
103
+ document.addEventListener('keydown', handleEscape)
104
+ return () => document.removeEventListener('keydown', handleEscape)
105
+ }
106
+ }, [isOpen])
107
+
108
+ const handleSelect = (docSet: DocSetMeta) => {
109
+ setIsOpen(false)
110
+ if (docSet.slug !== currentDocSet) {
111
+ router.visit(`${docSet.prefix}/`)
112
+ }
113
+ }
114
+
115
+ return (
116
+ <div className={cn('relative', className)} ref={dropdownRef}>
117
+ {/* Trigger Button - Clean, flat design like Fumadocs */}
118
+ <button
119
+ onClick={() => setIsOpen(!isOpen)}
120
+ className={cn(
121
+ 'w-full flex items-center gap-2.5 px-3 py-2',
122
+ 'bg-gray-100/80 dark:bg-white/5',
123
+ 'border border-gray-200 dark:border-white/10',
124
+ 'rounded-lg',
125
+ 'hover:bg-gray-200/80 dark:hover:bg-white/10',
126
+ 'transition-colors duration-150',
127
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50'
128
+ )}
129
+ aria-label="Select documentation"
130
+ aria-expanded={isOpen}
131
+ aria-haspopup="listbox"
132
+ >
133
+ {/* Icon */}
134
+ <div className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-gray-600 dark:text-gray-400">
135
+ {current.icon ? (
136
+ <span className="text-base leading-none">{current.icon}</span>
137
+ ) : current.iconUrl ? (
138
+ <img src={current.iconUrl} alt="" className="w-5 h-5" />
139
+ ) : (
140
+ <PackageIcon className="w-5 h-5" />
141
+ )}
142
+ </div>
143
+
144
+ {/* Text */}
145
+ <span className="flex-1 text-left text-sm font-medium text-gray-900 dark:text-white truncate">
146
+ {current.name}
147
+ </span>
148
+
149
+ {/* Chevron */}
150
+ <ChevronUpDownIcon className="flex-shrink-0 w-4 h-4 text-gray-400 dark:text-gray-500" />
151
+ </button>
152
+
153
+ {/* Dropdown Menu */}
154
+ <div
155
+ className={cn(
156
+ 'absolute left-0 right-0 mt-1.5',
157
+ 'py-1',
158
+ 'bg-white dark:bg-[#1a1a1a]',
159
+ 'border border-gray-200 dark:border-white/10',
160
+ 'rounded-lg',
161
+ 'shadow-lg shadow-black/5 dark:shadow-black/30',
162
+ 'z-50',
163
+ 'transition-all duration-150 ease-out origin-top',
164
+ isOpen
165
+ ? 'opacity-100 scale-100'
166
+ : 'opacity-0 scale-95 pointer-events-none'
167
+ )}
168
+ role="listbox"
169
+ aria-label="Select documentation set"
170
+ >
171
+ {docSets.map((docSet) => {
172
+ const isSelected = docSet.slug === currentDocSet
173
+
174
+ return (
175
+ <button
176
+ key={docSet.slug || '_root'}
177
+ onClick={() => handleSelect(docSet)}
178
+ className={cn(
179
+ 'w-full flex items-center gap-2.5 px-3 py-2',
180
+ 'transition-colors duration-100',
181
+ 'focus:outline-none',
182
+ isSelected
183
+ ? 'bg-primary-50 dark:bg-primary-500/10'
184
+ : 'hover:bg-gray-50 dark:hover:bg-white/5'
185
+ )}
186
+ role="option"
187
+ aria-selected={isSelected}
188
+ >
189
+ {/* Icon */}
190
+ <div className={cn(
191
+ 'flex-shrink-0 w-5 h-5 flex items-center justify-center',
192
+ isSelected
193
+ ? 'text-primary-600 dark:text-primary-400'
194
+ : 'text-gray-500 dark:text-gray-400'
195
+ )}>
196
+ {docSet.icon ? (
197
+ <span className="text-base leading-none">{docSet.icon}</span>
198
+ ) : docSet.iconUrl ? (
199
+ <img src={docSet.iconUrl} alt="" className="w-5 h-5" />
200
+ ) : (
201
+ <PackageIcon className="w-5 h-5" />
202
+ )}
203
+ </div>
204
+
205
+ {/* Text Content */}
206
+ <div className="flex-1 text-left min-w-0">
207
+ <div
208
+ className={cn(
209
+ 'text-sm font-medium truncate',
210
+ isSelected
211
+ ? 'text-primary-700 dark:text-primary-300'
212
+ : 'text-gray-900 dark:text-white'
213
+ )}
214
+ >
215
+ {docSet.name}
216
+ </div>
217
+ {docSet.description && (
218
+ <div className={cn(
219
+ 'text-xs truncate',
220
+ isSelected
221
+ ? 'text-primary-600/70 dark:text-primary-400/70'
222
+ : 'text-gray-500 dark:text-gray-400'
223
+ )}>
224
+ {docSet.description}
225
+ </div>
226
+ )}
227
+ </div>
228
+
229
+ {/* Checkmark indicator */}
230
+ {isSelected && (
231
+ <CheckIcon className="flex-shrink-0 w-4 h-4 text-primary-600 dark:text-primary-400" />
232
+ )}
233
+ </button>
234
+ )
235
+ })}
236
+ </div>
237
+ </div>
238
+ )
239
+ }
@@ -54,7 +54,7 @@ export function DocsLayout({
54
54
  footer,
55
55
  }: DocsLayoutProps) {
56
56
  const sharedProps = usePage<{ props: SharedProps }>().props as unknown as SharedProps
57
- const { nav, currentPath } = sharedProps
57
+ const { nav, currentPath, docSets, currentDocSet } = sharedProps
58
58
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
59
59
  const { resolvedTheme } = useTheme()
60
60
 
@@ -132,24 +132,24 @@ export function DocsLayout({
132
132
  {mobileMenuOpen && (
133
133
  <div className="fixed inset-0 z-40 lg:hidden">
134
134
  <div className="fixed inset-0 bg-black/50 dark:bg-black/70" onClick={() => setMobileMenuOpen(false)} />
135
- <div className="fixed inset-y-0 left-0 w-72 overflow-y-auto bg-white dark:bg-[#0f0f0f] px-4 lg:px-10 py-6 pt-20 border-r border-gray-200 dark:border-gray-800 transition-colors">
136
- <Sidebar nav={nav} currentPath={currentPath} />
135
+ <div className="fixed inset-y-0 left-0 w-64 overflow-y-auto bg-white dark:bg-[#0f0f0f] px-4 py-6 pt-20 border-r border-gray-200 dark:border-gray-800 transition-colors">
136
+ <Sidebar nav={nav} currentPath={currentPath} docSets={docSets} currentDocSet={currentDocSet} />
137
137
  </div>
138
138
  </div>
139
139
  )}
140
140
 
141
141
  {/* Main content area */}
142
142
  <div className="bg-white dark:bg-[#0f0f0f] pt-16 w-full flex-1 transition-colors">
143
- <div className="grid grid-cols-12">
144
- {/* Desktop sidebar */}
145
- <aside className="hidden lg:block lg:col-span-3 xl:col-span-2 border-r border-gray-200 dark:border-gray-800 min-h-[calc(100vh-4rem)] transition-colors">
146
- <nav className="sticky top-16 px-4 lg:px-10 py-6 max-h-[calc(100vh-4rem)] overflow-y-auto">
147
- <Sidebar nav={nav} currentPath={currentPath} />
143
+ <div className="flex">
144
+ {/* Desktop sidebar - fixed width */}
145
+ <aside className="hidden lg:block w-72 flex-shrink-0 border-r border-gray-200 dark:border-gray-800 min-h-[calc(100vh-4rem)] transition-colors">
146
+ <nav className="sticky top-16 px-4 py-6 max-h-[calc(100vh-4rem)] overflow-y-auto">
147
+ <Sidebar nav={nav} currentPath={currentPath} docSets={docSets} currentDocSet={currentDocSet} />
148
148
  </nav>
149
149
  </aside>
150
150
 
151
151
  {/* Main content */}
152
- <main className="col-span-12 lg:col-span-9 xl:col-span-10 p-4 lg:px-10 lg:py-6">
152
+ <main className="flex-1 min-w-0 p-4 lg:px-10 lg:py-6">
153
153
  <article className="prose prose-lg max-w-3xl prose-headings:font-bold prose-headings:tracking-tight prose-h1:text-3xl prose-h1:mb-4 prose-h2:text-2xl prose-h2:mt-10 first:prose-h2:mt-0 prose-h3:text-xl prose-a:text-primary-600 dark:prose-a:text-primary-400 prose-a:no-underline hover:prose-a:underline prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none dark:prose-headings:text-white dark:prose-strong:text-white dark:text-gray-300">
154
154
  {children}
155
155
  </article>
@@ -1,13 +1,22 @@
1
1
  import { Link } from '@inertiajs/react'
2
2
  import { cn } from '../lib/utils'
3
+ import { DocSetSelector } from './DocSetSelector'
3
4
  import type { SidebarProps } from '../types'
4
5
 
5
6
  /**
6
7
  * Documentation sidebar with section-based navigation.
8
+ * In multi-docs mode, includes a dropdown selector at the top.
7
9
  */
8
- export function Sidebar({ nav, currentPath, className }: SidebarProps) {
10
+ export function Sidebar({ nav, currentPath, className, docSets, currentDocSet }: SidebarProps) {
9
11
  return (
10
- <nav className={cn('space-y-8', className)}>
12
+ <nav className={cn('space-y-6', className)}>
13
+ {/* Doc Set Selector - only shown in multi-docs mode */}
14
+ {docSets && docSets.length > 1 && (
15
+ <DocSetSelector docSets={docSets} currentDocSet={currentDocSet ?? ''} className="mb-6" />
16
+ )}
17
+
18
+ {/* Navigation Sections */}
19
+ <div className="space-y-8">
11
20
  {nav.map((section) => (
12
21
  <div key={section.title}>
13
22
  <h3 className="mb-3 text-xs font-mono uppercase tracking-widest text-gray-500 dark:text-gray-400">
@@ -32,6 +41,7 @@ export function Sidebar({ nav, currentPath, className }: SidebarProps) {
32
41
  </ul>
33
42
  </div>
34
43
  ))}
44
+ </div>
35
45
  </nav>
36
46
  )
37
47
  }
@@ -1,4 +1,5 @@
1
1
  export { CodeBlock, InlineCode } from './CodeBlock'
2
+ export { DocSetSelector } from './DocSetSelector'
2
3
  export { DocsLayout } from './DocsLayout'
3
4
  export { DocsPage } from './DocsPage'
4
5
  export { EmojiConfetti } from './EmojiConfetti'
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Components
2
2
  export {
3
3
  CodeBlock,
4
+ DocSetSelector,
4
5
  DocsLayout,
5
6
  DocsPage,
6
7
  EmojiConfetti,
@@ -37,6 +38,7 @@ export type {
37
38
  DocContent,
38
39
  DocsAppConfig,
39
40
  DocsLayoutProps,
41
+ DocSetMeta,
40
42
  MarkdownProps,
41
43
  NavItem,
42
44
  NavSection,
package/src/styles.css CHANGED
@@ -13,6 +13,9 @@
13
13
  * Note: The Google Fonts import must come before other imports to avoid CSS ordering warnings.
14
14
  */
15
15
 
16
+ /* Enable class-based dark mode (using .dark class on root element) */
17
+ @custom-variant dark (&:where(.dark, .dark *));
18
+
16
19
  /* Theme customizations for Tailwind v4 */
17
20
  @theme {
18
21
  /* Max width */
package/src/types.ts CHANGED
@@ -16,6 +16,18 @@ export interface NavSection {
16
16
  items: NavItem[]
17
17
  }
18
18
 
19
+ /** Documentation set metadata (for multi-docs mode) */
20
+ export interface DocSetMeta {
21
+ name: string
22
+ slug: string
23
+ description: string
24
+ /** Emoji or short text icon (e.g., "🍓") */
25
+ icon?: string
26
+ /** URL to icon image */
27
+ iconUrl?: string
28
+ prefix: string
29
+ }
30
+
19
31
  /** Shared props passed to all pages via Inertia */
20
32
  export interface SharedProps {
21
33
  nav: NavSection[]
@@ -32,6 +44,10 @@ export interface SharedProps {
32
44
  githubUrl?: string
33
45
  /** Additional navigation links (from Python backend) */
34
46
  navLinks?: Array<{ label: string; href: string }>
47
+ /** Available documentation sets (multi-docs mode) */
48
+ docSets?: DocSetMeta[]
49
+ /** Current documentation set slug (multi-docs mode) */
50
+ currentDocSet?: string
35
51
  }
36
52
 
37
53
  /** Document content structure */
@@ -67,6 +83,10 @@ export interface SidebarProps {
67
83
  nav: NavSection[]
68
84
  currentPath: string
69
85
  className?: string
86
+ /** Available documentation sets (multi-docs mode) */
87
+ docSets?: DocSetMeta[]
88
+ /** Current documentation set slug (multi-docs mode) */
89
+ currentDocSet?: string
70
90
  }
71
91
 
72
92
  /** Props for Markdown component */