@lexibase/rdx-ui 2.0.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.
@@ -2,9 +2,14 @@
2
2
 
3
3
  ## shadcn/ui
4
4
 
5
- Table and Kbd are adapted from the official base-nova registry components:
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.
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.
8
13
 
9
14
  MIT License
10
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexibase/rdx-ui",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^22.18.0 || ^24.11.0"
@@ -23,14 +23,15 @@
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
32
  "next-themes": "^0.4.6",
33
33
  "postcss": "^8.5.31",
34
+ "shiki": "^4.5.0",
34
35
  "tailwind-merge": "^3.7.0",
35
36
  "tailwindcss": "^4.3.3",
36
37
  "tw-animate-css": "^1.4.0",
@@ -53,8 +54,8 @@
53
54
  "react-dom": "19.3.1",
54
55
  "tailwindcss": "^4.3.3",
55
56
  "typescript": "6.0.3",
56
- "@rdx/typescript-config": "1.0.0",
57
- "@rdx/eslint-config": "1.0.0"
57
+ "@rdx/eslint-config": "1.0.0",
58
+ "@rdx/typescript-config": "1.0.0"
58
59
  },
59
60
  "exports": {
60
61
  "./globals.css": {
@@ -65,7 +66,9 @@
65
66
  "./postcss-plugin": "./postcss-plugin.cjs",
66
67
  "./lib/*": "./src/lib/*.ts",
67
68
  "./components/*": "./src/components/*.tsx",
68
- "./providers/*": "./src/providers/*.tsx"
69
+ "./providers/*": "./src/providers/*.tsx",
70
+ "./lib/highlighter": "./src/lib/highlighter.tsx",
71
+ "./hooks/*": "./src/hooks/*.ts"
69
72
  },
70
73
  "scripts": {
71
74
  "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
- import { getMessages } from '../lib/i18n'
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
- /** Keep the code server-rendered; only clipboard interaction needs JavaScript. */
9
- export function CodeBlock({
10
- lang,
11
- 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({
12
29
  className,
30
+ variant = 'default',
31
+ size = 'default',
13
32
  ...props
14
- }: ComponentProps<'pre'>) {
15
- const messages = getMessages(lang)
16
- const code = Children.toArray(children).find(
17
- (child) =>
18
- isValidElement<{ className?: string }>(child) &&
19
- 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
+ />
20
56
  )
21
- const language = isValidElement<{ className?: string }>(code)
22
- ? code.props.className?.match(/(?:^|\s)language-([^\s]+)/)?.[1]
23
- : undefined
57
+ }
58
+
59
+ function CodeBlockTitle({ className, ...props }: React.ComponentProps<'div'>) {
24
60
  return (
25
61
  <div
26
- data-rdx-code-block
27
- className="my-4 min-w-0 max-w-full overflow-hidden rounded-lg border border-border bg-muted/30 text-foreground"
28
- >
29
- <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">
30
- <div className="flex min-w-0 items-center gap-2">
31
- <span
32
- aria-hidden="true"
33
- 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"
34
- >
35
- <CodeXml className="size-3.5" />
36
- </span>
37
- <span
38
- className="min-w-0 truncate font-mono text-xs font-medium text-muted-foreground"
39
- title={language}
40
- >
41
- {language ?? messages.code}
42
- </span>
43
- </div>
44
- <CopyCodeButton />
45
- </div>
46
- <pre
47
- tabIndex={0}
48
- className={cn(
49
- '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',
50
- className
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
- {children}
55
- </pre>
56
- </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
+ </>
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
- import { useMessages } from '../providers/locale-provider'
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'