@lexibase/rdx-ui 3.4.0 → 3.5.1
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/package.json +2 -2
- package/src/components/button-group.tsx +87 -0
- package/src/components/card.tsx +102 -0
- package/src/components/copy-page.tsx +124 -0
- package/src/components/docs-sidebar.tsx +18 -4
- package/src/components/docs-toc.tsx +6 -2
- package/src/components/edit-page-link.tsx +22 -0
- package/src/components/heading-permalink.tsx +74 -0
- package/src/components/mdx-code-block.tsx +8 -2
- package/src/components/mdx-components.tsx +47 -18
- package/src/components/steps.tsx +141 -0
- package/src/lib/i18n.ts +24 -0
- package/src/styles/globals.css +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexibase/rdx-ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.18.0 || ^24.11.0"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"tailwind-merge": "^3.7.0",
|
|
40
40
|
"tailwindcss": "^4.3.3",
|
|
41
41
|
"tw-animate-css": "^1.4.0",
|
|
42
|
-
"@lexibase/rdx-types": "2.
|
|
42
|
+
"@lexibase/rdx-types": "2.3.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"next": "^16.4.2",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mergeProps } from '@base-ui/react/merge-props'
|
|
2
|
+
import { useRender } from '@base-ui/react/use-render'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import { cn } from '@lexibase/rdx-ui/lib/utils'
|
|
5
|
+
|
|
6
|
+
import { Separator } from './separator'
|
|
7
|
+
|
|
8
|
+
const buttonGroupVariants = cva(
|
|
9
|
+
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
orientation: {
|
|
13
|
+
horizontal:
|
|
14
|
+
'*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0',
|
|
15
|
+
vertical:
|
|
16
|
+
'flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
orientation: 'horizontal',
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
function ButtonGroup({
|
|
26
|
+
className,
|
|
27
|
+
orientation,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
role="group"
|
|
33
|
+
data-slot="button-group"
|
|
34
|
+
data-orientation={orientation}
|
|
35
|
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ButtonGroupText({
|
|
42
|
+
className,
|
|
43
|
+
render,
|
|
44
|
+
...props
|
|
45
|
+
}: useRender.ComponentProps<'div'>) {
|
|
46
|
+
return useRender({
|
|
47
|
+
defaultTagName: 'div',
|
|
48
|
+
props: mergeProps<'div'>(
|
|
49
|
+
{
|
|
50
|
+
className: cn(
|
|
51
|
+
"flex items-center gap-2 rounded-lg border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
52
|
+
className
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
props
|
|
56
|
+
),
|
|
57
|
+
render,
|
|
58
|
+
state: {
|
|
59
|
+
slot: 'button-group-text',
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ButtonGroupSeparator({
|
|
65
|
+
className,
|
|
66
|
+
orientation = 'vertical',
|
|
67
|
+
...props
|
|
68
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
69
|
+
return (
|
|
70
|
+
<Separator
|
|
71
|
+
data-slot="button-group-separator"
|
|
72
|
+
orientation={orientation}
|
|
73
|
+
className={cn(
|
|
74
|
+
'relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
ButtonGroup,
|
|
84
|
+
ButtonGroupSeparator,
|
|
85
|
+
ButtonGroupText,
|
|
86
|
+
buttonGroupVariants,
|
|
87
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '@lexibase/rdx-ui/lib/utils'
|
|
3
|
+
|
|
4
|
+
function Card({
|
|
5
|
+
className,
|
|
6
|
+
size = 'default',
|
|
7
|
+
...props
|
|
8
|
+
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
data-slot="card"
|
|
12
|
+
data-size={size}
|
|
13
|
+
className={cn(
|
|
14
|
+
'group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
data-slot="card-header"
|
|
26
|
+
className={cn(
|
|
27
|
+
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-slot="card-title"
|
|
39
|
+
className={cn(
|
|
40
|
+
'cn-font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
data-slot="card-description"
|
|
52
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="card-action"
|
|
62
|
+
className={cn(
|
|
63
|
+
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-slot="card-content"
|
|
75
|
+
className={cn('px-(--card-spacing)', className)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
data-slot="card-footer"
|
|
85
|
+
className={cn(
|
|
86
|
+
'flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)',
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
Card,
|
|
96
|
+
CardHeader,
|
|
97
|
+
CardFooter,
|
|
98
|
+
CardTitle,
|
|
99
|
+
CardAction,
|
|
100
|
+
CardDescription,
|
|
101
|
+
CardContent,
|
|
102
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Check, ChevronDown, CircleAlert, Copy, FileText } from 'lucide-react'
|
|
4
|
+
import { useEffect, useRef, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import { useMessages } from '../providers/locale-provider'
|
|
7
|
+
import { Button } from './button'
|
|
8
|
+
import { ButtonGroup } from './button-group'
|
|
9
|
+
import {
|
|
10
|
+
DropdownMenu,
|
|
11
|
+
DropdownMenuContent,
|
|
12
|
+
DropdownMenuLinkItem,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
} from './dropdown-menu'
|
|
15
|
+
|
|
16
|
+
export function CopyPage({ href }: { href: string }) {
|
|
17
|
+
const messages = useMessages()
|
|
18
|
+
const [status, setStatus] = useState<'idle' | 'copying' | 'copied' | 'error'>(
|
|
19
|
+
'idle'
|
|
20
|
+
)
|
|
21
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
22
|
+
const request = useRef<AbortController | undefined>(undefined)
|
|
23
|
+
|
|
24
|
+
useEffect(
|
|
25
|
+
() => () => {
|
|
26
|
+
request.current?.abort()
|
|
27
|
+
clearTimeout(timer.current)
|
|
28
|
+
},
|
|
29
|
+
[]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
async function copyPage() {
|
|
33
|
+
if (request.current) return
|
|
34
|
+
const controller = new AbortController()
|
|
35
|
+
request.current = controller
|
|
36
|
+
clearTimeout(timer.current)
|
|
37
|
+
setStatus('copying')
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (!navigator.clipboard) throw new Error('Clipboard unavailable')
|
|
41
|
+
const markdown = fetch(href, { signal: controller.signal }).then(
|
|
42
|
+
(response) => {
|
|
43
|
+
if (
|
|
44
|
+
!response.ok ||
|
|
45
|
+
!response.headers.get('content-type')?.startsWith('text/markdown')
|
|
46
|
+
)
|
|
47
|
+
throw new Error('Markdown unavailable')
|
|
48
|
+
return response.text()
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
if (navigator.clipboard.write && typeof ClipboardItem !== 'undefined') {
|
|
52
|
+
// Initiate the write inside the click's activation, even while fetching.
|
|
53
|
+
const blob = markdown.then(
|
|
54
|
+
(text) => new Blob([text], { type: 'text/plain' })
|
|
55
|
+
)
|
|
56
|
+
// ClipboardItem construction can fail before the browser consumes the blob.
|
|
57
|
+
void blob.catch(() => {})
|
|
58
|
+
await Promise.all([
|
|
59
|
+
blob,
|
|
60
|
+
navigator.clipboard.write([
|
|
61
|
+
new ClipboardItem({ 'text/plain': blob }),
|
|
62
|
+
]),
|
|
63
|
+
])
|
|
64
|
+
} else {
|
|
65
|
+
await navigator.clipboard.writeText(await markdown)
|
|
66
|
+
}
|
|
67
|
+
if (controller.signal.aborted) return
|
|
68
|
+
setStatus('copied')
|
|
69
|
+
timer.current = setTimeout(() => setStatus('idle'), 1800)
|
|
70
|
+
} catch {
|
|
71
|
+
if (!controller.signal.aborted) setStatus('error')
|
|
72
|
+
controller.abort()
|
|
73
|
+
} finally {
|
|
74
|
+
request.current = undefined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const Icon =
|
|
79
|
+
status === 'copied' ? Check : status === 'error' ? CircleAlert : Copy
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<ButtonGroup
|
|
83
|
+
aria-label={messages.copyPageOptions}
|
|
84
|
+
className="ms-auto shrink-0"
|
|
85
|
+
>
|
|
86
|
+
<Button
|
|
87
|
+
type="button"
|
|
88
|
+
variant="outline"
|
|
89
|
+
size="sm"
|
|
90
|
+
onClick={copyPage}
|
|
91
|
+
aria-busy={status === 'copying'}
|
|
92
|
+
title={status === 'error' ? messages.copyPageError : undefined}
|
|
93
|
+
>
|
|
94
|
+
<Icon aria-hidden="true" />
|
|
95
|
+
{messages.copyThisPage}
|
|
96
|
+
</Button>
|
|
97
|
+
<DropdownMenu>
|
|
98
|
+
<DropdownMenuTrigger
|
|
99
|
+
aria-label={messages.copyPageOptions}
|
|
100
|
+
render={<Button type="button" variant="outline" size="icon-sm" />}
|
|
101
|
+
>
|
|
102
|
+
<ChevronDown aria-hidden="true" />
|
|
103
|
+
</DropdownMenuTrigger>
|
|
104
|
+
<DropdownMenuContent align="end">
|
|
105
|
+
<DropdownMenuLinkItem
|
|
106
|
+
render={<a href={href} target="_blank" rel="noopener noreferrer" />}
|
|
107
|
+
closeOnClick
|
|
108
|
+
>
|
|
109
|
+
<FileText aria-hidden="true" />
|
|
110
|
+
{messages.viewMarkdown}
|
|
111
|
+
<span className="sr-only"> ({messages.opensInNewTab})</span>
|
|
112
|
+
</DropdownMenuLinkItem>
|
|
113
|
+
</DropdownMenuContent>
|
|
114
|
+
</DropdownMenu>
|
|
115
|
+
<span role="status" className="sr-only">
|
|
116
|
+
{status === 'copied'
|
|
117
|
+
? messages.pageCopied
|
|
118
|
+
: status === 'error'
|
|
119
|
+
? messages.copyPageError
|
|
120
|
+
: ''}
|
|
121
|
+
</span>
|
|
122
|
+
</ButtonGroup>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -5,6 +5,7 @@ import { useMessages } from '../providers/locale-provider'
|
|
|
5
5
|
import { useEffect, useRef, useState } from 'react'
|
|
6
6
|
import { SidebarClose, SidebarOpen } from 'lucide-react'
|
|
7
7
|
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
|
8
|
+
import type { RdxPageHighlight } from '@lexibase/rdx-types'
|
|
8
9
|
|
|
9
10
|
import { Button } from './button'
|
|
10
11
|
import { Separator } from './separator'
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
|
|
19
20
|
export type SidebarCategory = {
|
|
20
21
|
title: string
|
|
21
|
-
links: { href: string; label: string }[]
|
|
22
|
+
links: { href: string; label: string; highlighting?: RdxPageHighlight }[]
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function DocsSidebar({
|
|
@@ -73,7 +74,11 @@ export function DocsSidebar({
|
|
|
73
74
|
variant="ghost"
|
|
74
75
|
size="sm"
|
|
75
76
|
active={pathname === link.href}
|
|
76
|
-
aria-label={
|
|
77
|
+
aria-label={
|
|
78
|
+
link.highlighting
|
|
79
|
+
? `${link.label} (${link.highlighting === 'important' ? messages.sidebarImportant : messages.sidebarNew})`
|
|
80
|
+
: link.label
|
|
81
|
+
}
|
|
77
82
|
href={link.href}
|
|
78
83
|
transitionTypes={
|
|
79
84
|
currentIndex < 0 || link.href === pathname
|
|
@@ -89,6 +94,15 @@ export function DocsSidebar({
|
|
|
89
94
|
className={`h-auto min-h-8 w-full min-w-0 justify-start whitespace-normal py-1.5 text-left text-sm [overflow-wrap:anywhere] ${pathname === link.href ? 'text-accent-foreground' : 'text-primary'}`}
|
|
90
95
|
>
|
|
91
96
|
{link.label}
|
|
97
|
+
{link.highlighting && (
|
|
98
|
+
<span
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
data-rdx-sidebar-highlight={link.highlighting}
|
|
101
|
+
className={`relative ms-auto size-2 shrink-0 rounded-full bg-current ${link.highlighting === 'important' ? 'text-amber-600 dark:text-amber-400' : 'text-blue-600 dark:text-blue-400'}`}
|
|
102
|
+
>
|
|
103
|
+
<span className="absolute inset-0 rounded-full bg-current opacity-0 motion-safe:animate-[rdx-sidebar-highlight_2.8s_ease-out_infinite]" />
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
92
106
|
</Button>
|
|
93
107
|
))}
|
|
94
108
|
{index < categories.length - 1 && <Separator className="my-4" />}
|
|
@@ -116,7 +130,7 @@ export function DocsSidebar({
|
|
|
116
130
|
</div>
|
|
117
131
|
<aside
|
|
118
132
|
data-rdx-sidebar
|
|
119
|
-
className="hidden md:block h-full w-40 lg:w-56 shrink-0 border-r border-border overflow-y-auto p-4"
|
|
133
|
+
className="hidden md:block h-full w-40 lg:w-56 shrink-0 border-r border-border overflow-y-auto scrollbar-none p-4"
|
|
120
134
|
>
|
|
121
135
|
{content}
|
|
122
136
|
</aside>
|
|
@@ -128,7 +142,7 @@ export function DocsSidebar({
|
|
|
128
142
|
showCloseButton={false}
|
|
129
143
|
portalProps={{ keepMounted: true }}
|
|
130
144
|
data-rdx-mobile-sidebar
|
|
131
|
-
className="md:hidden w-full! max-w-none! h-dvh border-0 bg-background text-foreground gap-0 p-4 overflow-y-auto translate-none! opacity-100! transition-none!"
|
|
145
|
+
className="md:hidden w-full! max-w-none! h-dvh border-0 bg-background text-foreground gap-0 p-4 overflow-y-auto scrollbar-none translate-none! opacity-100! transition-none!"
|
|
132
146
|
render={
|
|
133
147
|
<motion.div
|
|
134
148
|
initial="closed"
|
|
@@ -138,13 +138,17 @@ function PageToc({ contentId }: { contentId: string }) {
|
|
|
138
138
|
element.getClientRects().length > 0 &&
|
|
139
139
|
!!element.id &&
|
|
140
140
|
!!element.textContent?.trim() &&
|
|
141
|
+
!element.closest('[data-slot="step-panel"][inert]') &&
|
|
141
142
|
getComputedStyle(element).visibility !== 'hidden'
|
|
142
143
|
)
|
|
143
144
|
setHeadings(
|
|
144
145
|
elements
|
|
145
146
|
.map((element) => ({
|
|
146
147
|
id: element.id,
|
|
147
|
-
title:
|
|
148
|
+
title:
|
|
149
|
+
(
|
|
150
|
+
element.querySelector('[data-rdx-heading-text]') ?? element
|
|
151
|
+
).textContent?.trim() ?? '',
|
|
148
152
|
depth: Number(element.tagName.slice(1)),
|
|
149
153
|
}))
|
|
150
154
|
.filter((heading) => heading.title)
|
|
@@ -158,7 +162,7 @@ function PageToc({ contentId }: { contentId: string }) {
|
|
|
158
162
|
subtree: true,
|
|
159
163
|
characterData: true,
|
|
160
164
|
attributes: true,
|
|
161
|
-
attributeFilter: ['hidden', 'style', 'class', 'id'],
|
|
165
|
+
attributeFilter: ['hidden', 'style', 'class', 'id', 'inert'],
|
|
162
166
|
})
|
|
163
167
|
scrollTarget.addEventListener('scroll', schedule, { passive: true })
|
|
164
168
|
window.addEventListener('resize', collect)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Pencil } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
import { getMessages } from '../lib/i18n'
|
|
4
|
+
import { Button } from './button'
|
|
5
|
+
|
|
6
|
+
export function EditPageLink({ href, lang }: { href: string; lang?: string }) {
|
|
7
|
+
const messages = getMessages(lang)
|
|
8
|
+
return (
|
|
9
|
+
<Button
|
|
10
|
+
variant="link"
|
|
11
|
+
size="link"
|
|
12
|
+
href={href}
|
|
13
|
+
target="_blank"
|
|
14
|
+
rel="noopener noreferrer"
|
|
15
|
+
className="mb-4 self-start text-sm text-muted-foreground hover:text-foreground"
|
|
16
|
+
>
|
|
17
|
+
<Pencil aria-hidden="true" className="me-1.5 inline size-3.5" />
|
|
18
|
+
{messages.editThisPage}
|
|
19
|
+
<span className="sr-only"> ({messages.opensInNewTab})</span>
|
|
20
|
+
</Button>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Check, Hash } from 'lucide-react'
|
|
4
|
+
import {
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react'
|
|
11
|
+
|
|
12
|
+
import { useMessages } from '../providers/locale-provider'
|
|
13
|
+
|
|
14
|
+
export function HeadingPermalink({
|
|
15
|
+
id,
|
|
16
|
+
children,
|
|
17
|
+
}: {
|
|
18
|
+
id: string
|
|
19
|
+
children: ReactNode
|
|
20
|
+
}) {
|
|
21
|
+
const messages = useMessages()
|
|
22
|
+
const [copied, setCopied] = useState(false)
|
|
23
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
24
|
+
useEffect(() => () => clearTimeout(timer.current), [])
|
|
25
|
+
|
|
26
|
+
const href = `#${encodeURIComponent(id)}`
|
|
27
|
+
|
|
28
|
+
async function copy(event: MouseEvent<HTMLAnchorElement>) {
|
|
29
|
+
if (
|
|
30
|
+
event.defaultPrevented ||
|
|
31
|
+
event.metaKey ||
|
|
32
|
+
event.ctrlKey ||
|
|
33
|
+
event.altKey ||
|
|
34
|
+
event.shiftKey ||
|
|
35
|
+
!navigator.clipboard?.writeText
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await navigator.clipboard.writeText(event.currentTarget.href)
|
|
41
|
+
} catch {
|
|
42
|
+
// The native link still takes the reader to the section.
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setCopied(true)
|
|
47
|
+
clearTimeout(timer.current)
|
|
48
|
+
timer.current = setTimeout(() => setCopied(false), 1800)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const Icon = copied ? Check : Hash
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<a
|
|
56
|
+
href={href}
|
|
57
|
+
onClick={copy}
|
|
58
|
+
title={messages.sectionPermalink}
|
|
59
|
+
className="rounded-sm text-inherit no-underline underline-offset-4 hover:underline focus-visible:underline focus-visible:outline-2 focus-visible:outline-ring"
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</a>
|
|
63
|
+
<span
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
className="pointer-events-none ms-1 inline-flex size-6 items-center justify-center align-middle text-muted-foreground pointer-fine:opacity-0 pointer-fine:group-hover/mdx-heading:opacity-100 pointer-fine:group-focus-within/mdx-heading:opacity-100"
|
|
66
|
+
>
|
|
67
|
+
<Icon className="size-4" />
|
|
68
|
+
</span>
|
|
69
|
+
<span role="status" className="sr-only">
|
|
70
|
+
{copied ? messages.sectionLinkCopied : ''}
|
|
71
|
+
</span>
|
|
72
|
+
</>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -106,6 +106,7 @@ export function MdxCodeBlock({
|
|
|
106
106
|
if (lineNumbers && node === '\n') return null
|
|
107
107
|
if (!isValidElement<LineProps>(node)) return node
|
|
108
108
|
const variant = node.props['data-rdx-line-variant']
|
|
109
|
+
const hasDiffMarker = variant === 'added' || variant === 'removed'
|
|
109
110
|
if (
|
|
110
111
|
!variant &&
|
|
111
112
|
!(lineNumbers && node.props.className?.split(/\s+/).includes('line'))
|
|
@@ -124,15 +125,20 @@ export function MdxCodeBlock({
|
|
|
124
125
|
return (
|
|
125
126
|
<CodeBlockLine
|
|
126
127
|
key={node.key}
|
|
128
|
+
className={
|
|
129
|
+
hasDiffMarker
|
|
130
|
+
? '[&>[data-slot=code-block-line-content]]:ps-[1ch]'
|
|
131
|
+
: undefined
|
|
132
|
+
}
|
|
127
133
|
variant={
|
|
128
|
-
|
|
134
|
+
hasDiffMarker
|
|
129
135
|
? variant
|
|
130
136
|
: variant === 'hunk'
|
|
131
137
|
? 'highlighted'
|
|
132
138
|
: 'default'
|
|
133
139
|
}
|
|
134
140
|
>
|
|
135
|
-
{
|
|
141
|
+
{hasDiffMarker
|
|
136
142
|
? withoutMarker(node.props.children)
|
|
137
143
|
: node.props.children}
|
|
138
144
|
</CodeBlockLine>
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from './code-block'
|
|
30
30
|
import { MdxCodeBlock } from './mdx-code-block'
|
|
31
31
|
import { Kbd, KbdGroup } from './kbd'
|
|
32
|
+
import { HeadingPermalink } from './heading-permalink'
|
|
32
33
|
import { MdxKeyboardShortcut } from './mdx-keyboard-shortcut'
|
|
33
34
|
import {
|
|
34
35
|
Table,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
TableRow,
|
|
42
43
|
} from './table'
|
|
43
44
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs'
|
|
45
|
+
import { Steps, Step, StepTitle } from './steps'
|
|
44
46
|
import { Separator } from '@lexibase/rdx-ui/components/separator'
|
|
45
47
|
|
|
46
48
|
function linkedImages(children: ReactNode): ReactNode {
|
|
@@ -71,16 +73,46 @@ function MdxTable({ lang, className, ...props }: ComponentProps<'table'>) {
|
|
|
71
73
|
)
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
function containsLink(children: ReactNode): boolean {
|
|
77
|
+
return Children.toArray(children).some(
|
|
78
|
+
(child) =>
|
|
79
|
+
isValidElement<{ href?: string; children?: ReactNode }>(child) &&
|
|
80
|
+
(child.type === 'a' ||
|
|
81
|
+
child.props.href !== undefined ||
|
|
82
|
+
containsLink(child.props.children))
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function heading(Tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', styles: string) {
|
|
87
|
+
return function MdxHeading({
|
|
88
|
+
className,
|
|
89
|
+
children,
|
|
90
|
+
id,
|
|
91
|
+
...props
|
|
92
|
+
}: ComponentProps<'h2'>) {
|
|
93
|
+
const title = <span data-rdx-heading-text>{children}</span>
|
|
94
|
+
return (
|
|
95
|
+
<Tag
|
|
96
|
+
id={id}
|
|
97
|
+
className={cn('group/mdx-heading', styles, className)}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
{id && !containsLink(children) ? (
|
|
101
|
+
<HeadingPermalink key={id} id={id}>
|
|
102
|
+
{title}
|
|
103
|
+
</HeadingPermalink>
|
|
104
|
+
) : (
|
|
105
|
+
title
|
|
106
|
+
)}
|
|
107
|
+
</Tag>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
74
112
|
export const mdxComponents = {
|
|
75
|
-
h1: (
|
|
76
|
-
|
|
77
|
-
),
|
|
78
|
-
h2: ({ className, ...props }: ComponentProps<'h2'>) => (
|
|
79
|
-
<h2 className={cn('text-2xl font-bold my-6', className)} {...props} />
|
|
80
|
-
),
|
|
81
|
-
h3: ({ className, ...props }: ComponentProps<'h3'>) => (
|
|
82
|
-
<h3 className={cn('text-xl font-bold my-6', className)} {...props} />
|
|
83
|
-
),
|
|
113
|
+
h1: heading('h1', 'text-4xl font-bold my-6'),
|
|
114
|
+
h2: heading('h2', 'text-2xl font-bold my-6'),
|
|
115
|
+
h3: heading('h3', 'text-xl font-bold my-6'),
|
|
84
116
|
p: ({ className, ...props }: ComponentProps<'p'>) => (
|
|
85
117
|
<p className={cn('my-4', className)} {...props} />
|
|
86
118
|
),
|
|
@@ -122,15 +154,9 @@ export const mdxComponents = {
|
|
|
122
154
|
/>
|
|
123
155
|
),
|
|
124
156
|
|
|
125
|
-
h4: (
|
|
126
|
-
|
|
127
|
-
),
|
|
128
|
-
h5: ({ className, ...props }: ComponentProps<'h5'>) => (
|
|
129
|
-
<h5 className={cn('text-base font-semibold my-4', className)} {...props} />
|
|
130
|
-
),
|
|
131
|
-
h6: ({ className, ...props }: ComponentProps<'h6'>) => (
|
|
132
|
-
<h6 className={cn('text-sm font-semibold my-4', className)} {...props} />
|
|
133
|
-
),
|
|
157
|
+
h4: heading('h4', 'text-lg font-semibold my-4'),
|
|
158
|
+
h5: heading('h5', 'text-base font-semibold my-4'),
|
|
159
|
+
h6: heading('h6', 'text-sm font-semibold my-4'),
|
|
134
160
|
hr: ({ className, ...props }: ComponentProps<'hr'>) => (
|
|
135
161
|
<hr className={cn('my-8 border-border', className)} {...props} />
|
|
136
162
|
),
|
|
@@ -169,6 +195,9 @@ export const mdxComponents = {
|
|
|
169
195
|
AccordionItem,
|
|
170
196
|
AccordionTrigger,
|
|
171
197
|
AccordionContent,
|
|
198
|
+
Steps,
|
|
199
|
+
Step,
|
|
200
|
+
StepTitle,
|
|
172
201
|
Tabs,
|
|
173
202
|
TabsList,
|
|
174
203
|
TabsTrigger,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { motion, useReducedMotion } from 'motion/react'
|
|
4
|
+
import { Children, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/utils'
|
|
7
|
+
import { formatMessage } from '../lib/i18n'
|
|
8
|
+
import { useMessages } from '../providers/locale-provider'
|
|
9
|
+
import { Button } from './button'
|
|
10
|
+
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from './card'
|
|
11
|
+
|
|
12
|
+
function StepPanel({
|
|
13
|
+
children,
|
|
14
|
+
active,
|
|
15
|
+
}: {
|
|
16
|
+
children: ReactNode
|
|
17
|
+
active: boolean
|
|
18
|
+
}) {
|
|
19
|
+
const panel = useRef<HTMLDivElement>(null)
|
|
20
|
+
const wasActive = useRef(active)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (active && !wasActive.current)
|
|
24
|
+
panel.current?.focus({ preventScroll: true })
|
|
25
|
+
wasActive.current = active
|
|
26
|
+
}, [active])
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
ref={panel}
|
|
31
|
+
data-slot="step-panel"
|
|
32
|
+
tabIndex={-1}
|
|
33
|
+
inert={!active}
|
|
34
|
+
aria-hidden={!active || undefined}
|
|
35
|
+
className="min-w-0 shrink-0 basis-full focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring"
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Steps({ children }: { children: ReactNode }) {
|
|
43
|
+
const messages = useMessages()
|
|
44
|
+
const reduceMotion = useReducedMotion()
|
|
45
|
+
const steps = Children.toArray(children)
|
|
46
|
+
const [page, setPage] = useState(0)
|
|
47
|
+
const index = Math.min(page, steps.length - 1)
|
|
48
|
+
if (index < 0) return null
|
|
49
|
+
|
|
50
|
+
function navigate(direction: number) {
|
|
51
|
+
setPage((current) =>
|
|
52
|
+
Math.max(
|
|
53
|
+
0,
|
|
54
|
+
Math.min(
|
|
55
|
+
Math.min(current, steps.length - 1) + direction,
|
|
56
|
+
steps.length - 1
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card
|
|
64
|
+
className="mx-auto my-6 w-full min-w-0 max-w-xl text-base"
|
|
65
|
+
data-slot="steps"
|
|
66
|
+
>
|
|
67
|
+
<CardHeader>
|
|
68
|
+
<div aria-hidden="true" className="flex gap-1.5">
|
|
69
|
+
{steps.map((_, position) => (
|
|
70
|
+
<span
|
|
71
|
+
key={position}
|
|
72
|
+
className={cn(
|
|
73
|
+
'h-1.5 min-w-0 rounded-full motion-safe:transition-[flex-basis,background-color] motion-safe:duration-200',
|
|
74
|
+
position === index
|
|
75
|
+
? 'basis-10 bg-rdx-selected'
|
|
76
|
+
: 'basis-2.5 bg-muted-foreground/25'
|
|
77
|
+
)}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
<p
|
|
82
|
+
role="status"
|
|
83
|
+
className="m-0 mt-2 text-sm font-medium text-muted-foreground"
|
|
84
|
+
>
|
|
85
|
+
{formatMessage(messages.stepProgress, {
|
|
86
|
+
current: String(index + 1),
|
|
87
|
+
total: String(steps.length),
|
|
88
|
+
})}
|
|
89
|
+
</p>
|
|
90
|
+
</CardHeader>
|
|
91
|
+
<CardContent className="min-w-0">
|
|
92
|
+
<div className="min-h-32 overflow-clip">
|
|
93
|
+
<motion.div
|
|
94
|
+
className="flex items-start"
|
|
95
|
+
initial={false}
|
|
96
|
+
animate={{ x: `${-index * 100}%` }}
|
|
97
|
+
transition={{
|
|
98
|
+
duration: reduceMotion ? 0 : 0.24,
|
|
99
|
+
ease: 'easeInOut',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{steps.map((step, position) => (
|
|
103
|
+
<StepPanel key={position} active={position === index}>
|
|
104
|
+
{step}
|
|
105
|
+
</StepPanel>
|
|
106
|
+
))}
|
|
107
|
+
</motion.div>
|
|
108
|
+
</div>
|
|
109
|
+
</CardContent>
|
|
110
|
+
{steps.length > 1 && (
|
|
111
|
+
<CardFooter className="justify-between gap-2">
|
|
112
|
+
<Button
|
|
113
|
+
type="button"
|
|
114
|
+
variant="outline"
|
|
115
|
+
size="sm"
|
|
116
|
+
disabled={index === 0}
|
|
117
|
+
onClick={() => navigate(-1)}
|
|
118
|
+
>
|
|
119
|
+
{messages.previous}
|
|
120
|
+
</Button>
|
|
121
|
+
<Button
|
|
122
|
+
type="button"
|
|
123
|
+
size="sm"
|
|
124
|
+
disabled={index === steps.length - 1}
|
|
125
|
+
onClick={() => navigate(1)}
|
|
126
|
+
>
|
|
127
|
+
{messages.next}
|
|
128
|
+
</Button>
|
|
129
|
+
</CardFooter>
|
|
130
|
+
)}
|
|
131
|
+
</Card>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function Step({ children }: { children: ReactNode }) {
|
|
136
|
+
return <div className="min-w-0 [&>:last-child]:mb-0">{children}</div>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function StepTitle({ children }: { children: ReactNode }) {
|
|
140
|
+
return <CardTitle className="text-lg font-semibold">{children}</CardTitle>
|
|
141
|
+
}
|
package/src/lib/i18n.ts
CHANGED
|
@@ -31,8 +31,12 @@ const en = {
|
|
|
31
31
|
closeSidebar: 'Close sidebar',
|
|
32
32
|
previous: 'Previous',
|
|
33
33
|
next: 'Next',
|
|
34
|
+
stepProgress: 'Step {current} of {total}',
|
|
34
35
|
previousPage: 'Previous page',
|
|
35
36
|
nextPage: 'Next page',
|
|
37
|
+
sidebarImportant: 'Important',
|
|
38
|
+
sidebarNew: 'New',
|
|
39
|
+
editThisPage: 'Edit this page',
|
|
36
40
|
onThisPage: 'On this page',
|
|
37
41
|
search: 'Search…',
|
|
38
42
|
searchDocumentation: 'Search documentation',
|
|
@@ -60,6 +64,14 @@ const en = {
|
|
|
60
64
|
copyCodeError: 'Could not copy. Select the code and copy it manually.',
|
|
61
65
|
copyRetry: 'Could not copy. Try again or copy manually.',
|
|
62
66
|
copied: 'Copied!',
|
|
67
|
+
copyThisPage: 'Copy this page',
|
|
68
|
+
copyPageOptions: 'Page copy options',
|
|
69
|
+
viewMarkdown: 'View Markdown',
|
|
70
|
+
pageCopied: 'Page Markdown copied to clipboard.',
|
|
71
|
+
copyPageError:
|
|
72
|
+
'Could not copy the page. Open View Markdown to copy it manually.',
|
|
73
|
+
sectionPermalink: 'Copy link to this section',
|
|
74
|
+
sectionLinkCopied: 'Section link copied to clipboard.',
|
|
63
75
|
copyTab: 'Copy active tab',
|
|
64
76
|
documentationTabs: 'Documentation tabs',
|
|
65
77
|
tabCopied: 'Tab content copied to clipboard.',
|
|
@@ -132,8 +144,12 @@ const ptBR = {
|
|
|
132
144
|
closeSidebar: 'Fechar menu lateral',
|
|
133
145
|
previous: 'Anterior',
|
|
134
146
|
next: 'Próxima',
|
|
147
|
+
stepProgress: 'Etapa {current} de {total}',
|
|
135
148
|
previousPage: 'Página anterior',
|
|
136
149
|
nextPage: 'Próxima página',
|
|
150
|
+
sidebarImportant: 'Importante',
|
|
151
|
+
sidebarNew: 'Novo',
|
|
152
|
+
editThisPage: 'Editar esta página',
|
|
137
153
|
onThisPage: 'Nesta página',
|
|
138
154
|
search: 'Buscar…',
|
|
139
155
|
searchDocumentation: 'Buscar na documentação',
|
|
@@ -162,6 +178,14 @@ const ptBR = {
|
|
|
162
178
|
'Não foi possível copiar. Selecione o código e copie manualmente.',
|
|
163
179
|
copyRetry: 'Não foi possível copiar. Tente novamente ou copie manualmente.',
|
|
164
180
|
copied: 'Copiado!',
|
|
181
|
+
copyThisPage: 'Copiar esta página',
|
|
182
|
+
copyPageOptions: 'Opções de cópia da página',
|
|
183
|
+
viewMarkdown: 'Ver Markdown',
|
|
184
|
+
pageCopied: 'Markdown da página copiado para a área de transferência.',
|
|
185
|
+
copyPageError:
|
|
186
|
+
'Não foi possível copiar a página. Abra Ver Markdown para copiar manualmente.',
|
|
187
|
+
sectionPermalink: 'Copiar link para esta seção',
|
|
188
|
+
sectionLinkCopied: 'Link da seção copiado para a área de transferência.',
|
|
165
189
|
copyTab: 'Copiar aba ativa',
|
|
166
190
|
documentationTabs: 'Abas da documentação',
|
|
167
191
|
tabCopied: 'Conteúdo da aba copiado para a área de transferência.',
|
package/src/styles/globals.css
CHANGED
|
@@ -236,6 +236,18 @@
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
@keyframes rdx-sidebar-highlight {
|
|
240
|
+
0% {
|
|
241
|
+
opacity: 0.35;
|
|
242
|
+
transform: scale(1);
|
|
243
|
+
}
|
|
244
|
+
65%,
|
|
245
|
+
100% {
|
|
246
|
+
opacity: 0;
|
|
247
|
+
transform: scale(2.5);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
239
251
|
@keyframes rdx-page-enter {
|
|
240
252
|
from {
|
|
241
253
|
opacity: 0;
|