@lexibase/rdx-ui 1.1.0 → 3.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 CHANGED
@@ -45,11 +45,10 @@ manual-copy guidance instead of a false success message.
45
45
 
46
46
  The shared components use `@base-ui/react` and the RDX theme.
47
47
  `components.json` selects shadcn/ui's `base-nova` style.
48
- Use `render` for interactive composition and `ButtonLink` for navigation:
48
+ Use `render` for interactive composition and `Button` with `href` for navigation:
49
49
 
50
50
  ```tsx
51
51
  import { Button } from '@lexibase/rdx-ui/components/button'
52
- import { ButtonLink } from '@lexibase/rdx-ui/components/button-link'
53
52
  import {
54
53
  DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
55
54
  DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem,
@@ -57,7 +56,7 @@ import {
57
56
  } from '@lexibase/rdx-ui/components/dropdown-menu'
58
57
  import Link from 'next/link'
59
58
 
60
- <ButtonLink href="/docs" variant="outline">Documentation</ButtonLink>
59
+ <Button href="/docs" variant="link">Documentation</Button>
61
60
 
62
61
  <DropdownMenu>
63
62
  <DropdownMenuTrigger render={<Button variant="outline" />}>
@@ -75,8 +74,8 @@ import Link from 'next/link'
75
74
  </DropdownMenu>
76
75
  ```
77
76
 
78
- `ButtonLink` renders a Next.js link with button styling, not `role="button"`.
79
- `Button` renders a native button; use `type="submit"` explicitly for form submission.
77
+ `Button` with `href` renders a native Next.js link; use `variant="link"` for text links.
78
+ Without `href`, `Button` renders a native button; use `type="submit"` explicitly for form submission.
80
79
  Menu actions use `onClick`. Labels must be inside `DropdownMenuGroup`.
81
80
  Checkbox values are boolean; checkbox/radio items remain open unless
82
81
  `closeOnClick` is enabled.
@@ -0,0 +1,34 @@
1
+ # Third-party notices
2
+
3
+ ## shadcn/ui
4
+
5
+ Table and Kbd 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.
9
+ https://ui.shadcn.com/docs/components/base/table and
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.
13
+
14
+ MIT License
15
+
16
+ Copyright (c) 2023 shadcn
17
+
18
+ Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ of this software and associated documentation files (the "Software"), to deal
20
+ in the Software without restriction, including without limitation the rights
21
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+ copies of the Software, and to permit persons to whom the Software is
23
+ furnished to do so, subject to the following conditions:
24
+
25
+ The above copyright notice and this permission notice shall be included in all
26
+ copies or substantial portions of the Software.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexibase/rdx-ui",
3
- "version": "1.1.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^22.18.0 || ^24.11.0"
@@ -8,6 +8,7 @@
8
8
  "description": "The shared RDX documentation theme and UI components.",
9
9
  "license": "MIT",
10
10
  "files": [
11
+ "THIRD_PARTY_NOTICES.md",
11
12
  "src",
12
13
  "postcss.config.mjs",
13
14
  "postcss-plugin.cjs",
@@ -22,18 +23,19 @@
22
23
  "directory": "packages/rdx-ui"
23
24
  },
24
25
  "dependencies": {
25
- "cmdk": "^1.1.1",
26
26
  "@base-ui/react": "^1.9.0",
27
27
  "@tailwindcss/postcss": "^4.3.3",
28
28
  "class-variance-authority": "^0.7.1",
29
29
  "clsx": "^2.1.1",
30
+ "cmdk": "^1.1.1",
30
31
  "lucide-react": "^0.577.0",
31
32
  "next-themes": "^0.4.6",
32
33
  "postcss": "^8.5.31",
34
+ "shiki": "^4.5.0",
33
35
  "tailwind-merge": "^3.7.0",
34
36
  "tailwindcss": "^4.3.3",
35
37
  "tw-animate-css": "^1.4.0",
36
- "@lexibase/rdx-types": "1.1.0"
38
+ "@lexibase/rdx-types": "1.1.1"
37
39
  },
38
40
  "peerDependencies": {
39
41
  "next": "^16.4.2",
@@ -52,8 +54,8 @@
52
54
  "react-dom": "19.3.1",
53
55
  "tailwindcss": "^4.3.3",
54
56
  "typescript": "6.0.3",
55
- "@rdx/typescript-config": "1.0.0",
56
- "@rdx/eslint-config": "1.0.0"
57
+ "@rdx/eslint-config": "1.0.0",
58
+ "@rdx/typescript-config": "1.0.0"
57
59
  },
58
60
  "exports": {
59
61
  "./globals.css": {
@@ -64,7 +66,9 @@
64
66
  "./postcss-plugin": "./postcss-plugin.cjs",
65
67
  "./lib/*": "./src/lib/*.ts",
66
68
  "./components/*": "./src/components/*.tsx",
67
- "./providers/*": "./src/providers/*.tsx"
69
+ "./providers/*": "./src/providers/*.tsx",
70
+ "./lib/highlighter": "./src/lib/highlighter.tsx",
71
+ "./hooks/*": "./src/hooks/*.ts"
68
72
  },
69
73
  "scripts": {
70
74
  "lint": "eslint . --max-warnings 0",
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { Button as ButtonPrimitive } from '@base-ui/react/button'
4
4
  import { cva, type VariantProps } from 'class-variance-authority'
5
+ import Link from 'next/link'
6
+ import type { ComponentProps } from 'react'
5
7
 
6
8
  import { cn } from '@lexibase/rdx-ui/lib/utils'
7
9
 
@@ -29,6 +31,8 @@ const buttonVariants = cva(
29
31
  sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
30
32
  lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
31
33
  icon: 'size-9',
34
+ 'icon-sm': 'size-8',
35
+ 'icon-xs': 'size-6 rounded-sm [&_svg:not([class*=size-])]:size-3',
32
36
  link: 'inline-block',
33
37
  },
34
38
  active: {
@@ -43,13 +47,23 @@ const buttonVariants = cva(
43
47
  }
44
48
  )
45
49
 
46
- function Button({
47
- className,
48
- variant,
49
- size,
50
- active,
51
- ...props
52
- }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
50
+ type ButtonProps =
51
+ | (ButtonPrimitive.Props &
52
+ VariantProps<typeof buttonVariants> & { href?: never })
53
+ | (ComponentProps<typeof Link> & VariantProps<typeof buttonVariants>)
54
+
55
+ function Button(props: ButtonProps) {
56
+ if (props.href !== undefined) {
57
+ const { className, variant = 'link', size, active, ...linkProps } = props
58
+ return (
59
+ <Link
60
+ data-slot="button"
61
+ className={cn(buttonVariants({ variant, size, active }), className)}
62
+ {...linkProps}
63
+ />
64
+ )
65
+ }
66
+ const { className, variant, size, active, ...buttonProps } = props
53
67
  return (
54
68
  <ButtonPrimitive
55
69
  data-slot="button"
@@ -59,7 +73,7 @@ function Button({
59
73
  typeof className === 'function' ? className(state) : className
60
74
  )
61
75
  }
62
- {...props}
76
+ {...buttonProps}
63
77
  />
64
78
  )
65
79
  }
@@ -1,55 +1,286 @@
1
- import { Children, isValidElement, type ComponentProps } from 'react'
2
- import { CodeXml } from 'lucide-react'
1
+ 'use client'
3
2
 
3
+ import * as React from 'react'
4
+ import { cva, type VariantProps } from 'class-variance-authority'
4
5
  import { cn } from '../lib/utils'
5
- import { CopyCodeButton } from './copy-code-button'
6
6
 
7
- /** Keep the code server-rendered; only clipboard interaction needs JavaScript. */
8
- export function CodeBlock({
9
- children,
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({
10
29
  className,
30
+ variant = 'default',
31
+ size = 'default',
11
32
  ...props
12
- }: ComponentProps<'pre'>) {
13
- const code = Children.toArray(children).find(
14
- (child) =>
15
- isValidElement<{ className?: string }>(child) &&
16
- child.props.className?.includes('language-')
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
+ />
17
56
  )
18
- const language = isValidElement<{ className?: string }>(code)
19
- ? code.props.className?.match(/(?:^|\s)language-([^\s]+)/)?.[1]
20
- : undefined
57
+ }
58
+
59
+ function CodeBlockTitle({ className, ...props }: React.ComponentProps<'div'>) {
21
60
  return (
22
61
  <div
23
- data-rdx-code-block
24
- className="my-4 min-w-0 max-w-full overflow-hidden rounded-lg border border-border bg-muted/30 text-foreground"
25
- >
26
- <div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5 border-b border-border/70 bg-muted/30 px-3 py-1.5">
27
- <div className="flex min-w-0 items-center gap-2">
28
- <span
29
- aria-hidden="true"
30
- className="flex size-6 shrink-0 items-center justify-center rounded-md border border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-400"
31
- >
32
- <CodeXml className="size-3.5" />
33
- </span>
34
- <span
35
- className="min-w-0 truncate font-mono text-xs font-medium text-muted-foreground"
36
- title={language}
37
- >
38
- {language ?? 'Code'}
39
- </span>
40
- </div>
41
- <CopyCodeButton />
42
- </div>
43
- <pre
44
- tabIndex={0}
45
- className={cn(
46
- 'm-0 max-w-full overflow-x-auto p-3 font-mono text-[13px] leading-6 text-foreground/90 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring [&>code]:bg-transparent [&>code]:rounded-none [&>code]:p-0 [&>code]:[font-family:inherit] [&>code]:text-inherit',
47
- className
48
- )}
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
+ }}
49
160
  {...props}
50
161
  >
51
- {children}
52
- </pre>
53
- </div>
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
+ </>
54
173
  )
55
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,78 +1,3 @@
1
1
  'use client'
2
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
- }
3
+ export { CodeBlockCopyButton as CopyCodeButton } from './code-block'