@lexibase/rdx-ui 1.0.0-beta.1 → 1.0.0-beta.2
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 +91 -1
- package/package.json +32 -24
- 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 +21 -27
- 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 +34 -30
- 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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import { Github } from 'lucide-react'
|
|
3
|
-
import Link from 'next/link'
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { ButtonLink } from './button-link'
|
|
6
5
|
import { Logo } from './logo'
|
|
7
6
|
import { Separator } from './separator'
|
|
8
7
|
import { ModeToggle } from './mode-toggle'
|
|
@@ -16,6 +15,7 @@ export function Navbar({
|
|
|
16
15
|
docsHref = '/docs',
|
|
17
16
|
docsLink,
|
|
18
17
|
versionMenu,
|
|
18
|
+
search,
|
|
19
19
|
github,
|
|
20
20
|
showToggleMode = false,
|
|
21
21
|
}: {
|
|
@@ -26,6 +26,7 @@ export function Navbar({
|
|
|
26
26
|
docsHref?: string
|
|
27
27
|
docsLink?: ReactNode
|
|
28
28
|
versionMenu?: ReactNode
|
|
29
|
+
search?: ReactNode
|
|
29
30
|
github?: { visible: boolean; url: string }
|
|
30
31
|
showToggleMode?: boolean
|
|
31
32
|
}) {
|
|
@@ -36,48 +37,41 @@ export function Navbar({
|
|
|
36
37
|
className="w-full flex flex-wrap shrink-0 items-center justify-between px-6 py-2 border-b border-border gap-y-2"
|
|
37
38
|
>
|
|
38
39
|
<div className="flex items-center gap-1">
|
|
39
|
-
<
|
|
40
|
+
<ButtonLink
|
|
40
41
|
variant="ghost"
|
|
41
42
|
size={title === '' ? 'icon' : 'default'}
|
|
42
|
-
asChild
|
|
43
43
|
aria-label={siteTitle}
|
|
44
|
+
href={homeHref}
|
|
45
|
+
className="cursor-default flex items-center gap-2"
|
|
44
46
|
>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
>
|
|
49
|
-
{logo}
|
|
50
|
-
{title !== '' && <span>{title}</span>}
|
|
51
|
-
</Link>
|
|
52
|
-
</Button>
|
|
47
|
+
{logo}
|
|
48
|
+
{title !== '' && <span>{title}</span>}
|
|
49
|
+
</ButtonLink>
|
|
53
50
|
</div>
|
|
54
51
|
<div className="flex items-center md:gap-x-2 h-4">
|
|
52
|
+
{search}
|
|
55
53
|
{versionMenu}
|
|
56
54
|
{docsLink ?? (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
</
|
|
55
|
+
<ButtonLink variant="link" size="sm" href={docsHref}>
|
|
56
|
+
Docs
|
|
57
|
+
</ButtonLink>
|
|
60
58
|
)}
|
|
61
59
|
{github?.visible && github.url && (
|
|
62
60
|
<>
|
|
63
61
|
<Separator orientation="vertical" />
|
|
64
|
-
<
|
|
62
|
+
<ButtonLink
|
|
65
63
|
variant="ghost"
|
|
66
64
|
size="icon"
|
|
67
65
|
title="GitHub"
|
|
68
|
-
asChild
|
|
69
66
|
aria-label="GitHub"
|
|
67
|
+
href={github.url}
|
|
68
|
+
target={github.url === '#' ? undefined : '_blank'}
|
|
69
|
+
rel={github.url === '#' ? undefined : 'noopener noreferrer'}
|
|
70
|
+
className="cursor-default"
|
|
70
71
|
>
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
target={github.url === '#' ? undefined : '_blank'}
|
|
75
|
-
rel={github.url === '#' ? undefined : 'noopener noreferrer'}
|
|
76
|
-
>
|
|
77
|
-
<Github className="h-[1.2rem] w-[1.2rem]" />
|
|
78
|
-
<span className="sr-only">GitHub</span>
|
|
79
|
-
</Link>
|
|
80
|
-
</Button>
|
|
72
|
+
<Github className="h-[1.2rem] w-[1.2rem]" />
|
|
73
|
+
<span className="sr-only">GitHub</span>
|
|
74
|
+
</ButtonLink>
|
|
81
75
|
</>
|
|
82
76
|
)}
|
|
83
77
|
{showToggleMode && (
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Adapted from shadcn/ui's Base Nova Popover.
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import { Popover as PopoverPrimitive } from '@base-ui/react/popover'
|
|
6
|
+
|
|
7
|
+
import { cn, mergeStateClassName } from '@lexibase/rdx-ui/lib/utils'
|
|
8
|
+
|
|
9
|
+
export type PopoverActions = PopoverPrimitive.Root.Actions
|
|
10
|
+
|
|
11
|
+
function Popover(props: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
12
|
+
return <PopoverPrimitive.Root {...props} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function PopoverTrigger(
|
|
16
|
+
props: React.ComponentProps<typeof PopoverPrimitive.Trigger>
|
|
17
|
+
) {
|
|
18
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function PopoverContent({
|
|
22
|
+
className,
|
|
23
|
+
align = 'center',
|
|
24
|
+
alignOffset = 0,
|
|
25
|
+
side = 'bottom',
|
|
26
|
+
sideOffset = 4,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Popup> &
|
|
29
|
+
Pick<
|
|
30
|
+
React.ComponentProps<typeof PopoverPrimitive.Positioner>,
|
|
31
|
+
'align' | 'alignOffset' | 'side' | 'sideOffset'
|
|
32
|
+
>) {
|
|
33
|
+
return (
|
|
34
|
+
<PopoverPrimitive.Portal>
|
|
35
|
+
<PopoverPrimitive.Positioner
|
|
36
|
+
align={align}
|
|
37
|
+
alignOffset={alignOffset}
|
|
38
|
+
side={side}
|
|
39
|
+
sideOffset={sideOffset}
|
|
40
|
+
className="isolate z-50"
|
|
41
|
+
>
|
|
42
|
+
<PopoverPrimitive.Popup
|
|
43
|
+
data-slot="popover-content"
|
|
44
|
+
className={mergeStateClassName(
|
|
45
|
+
'z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden 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 data-closed:zoom-out-95 motion-reduce:data-open:animate-none motion-reduce:data-closed:animate-none',
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
</PopoverPrimitive.Positioner>
|
|
51
|
+
</PopoverPrimitive.Portal>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function PopoverTitle({
|
|
56
|
+
className,
|
|
57
|
+
...props
|
|
58
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Title>) {
|
|
59
|
+
return (
|
|
60
|
+
<PopoverPrimitive.Title
|
|
61
|
+
data-slot="popover-title"
|
|
62
|
+
className={cn('font-medium', className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { Popover, PopoverTrigger, PopoverContent, PopoverTitle }
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
|
3
|
+
import { Separator as SeparatorPrimitive } from '@base-ui/react/separator'
|
|
5
4
|
|
|
6
5
|
import { cn } from '@lexibase/rdx-ui/lib/utils'
|
|
7
6
|
|
|
@@ -10,16 +9,19 @@ function Separator({
|
|
|
10
9
|
orientation = 'horizontal',
|
|
11
10
|
decorative = true,
|
|
12
11
|
...props
|
|
13
|
-
}:
|
|
12
|
+
}: SeparatorPrimitive.Props & { decorative?: boolean }) {
|
|
14
13
|
return (
|
|
15
|
-
<SeparatorPrimitive
|
|
14
|
+
<SeparatorPrimitive
|
|
16
15
|
data-slot="separator"
|
|
17
|
-
|
|
16
|
+
role={decorative ? 'none' : 'separator'}
|
|
17
|
+
aria-hidden={decorative || undefined}
|
|
18
18
|
orientation={orientation}
|
|
19
|
-
className={
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
className={(state) =>
|
|
20
|
+
cn(
|
|
21
|
+
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
|
22
|
+
typeof className === 'function' ? className(state) : className
|
|
23
|
+
)
|
|
24
|
+
}
|
|
23
25
|
{...props}
|
|
24
26
|
/>
|
|
25
27
|
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'
|
|
4
|
+
import { Check, Copy } from 'lucide-react'
|
|
5
|
+
import { useEffect, useRef, useState } from 'react'
|
|
6
|
+
|
|
7
|
+
import { mergeStateClassName } from '../lib/utils'
|
|
8
|
+
import { Button } from './button'
|
|
9
|
+
|
|
10
|
+
// Keep document structure and code whitespace, excluding interactive UI chrome.
|
|
11
|
+
function panelText(node: Node): string {
|
|
12
|
+
if (node.nodeType === 3) {
|
|
13
|
+
const text = node.textContent ?? ''
|
|
14
|
+
return text.includes('\n') && !text.trim() ? '' : text
|
|
15
|
+
}
|
|
16
|
+
if (!(node instanceof HTMLElement)) return ''
|
|
17
|
+
if (
|
|
18
|
+
node.hidden ||
|
|
19
|
+
node.inert ||
|
|
20
|
+
node.getAttribute('aria-hidden') === 'true' ||
|
|
21
|
+
node.matches(
|
|
22
|
+
'button, [role="status"], [data-slot="tabs-header"], script, style'
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
return ''
|
|
26
|
+
if (node.matches('[data-rdx-code-block]')) {
|
|
27
|
+
return `${node.querySelector('pre')?.textContent ?? ''}\n`
|
|
28
|
+
}
|
|
29
|
+
if (node.tagName === 'PRE') return `${node.textContent ?? ''}\n`
|
|
30
|
+
if (node.tagName === 'BR') return '\n'
|
|
31
|
+
const text = Array.from(node.childNodes, panelText).join('')
|
|
32
|
+
if (!text) return ''
|
|
33
|
+
if (
|
|
34
|
+
node.matches('p, h1, h2, h3, h4, h5, h6, blockquote, ul, ol, div, aside')
|
|
35
|
+
) {
|
|
36
|
+
return text.endsWith('\n\n')
|
|
37
|
+
? text
|
|
38
|
+
: text + (text.endsWith('\n') ? '\n' : '\n\n')
|
|
39
|
+
}
|
|
40
|
+
if (node.matches('li, tr')) return `${text}\n`
|
|
41
|
+
if (node.matches('th, td')) return `${text}\t`
|
|
42
|
+
return text
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function CopyTabButton() {
|
|
46
|
+
const [status, setStatus] = useState<'idle' | 'copied' | 'error'>('idle')
|
|
47
|
+
const pending = useRef(false)
|
|
48
|
+
const mounted = useRef(false)
|
|
49
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
mounted.current = true
|
|
52
|
+
return () => {
|
|
53
|
+
mounted.current = false
|
|
54
|
+
clearTimeout(timer.current)
|
|
55
|
+
}
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
async function copy(button: HTMLButtonElement) {
|
|
59
|
+
if (pending.current) return
|
|
60
|
+
pending.current = true
|
|
61
|
+
clearTimeout(timer.current)
|
|
62
|
+
try {
|
|
63
|
+
const root = button.closest('[data-slot="tabs"]')
|
|
64
|
+
const panel = Array.from(
|
|
65
|
+
root?.querySelectorAll<HTMLElement>('[role="tabpanel"]') ?? []
|
|
66
|
+
).find(
|
|
67
|
+
(element) =>
|
|
68
|
+
element.closest('[data-slot="tabs"]') === root &&
|
|
69
|
+
!element.hidden &&
|
|
70
|
+
!element.inert
|
|
71
|
+
)
|
|
72
|
+
if (!panel || !navigator.clipboard?.writeText)
|
|
73
|
+
throw new Error('Clipboard unavailable')
|
|
74
|
+
await navigator.clipboard.writeText(
|
|
75
|
+
panelText(panel).replace(/^\n+|\n+$/g, '')
|
|
76
|
+
)
|
|
77
|
+
if (mounted.current) {
|
|
78
|
+
setStatus('copied')
|
|
79
|
+
timer.current = setTimeout(() => setStatus('idle'), 2000)
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
if (mounted.current) setStatus('error')
|
|
83
|
+
} finally {
|
|
84
|
+
pending.current = false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const Icon = status === 'copied' ? Check : Copy
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<Button
|
|
92
|
+
type="button"
|
|
93
|
+
variant="ghost"
|
|
94
|
+
size="icon"
|
|
95
|
+
aria-label="Copy active tab"
|
|
96
|
+
title={
|
|
97
|
+
status === 'error'
|
|
98
|
+
? 'Could not copy. Select the tab content and copy it manually.'
|
|
99
|
+
: status === 'copied'
|
|
100
|
+
? 'Copied!'
|
|
101
|
+
: 'Copy active tab'
|
|
102
|
+
}
|
|
103
|
+
data-status={status}
|
|
104
|
+
className="mr-1.5 size-7 shrink-0 text-muted-foreground hover:text-foreground data-[status=copied]:text-emerald-700 dark:data-[status=copied]:text-emerald-400 data-[status=error]:text-amber-600 [&>svg]:size-3.5"
|
|
105
|
+
onClick={(event) => copy(event.currentTarget)}
|
|
106
|
+
>
|
|
107
|
+
<Icon aria-hidden="true" />
|
|
108
|
+
</Button>
|
|
109
|
+
<span
|
|
110
|
+
role="status"
|
|
111
|
+
aria-live="polite"
|
|
112
|
+
aria-atomic="true"
|
|
113
|
+
className="sr-only"
|
|
114
|
+
>
|
|
115
|
+
{status === 'copied'
|
|
116
|
+
? 'Tab content copied to clipboard.'
|
|
117
|
+
: status === 'error'
|
|
118
|
+
? 'Could not copy. Select the tab content and copy it manually.'
|
|
119
|
+
: ''}
|
|
120
|
+
</span>
|
|
121
|
+
</>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
|
|
126
|
+
return (
|
|
127
|
+
<TabsPrimitive.Root
|
|
128
|
+
data-slot="tabs"
|
|
129
|
+
className={mergeStateClassName(
|
|
130
|
+
'my-6 min-w-0 max-w-full rounded-lg border border-border bg-muted/20 text-foreground',
|
|
131
|
+
className
|
|
132
|
+
)}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function TabsList({ className, ...props }: TabsPrimitive.List.Props) {
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
data-slot="tabs-header"
|
|
142
|
+
className="flex min-w-0 items-center rounded-t-lg border-b border-border bg-muted/30"
|
|
143
|
+
>
|
|
144
|
+
<TabsPrimitive.List
|
|
145
|
+
data-slot="tabs-list"
|
|
146
|
+
className={mergeStateClassName(
|
|
147
|
+
'flex min-w-0 flex-1 gap-0.5 overflow-x-auto p-1.5 data-[orientation=vertical]:flex-col',
|
|
148
|
+
className
|
|
149
|
+
)}
|
|
150
|
+
{...props}
|
|
151
|
+
/>
|
|
152
|
+
<CopyTabButton />
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
158
|
+
return (
|
|
159
|
+
<TabsPrimitive.Tab
|
|
160
|
+
data-slot="tabs-trigger"
|
|
161
|
+
className={mergeStateClassName(
|
|
162
|
+
'relative shrink-0 rounded-md border border-transparent px-2.5 py-1 text-sm font-medium whitespace-nowrap text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-active:border-border data-active:bg-background data-active:text-foreground data-active:shadow-xs data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:bg-transparent data-disabled:hover:text-muted-foreground motion-reduce:transition-none',
|
|
163
|
+
className
|
|
164
|
+
)}
|
|
165
|
+
{...props}
|
|
166
|
+
/>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
171
|
+
return (
|
|
172
|
+
<TabsPrimitive.Panel
|
|
173
|
+
data-slot="tabs-content"
|
|
174
|
+
className={mergeStateClassName(
|
|
175
|
+
'min-w-0 rounded-b-lg p-3 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&>:first-child]:mt-0 [&>:last-child]:mb-0 [&>[data-rdx-code-block]]:my-3 [&_[data-rdx-code-block]>pre]:p-3',
|
|
176
|
+
className
|
|
177
|
+
)}
|
|
178
|
+
{...props}
|
|
179
|
+
/>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
DropdownMenuContent,
|
|
11
11
|
DropdownMenuGroup,
|
|
12
12
|
DropdownMenuItem,
|
|
13
|
+
DropdownMenuLinkItem,
|
|
13
14
|
DropdownMenuLabel,
|
|
14
15
|
DropdownMenuSeparator,
|
|
15
16
|
DropdownMenuShortcut,
|
|
@@ -29,7 +30,7 @@ function VersionIcon({ kind }: { kind: VersionOption['kind'] }) {
|
|
|
29
30
|
return <Icon className="size-4" />
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
+
/** Shared version menu for the runtime and development showcase. */
|
|
33
34
|
export function VersionDropdown({
|
|
34
35
|
options,
|
|
35
36
|
currentId,
|
|
@@ -61,44 +62,47 @@ export function VersionDropdown({
|
|
|
61
62
|
<DropdownMenuItem
|
|
62
63
|
key={option.id}
|
|
63
64
|
className={className}
|
|
64
|
-
|
|
65
|
+
onClick={() => onSelect(option.id)}
|
|
65
66
|
>
|
|
66
67
|
{content}
|
|
67
68
|
</DropdownMenuItem>
|
|
68
69
|
) : (
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
<DropdownMenuLinkItem
|
|
71
|
+
key={option.id}
|
|
72
|
+
className={className}
|
|
73
|
+
closeOnClick
|
|
74
|
+
render={<Link href={option.href} />}
|
|
75
|
+
aria-current={option.id === current.id ? 'true' : undefined}
|
|
76
|
+
>
|
|
77
|
+
{content}
|
|
78
|
+
</DropdownMenuLinkItem>
|
|
77
79
|
)
|
|
78
80
|
}
|
|
79
81
|
return (
|
|
80
82
|
<DropdownMenu>
|
|
81
|
-
<DropdownMenuTrigger
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
<DropdownMenuTrigger
|
|
84
|
+
render={
|
|
85
|
+
<Button
|
|
86
|
+
data-rdx-version-trigger
|
|
87
|
+
variant="outline"
|
|
88
|
+
size="sm"
|
|
89
|
+
className="min-w-[134px] capitalize"
|
|
90
|
+
aria-label={`Documentation version: ${current.label}`}
|
|
91
|
+
/>
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
{current.label}
|
|
95
|
+
<DropdownMenuShortcut
|
|
96
|
+
className={
|
|
97
|
+
current.kind === 'live'
|
|
98
|
+
? 'text-blue-500'
|
|
99
|
+
: current.kind === 'archived'
|
|
100
|
+
? 'text-red-500'
|
|
101
|
+
: 'text-primary'
|
|
102
|
+
}
|
|
88
103
|
>
|
|
89
|
-
{current.
|
|
90
|
-
|
|
91
|
-
className={
|
|
92
|
-
current.kind === 'live'
|
|
93
|
-
? 'text-blue-500'
|
|
94
|
-
: current.kind === 'archived'
|
|
95
|
-
? 'text-red-500'
|
|
96
|
-
: 'text-primary'
|
|
97
|
-
}
|
|
98
|
-
>
|
|
99
|
-
<VersionIcon kind={current.kind} />
|
|
100
|
-
</DropdownMenuShortcut>
|
|
101
|
-
</Button>
|
|
104
|
+
<VersionIcon kind={current.kind} />
|
|
105
|
+
</DropdownMenuShortcut>
|
|
102
106
|
</DropdownMenuTrigger>
|
|
103
107
|
<DropdownMenuContent className="w-56" align="start" collisionPadding={8}>
|
|
104
108
|
{live.length > 0 && (
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { ArrowRight } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
import { Callout } from './callout'
|
|
5
|
+
|
|
6
|
+
/** Presentation only: version selection remains the responsibility of the caller. */
|
|
7
|
+
export function VersionNotice({
|
|
8
|
+
kind,
|
|
9
|
+
currentHref,
|
|
10
|
+
}: {
|
|
11
|
+
kind: 'canary' | 'outdated'
|
|
12
|
+
currentHref: string
|
|
13
|
+
}) {
|
|
14
|
+
const isCanary = kind === 'canary'
|
|
15
|
+
return (
|
|
16
|
+
<Callout
|
|
17
|
+
data-rdx-version-notice={kind}
|
|
18
|
+
variant={isCanary ? 'tip' : 'warning'}
|
|
19
|
+
title={isCanary ? 'Pre-release documentation' : 'Outdated documentation'}
|
|
20
|
+
className="mt-0 mb-5"
|
|
21
|
+
>
|
|
22
|
+
<p>
|
|
23
|
+
{isCanary
|
|
24
|
+
? 'This is a pre-release version of the documentation.'
|
|
25
|
+
: 'This is an outdated version of the documentation.'}{' '}
|
|
26
|
+
<Link
|
|
27
|
+
href={currentHref}
|
|
28
|
+
className="font-medium text-foreground underline decoration-current/40 underline-offset-4 transition-colors hover:decoration-current focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
29
|
+
>
|
|
30
|
+
Go back to the current version
|
|
31
|
+
<ArrowRight
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
className="ml-1 inline size-3.5 align-text-bottom"
|
|
34
|
+
/>
|
|
35
|
+
</Link>
|
|
36
|
+
</p>
|
|
37
|
+
</Callout>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { RdxSearchEntry, RdxSearchIndex } from '@lexibase/rdx-types'
|
|
2
|
+
|
|
3
|
+
export function normalizeSearch(value: string) {
|
|
4
|
+
return value.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function prepareSearch(index: RdxSearchIndex) {
|
|
8
|
+
if (index.schemaVersion !== 1 || !Array.isArray(index.entries))
|
|
9
|
+
throw new Error('Invalid search index')
|
|
10
|
+
return index.entries.map((entry) => {
|
|
11
|
+
if (
|
|
12
|
+
!['href', 'title', 'category', 'section', 'text'].every(
|
|
13
|
+
(field) => typeof entry[field as keyof RdxSearchEntry] === 'string'
|
|
14
|
+
) ||
|
|
15
|
+
!/^\/docs(?:\/|$)/.test(entry.href)
|
|
16
|
+
)
|
|
17
|
+
throw new Error('Invalid search entry')
|
|
18
|
+
return {
|
|
19
|
+
entry,
|
|
20
|
+
title: normalizeSearch(entry.title),
|
|
21
|
+
section: normalizeSearch(entry.section),
|
|
22
|
+
category: normalizeSearch(entry.category),
|
|
23
|
+
text: normalizeSearch(entry.text),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type PreparedSearch = ReturnType<typeof prepareSearch>
|
|
29
|
+
|
|
30
|
+
/** Require every query term, favor page/section titles, and bound result count. */
|
|
31
|
+
export function searchDocumentation(index: PreparedSearch, query: string) {
|
|
32
|
+
const normalized = normalizeSearch(query.trim().slice(0, 120))
|
|
33
|
+
const terms = [...new Set(normalized.split(/\s+/).filter(Boolean))].slice(
|
|
34
|
+
0,
|
|
35
|
+
8
|
|
36
|
+
)
|
|
37
|
+
if (!terms.length) return []
|
|
38
|
+
return index
|
|
39
|
+
.map((item) => {
|
|
40
|
+
let score = 0
|
|
41
|
+
for (const term of terms) {
|
|
42
|
+
const weight =
|
|
43
|
+
(item.title.includes(term) ? 12 : 0) +
|
|
44
|
+
(item.section.includes(term) ? 10 : 0) +
|
|
45
|
+
(item.category.includes(term) ? 2 : 0) +
|
|
46
|
+
(item.text.includes(term) ? 1 : 0)
|
|
47
|
+
if (!weight) return { item, score: 0 }
|
|
48
|
+
score += weight
|
|
49
|
+
}
|
|
50
|
+
if (item.title === normalized) score += 30
|
|
51
|
+
if (item.section === normalized) score += 25
|
|
52
|
+
return { item, score }
|
|
53
|
+
})
|
|
54
|
+
.filter((item) => item.score > 0)
|
|
55
|
+
.sort(
|
|
56
|
+
(a, b) =>
|
|
57
|
+
b.score - a.score || a.item.entry.href.localeCompare(b.item.entry.href)
|
|
58
|
+
)
|
|
59
|
+
.slice(0, 12)
|
|
60
|
+
.map(({ item }) => {
|
|
61
|
+
const term = terms.find((term) => item.text.includes(term))
|
|
62
|
+
const offset = term ? Math.max(0, item.text.indexOf(term) - 50) : 0
|
|
63
|
+
return {
|
|
64
|
+
...item.entry,
|
|
65
|
+
excerpt: `${offset ? '…' : ''}${item.entry.text.slice(offset, offset + 170)}${item.entry.text.length > offset + 170 ? '…' : ''}`,
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -4,3 +4,13 @@ import { twMerge } from 'tailwind-merge'
|
|
|
4
4
|
export function cn(...inputs: ClassValue[]) {
|
|
5
5
|
return twMerge(clsx(inputs))
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
/** Preserve Base UI state callbacks while merging the shared theme classes. */
|
|
9
|
+
export function mergeStateClassName<State>(
|
|
10
|
+
base: string,
|
|
11
|
+
className?: string | ((state: State) => string | undefined)
|
|
12
|
+
) {
|
|
13
|
+
return typeof className === 'function'
|
|
14
|
+
? (state: State) => cn(base, className(state))
|
|
15
|
+
: cn(base, className)
|
|
16
|
+
}
|
package/src/styles/globals.css
CHANGED
|
@@ -77,6 +77,28 @@
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
@theme inline {
|
|
80
|
+
/* shadcn accordion animation, using the height measured by Base UI. */
|
|
81
|
+
--animate-accordion-down: accordion-down 0.2s ease-out;
|
|
82
|
+
--animate-accordion-up: accordion-up 0.2s ease-out;
|
|
83
|
+
|
|
84
|
+
@keyframes accordion-down {
|
|
85
|
+
from {
|
|
86
|
+
height: 0;
|
|
87
|
+
}
|
|
88
|
+
to {
|
|
89
|
+
height: var(--accordion-panel-height);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@keyframes accordion-up {
|
|
94
|
+
from {
|
|
95
|
+
height: var(--accordion-panel-height);
|
|
96
|
+
}
|
|
97
|
+
to {
|
|
98
|
+
height: 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
--color-background: var(--background);
|
|
81
103
|
--color-foreground: var(--foreground);
|
|
82
104
|
--color-card: var(--card);
|
|
@@ -123,3 +145,15 @@
|
|
|
123
145
|
@apply bg-background text-foreground;
|
|
124
146
|
}
|
|
125
147
|
}
|
|
148
|
+
|
|
149
|
+
/* Shiki uses the same GitHub light/dark palette as shadcn's highlighter.
|
|
150
|
+
Keep the approved RDX surface and apply only token foreground colors. */
|
|
151
|
+
[data-rdx-code-block] pre.shiki,
|
|
152
|
+
[data-rdx-code-block] pre.shiki span {
|
|
153
|
+
color: var(--shiki-light, inherit);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.dark [data-rdx-code-block] pre.shiki,
|
|
157
|
+
.dark [data-rdx-code-block] pre.shiki span {
|
|
158
|
+
color: var(--shiki-dark, inherit);
|
|
159
|
+
}
|