@lexibase/rdx-ui 2.0.0 → 3.1.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/THIRD_PARTY_NOTICES.md +8 -2
- package/package.json +8 -3
- package/src/components/button.tsx +2 -0
- package/src/components/code-block.tsx +274 -46
- package/src/components/copy-code-button.tsx +1 -79
- package/src/components/dialog.tsx +123 -36
- package/src/components/docs-page-transition.tsx +178 -0
- package/src/components/docs-search-dialog.tsx +6 -1
- package/src/components/docs-sidebar.tsx +103 -30
- package/src/components/docs-toc.tsx +6 -6
- package/src/components/mdx-code-block-view.tsx +203 -0
- package/src/components/mdx-code-block.tsx +161 -0
- package/src/components/mdx-components.tsx +47 -16
- package/src/components/mdx-folder-tree.tsx +58 -0
- package/src/components/mdx-image.tsx +73 -0
- package/src/components/mdx-mermaid.tsx +252 -0
- package/src/components/sheet.tsx +146 -0
- package/src/components/tabs.tsx +12 -3
- package/src/hooks/use-highlighted-code.ts +75 -0
- package/src/lib/highlighter.tsx +124 -0
- package/src/lib/i18n.ts +32 -0
- package/src/lib/mermaid.ts +114 -0
- package/src/styles/globals.css +91 -14
- package/src/styles/shadcn.css +650 -0
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## shadcn/ui
|
|
4
4
|
|
|
5
|
-
Table
|
|
5
|
+
Table, Kbd and Sheet use the official base-nova registry components. Dialog, Code Block,
|
|
6
|
+
useHighlightedCode, and highlighter are installed in full from that registry,
|
|
7
|
+
with project imports, icons, localization and clipboard failure feedback wired
|
|
8
|
+
to RDX. The official shadcn 4.22.1 tailwind.css is included as styles/shadcn.css.
|
|
6
9
|
https://ui.shadcn.com/docs/components/base/table and
|
|
7
|
-
https://ui.shadcn.com/docs/components/base/kbd
|
|
10
|
+
https://ui.shadcn.com/docs/components/base/kbd and
|
|
11
|
+
https://ui.shadcn.com/docs/components/base/code-block and
|
|
12
|
+
https://ui.shadcn.com/docs/components/base/dialog and
|
|
13
|
+
https://ui.shadcn.com/docs/components/base/sheet.
|
|
8
14
|
|
|
9
15
|
MIT License
|
|
10
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexibase/rdx-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^22.18.0 || ^24.11.0"
|
|
@@ -23,14 +23,17 @@
|
|
|
23
23
|
"directory": "packages/rdx-ui"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"cmdk": "^1.1.1",
|
|
27
26
|
"@base-ui/react": "^1.9.0",
|
|
28
27
|
"@tailwindcss/postcss": "^4.3.3",
|
|
29
28
|
"class-variance-authority": "^0.7.1",
|
|
30
29
|
"clsx": "^2.1.1",
|
|
30
|
+
"cmdk": "^1.1.1",
|
|
31
31
|
"lucide-react": "^0.577.0",
|
|
32
|
+
"mermaid": "^12.2.0",
|
|
33
|
+
"motion": "^12.43.0",
|
|
32
34
|
"next-themes": "^0.4.6",
|
|
33
35
|
"postcss": "^8.5.31",
|
|
36
|
+
"shiki": "^4.5.0",
|
|
34
37
|
"tailwind-merge": "^3.7.0",
|
|
35
38
|
"tailwindcss": "^4.3.3",
|
|
36
39
|
"tw-animate-css": "^1.4.0",
|
|
@@ -65,7 +68,9 @@
|
|
|
65
68
|
"./postcss-plugin": "./postcss-plugin.cjs",
|
|
66
69
|
"./lib/*": "./src/lib/*.ts",
|
|
67
70
|
"./components/*": "./src/components/*.tsx",
|
|
68
|
-
"./providers/*": "./src/providers/*.tsx"
|
|
71
|
+
"./providers/*": "./src/providers/*.tsx",
|
|
72
|
+
"./lib/highlighter": "./src/lib/highlighter.tsx",
|
|
73
|
+
"./hooks/*": "./src/hooks/*.ts"
|
|
69
74
|
},
|
|
70
75
|
"scripts": {
|
|
71
76
|
"lint": "eslint . --max-warnings 0",
|
|
@@ -31,6 +31,8 @@ const buttonVariants = cva(
|
|
|
31
31
|
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
|
32
32
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
33
33
|
icon: 'size-9',
|
|
34
|
+
'icon-sm': 'size-8',
|
|
35
|
+
'icon-xs': 'size-6 rounded-sm [&_svg:not([class*=size-])]:size-3',
|
|
34
36
|
link: 'inline-block',
|
|
35
37
|
},
|
|
36
38
|
active: {
|
|
@@ -1,58 +1,286 @@
|
|
|
1
|
-
|
|
2
|
-
import { Children, isValidElement, type ComponentProps } from 'react'
|
|
3
|
-
import { CodeXml } from 'lucide-react'
|
|
1
|
+
'use client'
|
|
4
2
|
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
5
5
|
import { cn } from '../lib/utils'
|
|
6
|
-
import { CopyCodeButton } from './copy-code-button'
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import { Button } from './button'
|
|
8
|
+
import { CheckIcon, CopyIcon } from 'lucide-react'
|
|
9
|
+
import { useMessages } from '../providers/locale-provider'
|
|
10
|
+
|
|
11
|
+
const codeBlockVariants = cva(
|
|
12
|
+
'group/code-block relative flex w-full min-w-0 flex-col overflow-hidden rounded-lg text-sm has-[>[data-slot=code-block-content]:focus-visible]:ring-3 has-[>[data-slot=code-block-content]:focus-visible]:ring-ring/50 data-[size=sm]:text-xs',
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
variant: {
|
|
16
|
+
default:
|
|
17
|
+
'border border-input text-foreground has-[>[data-slot=code-block-content]:focus-visible]:border-ring dark:bg-input/30',
|
|
18
|
+
destructive:
|
|
19
|
+
'border border-destructive/30 text-destructive dark:border-destructive/40',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: 'default',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
function CodeBlock({
|
|
12
29
|
className,
|
|
30
|
+
variant = 'default',
|
|
31
|
+
size = 'default',
|
|
13
32
|
...props
|
|
14
|
-
}: ComponentProps<'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
}: React.ComponentProps<'div'> &
|
|
34
|
+
VariantProps<typeof codeBlockVariants> & { size?: 'default' | 'sm' }) {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
data-slot="code-block"
|
|
38
|
+
data-variant={variant}
|
|
39
|
+
data-size={size}
|
|
40
|
+
className={cn(codeBlockVariants({ variant }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function CodeBlockHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-slot="code-block-header"
|
|
50
|
+
className={cn(
|
|
51
|
+
'flex min-w-0 shrink-0 items-center gap-2 px-3 pt-3 text-muted-foreground group-data-[size=sm]/code-block:px-2.5 group-data-[size=sm]/code-block:pt-2.5 group-data-[variant=destructive]/code-block:text-destructive *:has-[>[data-slot=code-block-title]]:min-w-0',
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
20
56
|
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CodeBlockTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
24
60
|
return (
|
|
25
61
|
<div
|
|
26
|
-
data-
|
|
27
|
-
className=
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
data-slot="code-block-title"
|
|
63
|
+
className={cn(
|
|
64
|
+
"min-w-0 truncate font-sans text-sm font-medium text-foreground group-data-[size=sm]/code-block:text-xs group-data-[variant=destructive]/code-block:text-destructive [&_svg]:pointer-events-none [&_svg]:inline-block [&_svg]:align-middle [&_svg:not([class*='size-'])]:size-3.5 *:[svg]:me-1.5",
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function CodeBlockActions({
|
|
73
|
+
className,
|
|
74
|
+
...props
|
|
75
|
+
}: React.ComponentProps<'div'>) {
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
data-slot="code-block-actions"
|
|
79
|
+
className={cn(
|
|
80
|
+
'-my-1 ms-auto -me-1.5 flex shrink-0 items-center gap-1 [[data-slot=code-block]>&]:absolute [[data-slot=code-block]>&]:end-2 [[data-slot=code-block]>&]:top-2 [[data-slot=code-block]>&]:z-10 [[data-slot=code-block]>&]:m-0 [[data-slot=code-block]>&]:opacity-0 [[data-slot=code-block]>&]:transition-opacity [[data-slot=code-block]>&]:group-focus-within/code-block:opacity-100 [[data-slot=code-block]>&]:group-hover/code-block:opacity-100 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:end-1.5 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:top-1.5 [[data-slot=code-block]>&]:pointer-coarse:opacity-100 [[data-slot=code-block]>&]:*:[button]:bg-background [[data-slot=code-block]>&]:*:[button]:ring-1 [[data-slot=code-block]>&]:*:[button]:ring-foreground/10',
|
|
81
|
+
className
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Locale and failure feedback are RDX integration; the shadcn composition/API stays intact.
|
|
89
|
+
function CodeBlockCopyButton({
|
|
90
|
+
value,
|
|
91
|
+
variant = 'ghost',
|
|
92
|
+
size = 'icon-xs',
|
|
93
|
+
className,
|
|
94
|
+
onClick,
|
|
95
|
+
...props
|
|
96
|
+
}: Omit<
|
|
97
|
+
Extract<React.ComponentProps<typeof Button>, { href?: never }>,
|
|
98
|
+
'value'
|
|
99
|
+
> & { value?: string }) {
|
|
100
|
+
const messages = useMessages()
|
|
101
|
+
const [copied, setCopied] = React.useState(false)
|
|
102
|
+
const [error, setError] = React.useState(false)
|
|
103
|
+
const pending = React.useRef(false)
|
|
104
|
+
const mounted = React.useRef(false)
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
mounted.current = true
|
|
107
|
+
return () => {
|
|
108
|
+
mounted.current = false
|
|
109
|
+
}
|
|
110
|
+
}, [])
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
if (!copied) return
|
|
113
|
+
const timeout = setTimeout(() => setCopied(false), 2000)
|
|
114
|
+
return () => clearTimeout(timeout)
|
|
115
|
+
}, [copied])
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
<Button
|
|
119
|
+
data-slot="code-block-copy-button"
|
|
120
|
+
data-copied={copied || undefined}
|
|
121
|
+
data-status={error ? 'error' : copied ? 'copied' : 'idle'}
|
|
122
|
+
variant={variant}
|
|
123
|
+
size={size}
|
|
124
|
+
aria-label={messages.copyCode}
|
|
125
|
+
title={
|
|
126
|
+
error
|
|
127
|
+
? messages.copyRetry
|
|
128
|
+
: copied
|
|
129
|
+
? messages.copied
|
|
130
|
+
: messages.copyCode
|
|
131
|
+
}
|
|
132
|
+
className={className}
|
|
133
|
+
onClick={async (event) => {
|
|
134
|
+
onClick?.(event)
|
|
135
|
+
if (event.defaultPrevented || pending.current) return
|
|
136
|
+
pending.current = true
|
|
137
|
+
setError(false)
|
|
138
|
+
try {
|
|
139
|
+
// The fallback retains the published standalone CopyCodeButton API.
|
|
140
|
+
const block = event.currentTarget.closest<HTMLElement>(
|
|
141
|
+
'[data-rdx-code-block]'
|
|
142
|
+
)
|
|
143
|
+
const text =
|
|
144
|
+
value ??
|
|
145
|
+
block?.dataset.rdxCodeValue ??
|
|
146
|
+
block?.querySelector('pre')?.textContent
|
|
147
|
+
if (text === undefined || !navigator.clipboard?.writeText)
|
|
148
|
+
throw new Error('Clipboard unavailable')
|
|
149
|
+
await navigator.clipboard.writeText(text)
|
|
150
|
+
if (mounted.current) setCopied(true)
|
|
151
|
+
} catch {
|
|
152
|
+
if (mounted.current) {
|
|
153
|
+
setCopied(false)
|
|
154
|
+
setError(true)
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
pending.current = false
|
|
158
|
+
}
|
|
159
|
+
}}
|
|
52
160
|
{...props}
|
|
53
161
|
>
|
|
54
|
-
{
|
|
55
|
-
</
|
|
56
|
-
|
|
162
|
+
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
163
|
+
</Button>
|
|
164
|
+
<span
|
|
165
|
+
role="status"
|
|
166
|
+
aria-live="polite"
|
|
167
|
+
aria-atomic="true"
|
|
168
|
+
className={error ? 'max-w-48 text-xs text-muted-foreground' : 'sr-only'}
|
|
169
|
+
>
|
|
170
|
+
{error ? messages.copyCodeError : copied ? messages.codeCopied : ''}
|
|
171
|
+
</span>
|
|
172
|
+
</>
|
|
57
173
|
)
|
|
58
174
|
}
|
|
175
|
+
|
|
176
|
+
function CodeBlockContent({
|
|
177
|
+
className,
|
|
178
|
+
children,
|
|
179
|
+
style,
|
|
180
|
+
wrap = false,
|
|
181
|
+
lineNumbers = false,
|
|
182
|
+
startLine = 1,
|
|
183
|
+
...props
|
|
184
|
+
}: React.ComponentProps<'pre'> & {
|
|
185
|
+
wrap?: boolean
|
|
186
|
+
lineNumbers?: boolean
|
|
187
|
+
startLine?: number
|
|
188
|
+
}) {
|
|
189
|
+
return (
|
|
190
|
+
<pre
|
|
191
|
+
data-slot="code-block-content"
|
|
192
|
+
data-wrap={wrap || undefined}
|
|
193
|
+
data-numbered={lineNumbers || undefined}
|
|
194
|
+
dir="ltr"
|
|
195
|
+
tabIndex={0}
|
|
196
|
+
style={{ counterReset: `line ${startLine - 1}`, ...style }}
|
|
197
|
+
className={cn(
|
|
198
|
+
'min-h-0 min-w-0 scroll-fade-x scrollbar-thin overflow-auto px-3 py-3 text-start font-mono leading-relaxed outline-none group-has-data-[slot=code-block-footer]/code-block:pb-2 group-has-data-[slot=code-block-header]/code-block:pt-2 group-data-[size=sm]/code-block:px-2.5 group-data-[size=sm]/code-block:py-2.5 group-data-[size=sm]/code-block:group-has-data-[slot=code-block-header]/code-block:pt-1.5 data-wrap:scroll-fade-y data-wrap:wrap-break-word data-wrap:whitespace-pre-wrap',
|
|
199
|
+
className
|
|
200
|
+
)}
|
|
201
|
+
{...props}
|
|
202
|
+
>
|
|
203
|
+
<code
|
|
204
|
+
data-slot="code-block-code"
|
|
205
|
+
className="grid min-h-lh min-w-max content-start in-data-numbered:grid-cols-[auto_1fr] in-data-wrap:min-w-0"
|
|
206
|
+
>
|
|
207
|
+
{lineNumbers && typeof children === 'string'
|
|
208
|
+
? children
|
|
209
|
+
.replace(/\n$/, '')
|
|
210
|
+
.split('\n')
|
|
211
|
+
.map((line, index) => (
|
|
212
|
+
<CodeBlockLine key={index}>{line}</CodeBlockLine>
|
|
213
|
+
))
|
|
214
|
+
: children}
|
|
215
|
+
</code>
|
|
216
|
+
</pre>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const codeBlockLineVariants = cva(
|
|
221
|
+
"relative -mx-3 block min-h-lh px-3 group-data-[size=sm]/code-block:-mx-2.5 group-data-[size=sm]/code-block:px-2.5 before:min-w-6 before:text-end before:text-muted-foreground after:absolute after:start-1 after:top-0 after:text-muted-foreground after:select-none in-data-numbered:col-span-2 in-data-numbered:grid in-data-numbered:grid-cols-subgrid in-data-numbered:gap-x-3 in-data-numbered:[counter-increment:line] in-data-numbered:before:content-[counter(line)] in-data-numbered:before:select-none in-data-numbered:supports-[content:counter(line)_/_'']:before:content-[counter(line)_/_'']",
|
|
222
|
+
{
|
|
223
|
+
variants: {
|
|
224
|
+
variant: {
|
|
225
|
+
default: '',
|
|
226
|
+
added: "bg-muted after:content-['+']",
|
|
227
|
+
removed: "bg-destructive/10 after:text-destructive after:content-['−']",
|
|
228
|
+
highlighted: 'bg-muted',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
defaultVariants: {
|
|
232
|
+
variant: 'default',
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
function CodeBlockLine({
|
|
238
|
+
className,
|
|
239
|
+
variant = 'default',
|
|
240
|
+
children,
|
|
241
|
+
...props
|
|
242
|
+
}: React.ComponentProps<'span'> & VariantProps<typeof codeBlockLineVariants>) {
|
|
243
|
+
// An empty line holds a newline so copying a selection keeps blank lines.
|
|
244
|
+
const empty =
|
|
245
|
+
children === undefined ||
|
|
246
|
+
children === null ||
|
|
247
|
+
children === '' ||
|
|
248
|
+
(Array.isArray(children) && children.length === 0)
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<span
|
|
252
|
+
data-slot="code-block-line"
|
|
253
|
+
data-variant={variant}
|
|
254
|
+
className={cn(codeBlockLineVariants({ variant }), className)}
|
|
255
|
+
{...props}
|
|
256
|
+
>
|
|
257
|
+
<span data-slot="code-block-line-content" className="min-w-0">
|
|
258
|
+
{empty ? '\n' : children}
|
|
259
|
+
</span>
|
|
260
|
+
</span>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function CodeBlockFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
265
|
+
return (
|
|
266
|
+
<div
|
|
267
|
+
data-slot="code-block-footer"
|
|
268
|
+
className={cn(
|
|
269
|
+
'flex min-w-0 shrink-0 items-center gap-2 px-3 pb-3 text-muted-foreground group-data-[size=sm]/code-block:px-2.5 group-data-[size=sm]/code-block:pb-2.5',
|
|
270
|
+
className
|
|
271
|
+
)}
|
|
272
|
+
{...props}
|
|
273
|
+
/>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export {
|
|
278
|
+
CodeBlock,
|
|
279
|
+
CodeBlockHeader,
|
|
280
|
+
CodeBlockTitle,
|
|
281
|
+
CodeBlockActions,
|
|
282
|
+
CodeBlockCopyButton,
|
|
283
|
+
CodeBlockContent,
|
|
284
|
+
CodeBlockLine,
|
|
285
|
+
CodeBlockFooter,
|
|
286
|
+
}
|
|
@@ -1,81 +1,3 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { useEffect, useRef, useState } from 'react'
|
|
6
|
-
import { Check, Copy } from 'lucide-react'
|
|
7
|
-
|
|
8
|
-
import { Button } from './button'
|
|
9
|
-
|
|
10
|
-
export function CopyCodeButton() {
|
|
11
|
-
const messages = useMessages()
|
|
12
|
-
const [status, setStatus] = useState<'idle' | 'copied' | 'error'>('idle')
|
|
13
|
-
const pending = useRef(false)
|
|
14
|
-
const mounted = useRef(false)
|
|
15
|
-
const reset = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
mounted.current = true
|
|
18
|
-
return () => {
|
|
19
|
-
mounted.current = false
|
|
20
|
-
clearTimeout(reset.current)
|
|
21
|
-
}
|
|
22
|
-
}, [])
|
|
23
|
-
|
|
24
|
-
async function copy(button: HTMLButtonElement) {
|
|
25
|
-
if (pending.current) return
|
|
26
|
-
pending.current = true
|
|
27
|
-
clearTimeout(reset.current)
|
|
28
|
-
try {
|
|
29
|
-
const code = button.closest('[data-rdx-code-block]')?.querySelector('pre')
|
|
30
|
-
if (!code || !navigator.clipboard?.writeText)
|
|
31
|
-
throw new Error('Clipboard unavailable')
|
|
32
|
-
await navigator.clipboard.writeText(code.textContent ?? '')
|
|
33
|
-
if (mounted.current) {
|
|
34
|
-
setStatus('copied')
|
|
35
|
-
reset.current = setTimeout(() => setStatus('idle'), 2000)
|
|
36
|
-
}
|
|
37
|
-
} catch {
|
|
38
|
-
if (mounted.current) setStatus('error')
|
|
39
|
-
} finally {
|
|
40
|
-
pending.current = false
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const Icon = status === 'copied' ? Check : Copy
|
|
45
|
-
return (
|
|
46
|
-
<div className="flex min-w-0 items-center justify-end gap-2">
|
|
47
|
-
<span
|
|
48
|
-
role="status"
|
|
49
|
-
aria-live="polite"
|
|
50
|
-
aria-atomic="true"
|
|
51
|
-
className={
|
|
52
|
-
status === 'error' ? 'text-xs text-muted-foreground' : 'sr-only'
|
|
53
|
-
}
|
|
54
|
-
>
|
|
55
|
-
{status === 'copied'
|
|
56
|
-
? messages.codeCopied
|
|
57
|
-
: status === 'error'
|
|
58
|
-
? messages.copyCodeError
|
|
59
|
-
: ''}
|
|
60
|
-
</span>
|
|
61
|
-
<Button
|
|
62
|
-
type="button"
|
|
63
|
-
variant="outline"
|
|
64
|
-
size="icon"
|
|
65
|
-
data-status={status}
|
|
66
|
-
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"
|
|
67
|
-
aria-label={messages.copyCode}
|
|
68
|
-
title={
|
|
69
|
-
status === 'copied'
|
|
70
|
-
? messages.copied
|
|
71
|
-
: status === 'error'
|
|
72
|
-
? messages.copyRetry
|
|
73
|
-
: messages.copyCode
|
|
74
|
-
}
|
|
75
|
-
onClick={(event) => copy(event.currentTarget)}
|
|
76
|
-
>
|
|
77
|
-
<Icon aria-hidden="true" className="size-4" />
|
|
78
|
-
</Button>
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
3
|
+
export { CodeBlockCopyButton as CopyCodeButton } from './code-block'
|