@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.
- package/dist/index.d.ts +16 -4
- package/dist/index.js +353 -161
- package/dist/index.js.map +1 -1
- package/dist/ssr.d.ts +1 -1
- package/dist/{types-CR-kx8KP.d.ts → types-DlF8TX2Q.d.ts} +20 -1
- package/package.json +1 -1
- package/src/components/DocSetSelector.tsx +239 -0
- package/src/components/DocsLayout.tsx +9 -9
- package/src/components/Sidebar.tsx +12 -2
- package/src/components/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/styles.css +3 -0
- package/src/types.ts +20 -0
|
@@ -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-
|
|
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="
|
|
144
|
-
{/* Desktop sidebar */}
|
|
145
|
-
<aside className="hidden lg:block
|
|
146
|
-
<nav className="sticky top-16 px-4
|
|
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="
|
|
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-
|
|
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
|
}
|
package/src/components/index.ts
CHANGED
package/src/index.ts
CHANGED
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 */
|