@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 +4 -5
- package/THIRD_PARTY_NOTICES.md +34 -0
- package/package.json +10 -6
- package/src/components/button.tsx +22 -8
- package/src/components/code-block.tsx +274 -43
- package/src/components/copy-code-button.tsx +1 -76
- package/src/components/dialog.tsx +124 -34
- package/src/components/docs-search-dialog.tsx +29 -14
- package/src/components/docs-search.tsx +9 -5
- package/src/components/docs-sidebar.tsx +9 -7
- package/src/components/docs-toc.tsx +6 -3
- package/src/components/footer-credit.tsx +8 -4
- package/src/components/home.tsx +3 -3
- package/src/components/kbd.tsx +29 -0
- package/src/components/loading-button.tsx +5 -1
- package/src/components/mdx-code-block-view.tsx +77 -0
- package/src/components/mdx-code-block.tsx +108 -0
- package/src/components/mdx-components.tsx +105 -47
- package/src/components/mdx-image.tsx +73 -0
- package/src/components/mode-toggle.tsx +5 -2
- package/src/components/navbar.tsx +13 -9
- package/src/components/not-found.tsx +36 -0
- package/src/components/table.tsx +122 -0
- package/src/components/tabs.tsx +17 -9
- package/src/components/version-dropdown.tsx +9 -2
- package/src/components/version-notice.tsx +7 -5
- package/src/hooks/use-highlighted-code.ts +75 -0
- package/src/lib/highlighter.tsx +124 -0
- package/src/lib/i18n.ts +156 -0
- package/src/providers/locale-provider.tsx +29 -0
- package/src/styles/globals.css +1 -0
- package/src/styles/shadcn.css +650 -0
- package/src/components/button-link.tsx +0 -27
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 `
|
|
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
|
-
<
|
|
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
|
-
`
|
|
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": "
|
|
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.
|
|
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/
|
|
56
|
-
"@rdx/
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
{...
|
|
76
|
+
{...buttonProps}
|
|
63
77
|
/>
|
|
64
78
|
)
|
|
65
79
|
}
|
|
@@ -1,55 +1,286 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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<'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CodeBlockTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
21
60
|
return (
|
|
22
61
|
<div
|
|
23
|
-
data-
|
|
24
|
-
className=
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
{
|
|
52
|
-
</
|
|
53
|
-
|
|
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
|
-
|
|
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'
|