@lexibase/rdx-ui 1.0.0-beta.1 → 1.0.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/README.md +88 -1
- package/package.json +32 -26
- package/postcss-plugin.cjs +2 -0
- package/postcss.config.mjs +1 -5
- package/src/components/accordion.tsx +91 -0
- package/src/components/blockquote.tsx +33 -0
- package/src/components/button-link.tsx +27 -0
- package/src/components/button.tsx +9 -12
- package/src/components/callout.tsx +96 -0
- package/src/components/code-block.tsx +55 -0
- package/src/components/command.tsx +90 -0
- package/src/components/copy-code-button.tsx +78 -0
- package/src/components/dialog.tsx +76 -0
- package/src/components/docs-search-dialog.tsx +184 -0
- package/src/components/docs-search.tsx +80 -0
- package/src/components/docs-sidebar.tsx +13 -20
- package/src/components/docs-toc.tsx +231 -0
- package/src/components/dropdown-menu.tsx +88 -39
- package/src/components/home.tsx +5 -6
- package/src/components/mdx-components.tsx +118 -27
- package/src/components/navbar.tsx +22 -28
- package/src/components/popover.tsx +68 -0
- package/src/components/separator.tsx +11 -9
- package/src/components/tabs.tsx +183 -0
- package/src/components/version-dropdown.tsx +38 -32
- package/src/components/version-notice.tsx +39 -0
- package/src/lib/docs-search.ts +68 -0
- package/src/lib/utils.ts +10 -0
- package/src/styles/globals.css +34 -0
- package/src/styles/globals.d.css.ts +2 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { Check, Copy } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import { Button } from './button'
|
|
7
|
+
|
|
8
|
+
export function CopyCodeButton() {
|
|
9
|
+
const [status, setStatus] = useState<'idle' | 'copied' | 'error'>('idle')
|
|
10
|
+
const pending = useRef(false)
|
|
11
|
+
const mounted = useRef(false)
|
|
12
|
+
const reset = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
mounted.current = true
|
|
15
|
+
return () => {
|
|
16
|
+
mounted.current = false
|
|
17
|
+
clearTimeout(reset.current)
|
|
18
|
+
}
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
async function copy(button: HTMLButtonElement) {
|
|
22
|
+
if (pending.current) return
|
|
23
|
+
pending.current = true
|
|
24
|
+
clearTimeout(reset.current)
|
|
25
|
+
try {
|
|
26
|
+
const code = button.closest('[data-rdx-code-block]')?.querySelector('pre')
|
|
27
|
+
if (!code || !navigator.clipboard?.writeText)
|
|
28
|
+
throw new Error('Clipboard unavailable')
|
|
29
|
+
await navigator.clipboard.writeText(code.textContent ?? '')
|
|
30
|
+
if (mounted.current) {
|
|
31
|
+
setStatus('copied')
|
|
32
|
+
reset.current = setTimeout(() => setStatus('idle'), 2000)
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
if (mounted.current) setStatus('error')
|
|
36
|
+
} finally {
|
|
37
|
+
pending.current = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Icon = status === 'copied' ? Check : Copy
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex min-w-0 items-center justify-end gap-2">
|
|
44
|
+
<span
|
|
45
|
+
role="status"
|
|
46
|
+
aria-live="polite"
|
|
47
|
+
aria-atomic="true"
|
|
48
|
+
className={
|
|
49
|
+
status === 'error' ? 'text-xs text-muted-foreground' : 'sr-only'
|
|
50
|
+
}
|
|
51
|
+
>
|
|
52
|
+
{status === 'copied'
|
|
53
|
+
? 'Code copied to clipboard.'
|
|
54
|
+
: status === 'error'
|
|
55
|
+
? 'Could not copy. Select the code and copy it manually.'
|
|
56
|
+
: ''}
|
|
57
|
+
</span>
|
|
58
|
+
<Button
|
|
59
|
+
type="button"
|
|
60
|
+
variant="outline"
|
|
61
|
+
size="icon"
|
|
62
|
+
data-status={status}
|
|
63
|
+
className="size-7 shrink-0 border-border/70 bg-background/60 p-0 text-muted-foreground shadow-none hover:text-foreground data-[status=copied]:border-emerald-500/30 data-[status=copied]:bg-emerald-500/10 data-[status=copied]:text-emerald-700 dark:data-[status=copied]:text-emerald-400 data-[status=error]:border-amber-500/30 [&>svg]:size-3.5"
|
|
64
|
+
aria-label="Copy code"
|
|
65
|
+
title={
|
|
66
|
+
status === 'copied'
|
|
67
|
+
? 'Copied!'
|
|
68
|
+
: status === 'error'
|
|
69
|
+
? 'Could not copy. Try again or copy manually.'
|
|
70
|
+
: 'Copy code'
|
|
71
|
+
}
|
|
72
|
+
onClick={(event) => copy(event.currentTarget)}
|
|
73
|
+
>
|
|
74
|
+
<Icon aria-hidden="true" className="size-4" />
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// shadcn/ui Base Nova Dialog, using the shared RDX theme and buttons.
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
|
|
6
|
+
import { X } from 'lucide-react'
|
|
7
|
+
import { mergeStateClassName } from '../lib/utils'
|
|
8
|
+
import { Button } from './button'
|
|
9
|
+
|
|
10
|
+
function Dialog(props: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
11
|
+
return <DialogPrimitive.Root {...props} />
|
|
12
|
+
}
|
|
13
|
+
function DialogTrigger(
|
|
14
|
+
props: React.ComponentProps<typeof DialogPrimitive.Trigger>
|
|
15
|
+
) {
|
|
16
|
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
17
|
+
}
|
|
18
|
+
function DialogContent({
|
|
19
|
+
className,
|
|
20
|
+
children,
|
|
21
|
+
...props
|
|
22
|
+
}: React.ComponentProps<typeof DialogPrimitive.Popup>) {
|
|
23
|
+
return (
|
|
24
|
+
<DialogPrimitive.Portal>
|
|
25
|
+
<DialogPrimitive.Backdrop
|
|
26
|
+
data-slot="dialog-overlay"
|
|
27
|
+
className="fixed inset-0 isolate z-50 bg-black/30 backdrop-blur-xs duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 motion-reduce:data-open:animate-none motion-reduce:data-closed:animate-none"
|
|
28
|
+
/>
|
|
29
|
+
<DialogPrimitive.Popup
|
|
30
|
+
data-slot="dialog-content"
|
|
31
|
+
className={mergeStateClassName(
|
|
32
|
+
'fixed top-[20%] left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 gap-4 rounded-xl bg-popover p-1 text-sm text-popover-foreground ring-1 ring-foreground/10 shadow-lg outline-none duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 motion-reduce:data-open:animate-none motion-reduce:data-closed:animate-none',
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
<DialogPrimitive.Close
|
|
39
|
+
render={
|
|
40
|
+
<Button
|
|
41
|
+
variant="ghost"
|
|
42
|
+
size="icon"
|
|
43
|
+
className="absolute top-2 right-2 size-7"
|
|
44
|
+
/>
|
|
45
|
+
}
|
|
46
|
+
aria-label="Close search"
|
|
47
|
+
>
|
|
48
|
+
<X className="size-3.5" />
|
|
49
|
+
</DialogPrimitive.Close>
|
|
50
|
+
</DialogPrimitive.Popup>
|
|
51
|
+
</DialogPrimitive.Portal>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
function DialogTitle({
|
|
55
|
+
className,
|
|
56
|
+
...props
|
|
57
|
+
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
58
|
+
return (
|
|
59
|
+
<DialogPrimitive.Title
|
|
60
|
+
className={mergeStateClassName('font-medium', className)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
function DialogDescription({
|
|
66
|
+
className,
|
|
67
|
+
...props
|
|
68
|
+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
69
|
+
return (
|
|
70
|
+
<DialogPrimitive.Description
|
|
71
|
+
className={mergeStateClassName('text-muted-foreground', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
export { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription }
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useDeferredValue,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
type RefObject,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import { useRouter, usePathname } from 'next/navigation'
|
|
11
|
+
import { FileText, Hash, LoaderCircle } from 'lucide-react'
|
|
12
|
+
import type { RdxSearchIndex } from '@lexibase/rdx-types'
|
|
13
|
+
import {
|
|
14
|
+
prepareSearch,
|
|
15
|
+
searchDocumentation,
|
|
16
|
+
type PreparedSearch,
|
|
17
|
+
} from '../lib/docs-search'
|
|
18
|
+
import { Dialog, DialogContent, DialogDescription, DialogTitle } from './dialog'
|
|
19
|
+
import {
|
|
20
|
+
Command,
|
|
21
|
+
CommandEmpty,
|
|
22
|
+
CommandInput,
|
|
23
|
+
CommandItem,
|
|
24
|
+
CommandList,
|
|
25
|
+
} from './command'
|
|
26
|
+
import { Button } from './button'
|
|
27
|
+
|
|
28
|
+
const cache = new Map<string, PreparedSearch>()
|
|
29
|
+
|
|
30
|
+
export function DocsSearchDialog({
|
|
31
|
+
indexUrl,
|
|
32
|
+
version,
|
|
33
|
+
open,
|
|
34
|
+
onOpenChange,
|
|
35
|
+
triggerRef,
|
|
36
|
+
}: {
|
|
37
|
+
indexUrl: string
|
|
38
|
+
version: string
|
|
39
|
+
open: boolean
|
|
40
|
+
onOpenChange: (value: boolean) => void
|
|
41
|
+
triggerRef: RefObject<HTMLButtonElement | null>
|
|
42
|
+
}) {
|
|
43
|
+
const router = useRouter()
|
|
44
|
+
const pathname = usePathname()
|
|
45
|
+
const [query, setQuery] = useState('')
|
|
46
|
+
const deferred = useDeferredValue(query)
|
|
47
|
+
const [state, setState] = useState<{
|
|
48
|
+
entries?: PreparedSearch
|
|
49
|
+
error?: boolean
|
|
50
|
+
}>({})
|
|
51
|
+
const [retry, setRetry] = useState(0)
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!open) return
|
|
54
|
+
const controller = new AbortController()
|
|
55
|
+
async function load() {
|
|
56
|
+
try {
|
|
57
|
+
const cached =
|
|
58
|
+
process.env.NODE_ENV === 'production'
|
|
59
|
+
? cache.get(indexUrl)
|
|
60
|
+
: undefined
|
|
61
|
+
let entries = cached
|
|
62
|
+
if (!entries) {
|
|
63
|
+
const response = await fetch(indexUrl, {
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
cache: 'no-store',
|
|
66
|
+
})
|
|
67
|
+
if (!response.ok) throw new Error('Search unavailable')
|
|
68
|
+
entries = prepareSearch((await response.json()) as RdxSearchIndex)
|
|
69
|
+
if (process.env.NODE_ENV === 'production') {
|
|
70
|
+
if (cache.size >= 3) cache.delete(cache.keys().next().value!)
|
|
71
|
+
cache.set(indexUrl, entries)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!controller.signal.aborted) setState({ entries })
|
|
75
|
+
} catch {
|
|
76
|
+
if (!controller.signal.aborted) setState({ error: true })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
void load()
|
|
80
|
+
return () => controller.abort()
|
|
81
|
+
}, [open, indexUrl, retry])
|
|
82
|
+
const results = useMemo(
|
|
83
|
+
() => searchDocumentation(state.entries ?? [], deferred),
|
|
84
|
+
[state.entries, deferred]
|
|
85
|
+
)
|
|
86
|
+
const loading = !state.entries && !state.error
|
|
87
|
+
function select(href: string) {
|
|
88
|
+
onOpenChange(false)
|
|
89
|
+
setQuery('')
|
|
90
|
+
// Native fragment navigation also scrolls when already on the target page.
|
|
91
|
+
const url = new URL(href, window.location.origin)
|
|
92
|
+
if (url.pathname === pathname && url.hash) window.location.assign(url.hash)
|
|
93
|
+
else router.push(href)
|
|
94
|
+
}
|
|
95
|
+
return (
|
|
96
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
97
|
+
<DialogContent finalFocus={triggerRef}>
|
|
98
|
+
<DialogTitle className="sr-only">Search documentation</DialogTitle>
|
|
99
|
+
<DialogDescription className="sr-only">
|
|
100
|
+
Search pages and sections in {version}. Use arrow keys to choose a
|
|
101
|
+
result and Enter to open it.
|
|
102
|
+
</DialogDescription>
|
|
103
|
+
<Command shouldFilter={false} label="Search documentation">
|
|
104
|
+
<CommandInput
|
|
105
|
+
aria-label="Search documentation"
|
|
106
|
+
placeholder="Search documentation…"
|
|
107
|
+
value={query}
|
|
108
|
+
onValueChange={setQuery}
|
|
109
|
+
maxLength={120}
|
|
110
|
+
/>
|
|
111
|
+
<div className="px-2 pt-2 pb-1 text-[11px] text-muted-foreground">
|
|
112
|
+
{version}
|
|
113
|
+
</div>
|
|
114
|
+
<CommandList aria-busy={loading}>
|
|
115
|
+
{loading ? (
|
|
116
|
+
<div
|
|
117
|
+
role="status"
|
|
118
|
+
className="flex justify-center gap-2 p-6 text-sm text-muted-foreground"
|
|
119
|
+
>
|
|
120
|
+
<LoaderCircle className="size-4 animate-spin motion-reduce:animate-none" />
|
|
121
|
+
Loading search…
|
|
122
|
+
</div>
|
|
123
|
+
) : state.error ? (
|
|
124
|
+
<div
|
|
125
|
+
role="status"
|
|
126
|
+
className="p-4 text-center text-sm text-muted-foreground"
|
|
127
|
+
>
|
|
128
|
+
Search is unavailable.{' '}
|
|
129
|
+
<Button
|
|
130
|
+
variant="link"
|
|
131
|
+
size="sm"
|
|
132
|
+
onClick={() => {
|
|
133
|
+
setState({})
|
|
134
|
+
setRetry((value) => value + 1)
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
Try again
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
) : !query.trim() ? (
|
|
141
|
+
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
|
|
142
|
+
Type to search this documentation.
|
|
143
|
+
</div>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
147
|
+
{results.map((entry) => (
|
|
148
|
+
<CommandItem
|
|
149
|
+
key={entry.href}
|
|
150
|
+
value={entry.href}
|
|
151
|
+
onSelect={() => select(entry.href)}
|
|
152
|
+
>
|
|
153
|
+
{entry.section ? (
|
|
154
|
+
<Hash className="size-4 shrink-0 self-start mt-1 text-muted-foreground" />
|
|
155
|
+
) : (
|
|
156
|
+
<FileText className="size-4 shrink-0 self-start mt-1 text-muted-foreground" />
|
|
157
|
+
)}
|
|
158
|
+
<div className="min-w-0">
|
|
159
|
+
<div className="text-[11px] text-muted-foreground">
|
|
160
|
+
{entry.category}
|
|
161
|
+
{entry.section && ` · ${entry.title}`}
|
|
162
|
+
</div>
|
|
163
|
+
<div className="font-medium leading-5">
|
|
164
|
+
{entry.section || entry.title}
|
|
165
|
+
</div>
|
|
166
|
+
{entry.excerpt && (
|
|
167
|
+
<p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
168
|
+
{entry.excerpt}
|
|
169
|
+
</p>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</CommandItem>
|
|
173
|
+
))}
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
</CommandList>
|
|
177
|
+
<div className="border-t px-2 py-2 text-[10px] text-muted-foreground">
|
|
178
|
+
↑ ↓ Navigate <span className="mx-2">↵ Open</span> Esc Close
|
|
179
|
+
</div>
|
|
180
|
+
</Command>
|
|
181
|
+
</DialogContent>
|
|
182
|
+
</Dialog>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import dynamic from 'next/dynamic'
|
|
5
|
+
import { Search } from 'lucide-react'
|
|
6
|
+
import { Button } from './button'
|
|
7
|
+
|
|
8
|
+
const SearchDialog = dynamic(
|
|
9
|
+
() =>
|
|
10
|
+
import('./docs-search-dialog').then((module) => module.DocsSearchDialog),
|
|
11
|
+
{ ssr: false }
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
/** Only the small trigger/shortcut is loaded until the first search opens. */
|
|
15
|
+
export function DocsSearch({
|
|
16
|
+
indexUrl,
|
|
17
|
+
version,
|
|
18
|
+
}: {
|
|
19
|
+
indexUrl: string
|
|
20
|
+
version: string
|
|
21
|
+
}) {
|
|
22
|
+
const [open, setOpen] = useState(false)
|
|
23
|
+
const [activated, setActivated] = useState(false)
|
|
24
|
+
const trigger = useRef<HTMLButtonElement | null>(null)
|
|
25
|
+
function show() {
|
|
26
|
+
setActivated(true)
|
|
27
|
+
setOpen(true)
|
|
28
|
+
}
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
function shortcut(event: KeyboardEvent) {
|
|
31
|
+
if (
|
|
32
|
+
event.key.toLowerCase() !== 'k' ||
|
|
33
|
+
(!event.ctrlKey && !event.metaKey) ||
|
|
34
|
+
event.altKey ||
|
|
35
|
+
event.isComposing ||
|
|
36
|
+
event.repeat
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
setActivated(true)
|
|
41
|
+
setOpen((value) => !value)
|
|
42
|
+
}
|
|
43
|
+
document.addEventListener('keydown', shortcut)
|
|
44
|
+
return () => document.removeEventListener('keydown', shortcut)
|
|
45
|
+
}, [])
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<Button
|
|
49
|
+
ref={trigger}
|
|
50
|
+
data-rdx-search-trigger
|
|
51
|
+
variant="outline"
|
|
52
|
+
size="sm"
|
|
53
|
+
aria-label="Search documentation"
|
|
54
|
+
aria-haspopup="dialog"
|
|
55
|
+
aria-expanded={open}
|
|
56
|
+
onClick={show}
|
|
57
|
+
className="h-8 gap-2 px-2 text-muted-foreground sm:min-w-40 sm:justify-start"
|
|
58
|
+
>
|
|
59
|
+
<Search className="size-3.5" />
|
|
60
|
+
<span className="hidden sm:inline">Search…</span>
|
|
61
|
+
<kbd
|
|
62
|
+
className="ml-auto hidden rounded border px-1 text-[10px] text-muted-foreground sm:inline"
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
>
|
|
65
|
+
⌘ / Ctrl K
|
|
66
|
+
</kbd>
|
|
67
|
+
</Button>
|
|
68
|
+
{activated && (
|
|
69
|
+
<SearchDialog
|
|
70
|
+
key={indexUrl}
|
|
71
|
+
indexUrl={indexUrl}
|
|
72
|
+
version={version}
|
|
73
|
+
open={open}
|
|
74
|
+
onOpenChange={setOpen}
|
|
75
|
+
triggerRef={trigger}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react'
|
|
4
4
|
import { SidebarClose, SidebarOpen } from 'lucide-react'
|
|
5
|
-
import Link from 'next/link'
|
|
6
5
|
|
|
7
6
|
import { Button } from './button'
|
|
7
|
+
import { ButtonLink } from './button-link'
|
|
8
8
|
import { Separator } from './separator'
|
|
9
9
|
|
|
10
10
|
export type SidebarCategory = {
|
|
@@ -19,15 +19,13 @@ export function DocsSidebar({
|
|
|
19
19
|
categories: SidebarCategory[]
|
|
20
20
|
pathname: string
|
|
21
21
|
}) {
|
|
22
|
-
const [
|
|
22
|
+
const [openPath, setOpenPath] = useState<string | null>(null)
|
|
23
|
+
const isOpen = openPath === pathname
|
|
23
24
|
const dialog = useRef<HTMLDialogElement>(null)
|
|
24
25
|
useEffect(() => {
|
|
25
26
|
if (isOpen && !dialog.current?.open) dialog.current?.showModal()
|
|
26
27
|
if (!isOpen && dialog.current?.open) dialog.current?.close()
|
|
27
28
|
}, [isOpen])
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
setIsOpen(false)
|
|
30
|
-
}, [pathname])
|
|
31
29
|
const content = (
|
|
32
30
|
<nav aria-label="Documentation pages">
|
|
33
31
|
{categories.map((category, index) => (
|
|
@@ -39,24 +37,19 @@ export function DocsSidebar({
|
|
|
39
37
|
{category.title}
|
|
40
38
|
</p>
|
|
41
39
|
{category.links.map((link) => (
|
|
42
|
-
<
|
|
40
|
+
<ButtonLink
|
|
43
41
|
key={link.href}
|
|
44
42
|
variant="ghost"
|
|
45
43
|
size="sm"
|
|
46
|
-
className="justify-start"
|
|
47
44
|
active={pathname === link.href}
|
|
48
|
-
asChild
|
|
49
45
|
aria-label={link.label}
|
|
46
|
+
href={link.href}
|
|
47
|
+
onClick={() => setOpenPath(null)}
|
|
48
|
+
aria-current={pathname === link.href ? 'page' : undefined}
|
|
49
|
+
className="justify-start text-sm text-primary"
|
|
50
50
|
>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
href={link.href}
|
|
54
|
-
onClick={() => setIsOpen(false)}
|
|
55
|
-
aria-current={pathname === link.href ? 'page' : undefined}
|
|
56
|
-
>
|
|
57
|
-
{link.label}
|
|
58
|
-
</Link>
|
|
59
|
-
</Button>
|
|
51
|
+
{link.label}
|
|
52
|
+
</ButtonLink>
|
|
60
53
|
))}
|
|
61
54
|
{index < categories.length - 1 && <Separator className="my-4" />}
|
|
62
55
|
</div>
|
|
@@ -68,7 +61,7 @@ export function DocsSidebar({
|
|
|
68
61
|
<div className="md:hidden absolute top-2 left-4 z-30">
|
|
69
62
|
<Button
|
|
70
63
|
variant="ghost"
|
|
71
|
-
onClick={() =>
|
|
64
|
+
onClick={() => setOpenPath(pathname)}
|
|
72
65
|
aria-label="Open sidebar"
|
|
73
66
|
>
|
|
74
67
|
<SidebarOpen className="size-4" />
|
|
@@ -83,13 +76,13 @@ export function DocsSidebar({
|
|
|
83
76
|
<dialog
|
|
84
77
|
ref={dialog}
|
|
85
78
|
aria-label="Documentation navigation"
|
|
86
|
-
onClose={() =>
|
|
79
|
+
onClose={() => setOpenPath(null)}
|
|
87
80
|
className="fixed inset-0 m-0 h-dvh max-h-none w-full max-w-none bg-background text-foreground border-0 p-4 backdrop:bg-black/40"
|
|
88
81
|
>
|
|
89
82
|
<div className="flex justify-end mb-4">
|
|
90
83
|
<Button
|
|
91
84
|
variant="ghost"
|
|
92
|
-
onClick={() =>
|
|
85
|
+
onClick={() => setOpenPath(null)}
|
|
93
86
|
aria-label="Close sidebar"
|
|
94
87
|
>
|
|
95
88
|
<SidebarClose className="size-4" />
|