@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.
@@ -1,76 +1,166 @@
1
1
  'use client'
2
2
 
3
- // shadcn/ui Base Nova Dialog, using the shared RDX theme and buttons.
4
3
  import * as React from 'react'
5
4
  import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
6
- import { X } from 'lucide-react'
7
- import { mergeStateClassName } from '../lib/utils'
5
+ import { cn, mergeStateClassName } from '../lib/utils'
6
+
8
7
  import { Button } from './button'
8
+ import { XIcon } from 'lucide-react'
9
+ import { useMessages } from '../providers/locale-provider'
9
10
 
10
- function Dialog(props: React.ComponentProps<typeof DialogPrimitive.Root>) {
11
- return <DialogPrimitive.Root {...props} />
11
+ function Dialog({ ...props }: DialogPrimitive.Root.Props) {
12
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
12
13
  }
13
- function DialogTrigger(
14
- props: React.ComponentProps<typeof DialogPrimitive.Trigger>
15
- ) {
14
+
15
+ function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
16
16
  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
17
17
  }
18
+
19
+ function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
20
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
21
+ }
22
+
23
+ function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
24
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
25
+ }
26
+
27
+ function DialogOverlay({
28
+ className,
29
+ ...props
30
+ }: DialogPrimitive.Backdrop.Props) {
31
+ return (
32
+ <DialogPrimitive.Backdrop
33
+ data-slot="dialog-overlay"
34
+ className={mergeStateClassName(
35
+ 'fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 motion-reduce:data-open:animate-none motion-reduce:data-closed:animate-none',
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
18
43
  function DialogContent({
19
44
  className,
20
45
  children,
46
+ showCloseButton = true,
47
+ closeLabel,
48
+ overlayClassName,
21
49
  ...props
22
- }: React.ComponentProps<typeof DialogPrimitive.Popup>) {
50
+ }: DialogPrimitive.Popup.Props & {
51
+ showCloseButton?: boolean
52
+ closeLabel?: string
53
+ overlayClassName?: DialogPrimitive.Backdrop.Props['className']
54
+ }) {
55
+ const messages = useMessages()
23
56
  return (
24
- <DialogPrimitive.Portal>
25
- <DialogPrimitive.Backdrop
26
- data-slot="dialog-overlay"
27
- className="fixed inset-0 isolate z-50 bg-black/30 backdrop-blur-xs duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 motion-reduce:data-open:animate-none motion-reduce:data-closed:animate-none"
28
- />
57
+ <DialogPortal>
58
+ <DialogOverlay className={overlayClassName} />
29
59
  <DialogPrimitive.Popup
30
60
  data-slot="dialog-content"
31
61
  className={mergeStateClassName(
32
- 'fixed top-[20%] left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 gap-4 rounded-xl bg-popover p-1 text-sm text-popover-foreground ring-1 ring-foreground/10 shadow-lg outline-none duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 motion-reduce:data-open:animate-none motion-reduce:data-closed:animate-none',
62
+ 'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 motion-reduce:data-open:animate-none motion-reduce:data-closed:animate-none',
33
63
  className
34
64
  )}
35
65
  {...props}
36
66
  >
37
67
  {children}
38
- <DialogPrimitive.Close
39
- render={
40
- <Button
41
- variant="ghost"
42
- size="icon"
43
- className="absolute top-2 right-2 size-7"
44
- />
45
- }
46
- aria-label="Close search"
47
- >
48
- <X className="size-3.5" />
49
- </DialogPrimitive.Close>
68
+ {showCloseButton && (
69
+ <DialogPrimitive.Close
70
+ data-slot="dialog-close"
71
+ render={
72
+ <Button
73
+ variant="ghost"
74
+ className="absolute top-2 right-2"
75
+ size="icon-sm"
76
+ />
77
+ }
78
+ >
79
+ <XIcon />
80
+ <span className="sr-only">{closeLabel ?? messages.close}</span>
81
+ </DialogPrimitive.Close>
82
+ )}
50
83
  </DialogPrimitive.Popup>
51
- </DialogPrimitive.Portal>
84
+ </DialogPortal>
52
85
  )
53
86
  }
54
- function DialogTitle({
87
+
88
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
89
+ return (
90
+ <div
91
+ data-slot="dialog-header"
92
+ className={cn('flex flex-col gap-2', className)}
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ function DialogFooter({
55
99
  className,
100
+ showCloseButton = false,
101
+ children,
56
102
  ...props
57
- }: React.ComponentProps<typeof DialogPrimitive.Title>) {
103
+ }: React.ComponentProps<'div'> & {
104
+ showCloseButton?: boolean
105
+ }) {
106
+ const messages = useMessages()
107
+ return (
108
+ <div
109
+ data-slot="dialog-footer"
110
+ className={cn(
111
+ '-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end',
112
+ className
113
+ )}
114
+ {...props}
115
+ >
116
+ {children}
117
+ {showCloseButton && (
118
+ <DialogPrimitive.Close render={<Button variant="outline" />}>
119
+ {messages.close}
120
+ </DialogPrimitive.Close>
121
+ )}
122
+ </div>
123
+ )
124
+ }
125
+
126
+ function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
58
127
  return (
59
128
  <DialogPrimitive.Title
60
- className={mergeStateClassName('font-medium', className)}
129
+ data-slot="dialog-title"
130
+ className={mergeStateClassName(
131
+ 'cn-font-heading text-base leading-none font-medium',
132
+ className
133
+ )}
61
134
  {...props}
62
135
  />
63
136
  )
64
137
  }
138
+
65
139
  function DialogDescription({
66
140
  className,
67
141
  ...props
68
- }: React.ComponentProps<typeof DialogPrimitive.Description>) {
142
+ }: DialogPrimitive.Description.Props) {
69
143
  return (
70
144
  <DialogPrimitive.Description
71
- className={mergeStateClassName('text-muted-foreground', className)}
145
+ data-slot="dialog-description"
146
+ className={mergeStateClassName(
147
+ 'text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
148
+ className
149
+ )}
72
150
  {...props}
73
151
  />
74
152
  )
75
153
  }
76
- export { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription }
154
+
155
+ export {
156
+ Dialog,
157
+ DialogClose,
158
+ DialogContent,
159
+ DialogDescription,
160
+ DialogFooter,
161
+ DialogHeader,
162
+ DialogOverlay,
163
+ DialogPortal,
164
+ DialogTitle,
165
+ DialogTrigger,
166
+ }
@@ -1,5 +1,7 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import {
4
6
  useDeferredValue,
5
7
  useEffect,
@@ -23,6 +25,8 @@ import {
23
25
  CommandItem,
24
26
  CommandList,
25
27
  } from './command'
28
+ import { formatMessage } from '../lib/i18n'
29
+
26
30
  import { Button } from './button'
27
31
 
28
32
  const cache = new Map<string, PreparedSearch>()
@@ -40,6 +44,7 @@ export function DocsSearchDialog({
40
44
  onOpenChange: (value: boolean) => void
41
45
  triggerRef: RefObject<HTMLButtonElement | null>
42
46
  }) {
47
+ const messages = useMessages()
43
48
  const router = useRouter()
44
49
  const pathname = usePathname()
45
50
  const [query, setQuery] = useState('')
@@ -94,16 +99,22 @@ export function DocsSearchDialog({
94
99
  }
95
100
  return (
96
101
  <Dialog open={open} onOpenChange={onOpenChange}>
97
- <DialogContent finalFocus={triggerRef}>
98
- <DialogTitle className="sr-only">Search documentation</DialogTitle>
102
+ <DialogContent
103
+ finalFocus={triggerRef}
104
+ closeLabel={messages.closeSearch}
105
+ overlayClassName="bg-black/30"
106
+ className="top-[20%] w-[calc(100%-2rem)] max-w-lg translate-y-0 p-1 shadow-lg sm:max-w-lg"
107
+ >
108
+ <DialogTitle className="sr-only">
109
+ {messages.searchDocumentation}
110
+ </DialogTitle>
99
111
  <DialogDescription className="sr-only">
100
- Search pages and sections in {version}. Use arrow keys to choose a
101
- result and Enter to open it.
112
+ {formatMessage(messages.searchDescription, { version })}
102
113
  </DialogDescription>
103
- <Command shouldFilter={false} label="Search documentation">
114
+ <Command shouldFilter={false} label={messages.searchDocumentation}>
104
115
  <CommandInput
105
- aria-label="Search documentation"
106
- placeholder="Search documentation…"
116
+ aria-label={messages.searchDocumentation}
117
+ placeholder={messages.searchPlaceholder}
107
118
  value={query}
108
119
  onValueChange={setQuery}
109
120
  maxLength={120}
@@ -111,21 +122,24 @@ export function DocsSearchDialog({
111
122
  <div className="px-2 pt-2 pb-1 text-[11px] text-muted-foreground">
112
123
  {version}
113
124
  </div>
114
- <CommandList aria-busy={loading}>
125
+ <CommandList
126
+ aria-busy={loading}
127
+ aria-label={messages.searchDocumentation}
128
+ >
115
129
  {loading ? (
116
130
  <div
117
131
  role="status"
118
132
  className="flex justify-center gap-2 p-6 text-sm text-muted-foreground"
119
133
  >
120
134
  <LoaderCircle className="size-4 animate-spin motion-reduce:animate-none" />
121
- Loading search…
135
+ {messages.loadingSearch}
122
136
  </div>
123
137
  ) : state.error ? (
124
138
  <div
125
139
  role="status"
126
140
  className="p-4 text-center text-sm text-muted-foreground"
127
141
  >
128
- Search is unavailable.{' '}
142
+ {messages.searchUnavailable}{' '}
129
143
  <Button
130
144
  variant="link"
131
145
  size="sm"
@@ -134,16 +148,16 @@ export function DocsSearchDialog({
134
148
  setRetry((value) => value + 1)
135
149
  }}
136
150
  >
137
- Try again
151
+ {messages.tryAgain}
138
152
  </Button>
139
153
  </div>
140
154
  ) : !query.trim() ? (
141
155
  <div className="px-4 py-6 text-center text-sm text-muted-foreground">
142
- Type to search this documentation.
156
+ {messages.typeToSearch}
143
157
  </div>
144
158
  ) : (
145
159
  <>
146
- <CommandEmpty>No results found.</CommandEmpty>
160
+ <CommandEmpty>{messages.noResults}</CommandEmpty>
147
161
  {results.map((entry) => (
148
162
  <CommandItem
149
163
  key={entry.href}
@@ -175,7 +189,8 @@ export function DocsSearchDialog({
175
189
  )}
176
190
  </CommandList>
177
191
  <div className="border-t px-2 py-2 text-[10px] text-muted-foreground">
178
- ↑ ↓ Navigate <span className="mx-2">↵ Open</span> Esc Close
192
+ ↑ ↓ {messages.navigate}{' '}
193
+ <span className="mx-2">↵ {messages.open}</span> Esc {messages.close}
179
194
  </div>
180
195
  </Command>
181
196
  </DialogContent>
@@ -1,9 +1,12 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import { useEffect, useRef, useState } from 'react'
4
6
  import dynamic from 'next/dynamic'
5
7
  import { Search } from 'lucide-react'
6
8
  import { Button } from './button'
9
+ import { Kbd } from './kbd'
7
10
 
8
11
  const SearchDialog = dynamic(
9
12
  () =>
@@ -19,6 +22,7 @@ export function DocsSearch({
19
22
  indexUrl: string
20
23
  version: string
21
24
  }) {
25
+ const messages = useMessages()
22
26
  const [open, setOpen] = useState(false)
23
27
  const [activated, setActivated] = useState(false)
24
28
  const trigger = useRef<HTMLButtonElement | null>(null)
@@ -50,20 +54,20 @@ export function DocsSearch({
50
54
  data-rdx-search-trigger
51
55
  variant="outline"
52
56
  size="sm"
53
- aria-label="Search documentation"
57
+ aria-label={messages.searchDocumentation}
54
58
  aria-haspopup="dialog"
55
59
  aria-expanded={open}
56
60
  onClick={show}
57
61
  className="h-8 gap-2 px-2 text-muted-foreground sm:min-w-40 sm:justify-start"
58
62
  >
59
63
  <Search className="size-3.5" />
60
- <span className="hidden sm:inline">Search…</span>
61
- <kbd
62
- className="ml-auto hidden rounded border px-1 text-[10px] text-muted-foreground sm:inline"
64
+ <span className="hidden sm:inline">{messages.search}</span>
65
+ <Kbd
66
+ className="ml-auto hidden h-auto rounded border bg-transparent px-1 text-[10px] text-muted-foreground sm:inline"
63
67
  aria-hidden="true"
64
68
  >
65
69
  ⌘ / Ctrl K
66
- </kbd>
70
+ </Kbd>
67
71
  </Button>
68
72
  {activated && (
69
73
  <SearchDialog
@@ -1,10 +1,11 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import { useEffect, useRef, useState } from 'react'
4
6
  import { SidebarClose, SidebarOpen } from 'lucide-react'
5
7
 
6
8
  import { Button } from './button'
7
- import { ButtonLink } from './button-link'
8
9
  import { Separator } from './separator'
9
10
 
10
11
  export type SidebarCategory = {
@@ -19,6 +20,7 @@ export function DocsSidebar({
19
20
  categories: SidebarCategory[]
20
21
  pathname: string
21
22
  }) {
23
+ const messages = useMessages()
22
24
  const [openPath, setOpenPath] = useState<string | null>(null)
23
25
  const isOpen = openPath === pathname
24
26
  const dialog = useRef<HTMLDialogElement>(null)
@@ -27,7 +29,7 @@ export function DocsSidebar({
27
29
  if (!isOpen && dialog.current?.open) dialog.current?.close()
28
30
  }, [isOpen])
29
31
  const content = (
30
- <nav aria-label="Documentation pages">
32
+ <nav aria-label={messages.documentationPages}>
31
33
  {categories.map((category, index) => (
32
34
  <div
33
35
  key={`${index}:${category.title}`}
@@ -37,7 +39,7 @@ export function DocsSidebar({
37
39
  {category.title}
38
40
  </p>
39
41
  {category.links.map((link) => (
40
- <ButtonLink
42
+ <Button
41
43
  key={link.href}
42
44
  variant="ghost"
43
45
  size="sm"
@@ -49,7 +51,7 @@ export function DocsSidebar({
49
51
  className="justify-start text-sm text-primary"
50
52
  >
51
53
  {link.label}
52
- </ButtonLink>
54
+ </Button>
53
55
  ))}
54
56
  {index < categories.length - 1 && <Separator className="my-4" />}
55
57
  </div>
@@ -62,7 +64,7 @@ export function DocsSidebar({
62
64
  <Button
63
65
  variant="ghost"
64
66
  onClick={() => setOpenPath(pathname)}
65
- aria-label="Open sidebar"
67
+ aria-label={messages.openSidebar}
66
68
  >
67
69
  <SidebarOpen className="size-4" />
68
70
  </Button>
@@ -75,7 +77,7 @@ export function DocsSidebar({
75
77
  </aside>
76
78
  <dialog
77
79
  ref={dialog}
78
- aria-label="Documentation navigation"
80
+ aria-label={messages.documentationNavigation}
79
81
  onClose={() => setOpenPath(null)}
80
82
  className="fixed inset-0 m-0 h-dvh max-h-none w-full max-w-none bg-background text-foreground border-0 p-4 backdrop:bg-black/40"
81
83
  >
@@ -83,7 +85,7 @@ export function DocsSidebar({
83
85
  <Button
84
86
  variant="ghost"
85
87
  onClick={() => setOpenPath(null)}
86
- aria-label="Close sidebar"
88
+ aria-label={messages.closeSidebar}
87
89
  >
88
90
  <SidebarClose className="size-4" />
89
91
  </Button>
@@ -1,5 +1,7 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import { useEffect, useRef, useState } from 'react'
4
6
  import { usePathname } from 'next/navigation'
5
7
 
@@ -77,6 +79,7 @@ export function DocsToc({ contentId = 'rdx-main' }: { contentId?: string }) {
77
79
  }
78
80
 
79
81
  function PageToc({ contentId }: { contentId: string }) {
82
+ const messages = useMessages()
80
83
  const [headings, setHeadings] = useState<Heading[]>([])
81
84
  const [activeId, setActiveId] = useState('')
82
85
  const actions = useRef<PopoverActions | null>(null)
@@ -184,7 +187,7 @@ function PageToc({ contentId }: { contentId: string }) {
184
187
  <PopoverTrigger
185
188
  openOnHover
186
189
  delay={120}
187
- aria-label="On this page"
190
+ aria-label={messages.onThisPage}
188
191
  className="flex max-h-[40vh] w-12 flex-col items-end gap-2 overflow-hidden rounded-md p-3 text-muted-foreground/40 outline-none transition-colors hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
189
192
  >
190
193
  {headings.map((heading) => (
@@ -215,9 +218,9 @@ function PageToc({ contentId }: { contentId: string }) {
215
218
  className="hidden max-h-[min(60vh,var(--available-height))] w-64 overflow-y-auto xl:flex"
216
219
  >
217
220
  <PopoverTitle className="px-2 pt-1 text-xs text-muted-foreground">
218
- On this page
221
+ {messages.onThisPage}
219
222
  </PopoverTitle>
220
- <nav aria-label="On this page">
223
+ <nav aria-label={messages.onThisPage}>
221
224
  <HeadingLinks
222
225
  nodes={headingTree(headings)}
223
226
  baseDepth={baseDepth}
@@ -1,17 +1,21 @@
1
+ import { getMessages } from '../lib/i18n'
1
2
  import type { ReactNode } from 'react'
2
3
 
3
- import { ButtonLink } from './button-link'
4
+ import { Button } from './button'
4
5
 
5
6
  /** Shared site identity and framework credit for documentation footers. */
6
7
  export function FooterCredit({
8
+ lang,
7
9
  name,
8
10
  since,
9
11
  children,
10
12
  }: {
13
+ lang?: string
11
14
  name: string
12
15
  since?: number
13
16
  children?: ReactNode
14
17
  }) {
18
+ const messages = getMessages(lang)
15
19
  const currentYear = new Date().getFullYear()
16
20
  const years =
17
21
  since !== undefined && since !== currentYear
@@ -24,8 +28,8 @@ export function FooterCredit({
24
28
  &copy; {years} <strong>{name}</strong>
25
29
  {children}
26
30
  <span className="inline-block">
27
- {' · Powered by '}
28
- <ButtonLink
31
+ {` · ${messages.poweredBy} `}
32
+ <Button
29
33
  variant="link"
30
34
  size="link"
31
35
  href="https://github.com/lexibase/rdx"
@@ -33,7 +37,7 @@ export function FooterCredit({
33
37
  rel="noopener noreferrer"
34
38
  >
35
39
  RDX
36
- </ButtonLink>
40
+ </Button>
37
41
  </span>
38
42
  </p>
39
43
  </section>
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import { ButtonLink } from './button-link'
3
+ import { Button } from './button'
4
4
  import { Logo } from './logo'
5
5
 
6
6
  /** The default landing page from apps/web, not a separate marketing theme. */
@@ -32,13 +32,13 @@ export function Home({
32
32
  {buttons.length > 0 && (
33
33
  <div className="flex flex-wrap justify-center gap-3 mt-6 px-6">
34
34
  {buttons.map((button, index) => (
35
- <ButtonLink
35
+ <Button
36
36
  key={`${index}:${button.href}`}
37
37
  variant={index === 0 ? 'default' : 'outline'}
38
38
  href={button.href}
39
39
  >
40
40
  {button.label}
41
- </ButtonLink>
41
+ </Button>
42
42
  ))}
43
43
  </div>
44
44
  )}
@@ -0,0 +1,29 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { cn } from '../lib/utils'
4
+
5
+ // shadcn/ui base-nova Kbd; see THIRD_PARTY_NOTICES.md for the upstream MIT license.
6
+ function Kbd({ className, ...props }: ComponentProps<'kbd'>) {
7
+ return (
8
+ <kbd
9
+ data-slot="kbd"
10
+ className={cn(
11
+ "pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ function KbdGroup({ className, ...props }: ComponentProps<'div'>) {
20
+ return (
21
+ <kbd
22
+ data-slot="kbd-group"
23
+ className={cn('inline-flex items-center gap-1', className)}
24
+ {...props}
25
+ />
26
+ )
27
+ }
28
+
29
+ export { Kbd, KbdGroup }
@@ -1,7 +1,11 @@
1
+ 'use client'
2
+
3
+ import { useMessages } from '../providers/locale-provider'
1
4
  import { Spline } from 'lucide-react'
2
5
  import { Button } from '@lexibase/rdx-ui/components/button'
3
6
 
4
7
  export function LoadingButton() {
8
+ const messages = useMessages()
5
9
  return (
6
10
  <Button
7
11
  variant="outline"
@@ -9,7 +13,7 @@ export function LoadingButton() {
9
13
  className="w-[134px] px-9 overflow-hidden"
10
14
  disabled
11
15
  aria-busy="true"
12
- aria-label="Loading versions"
16
+ aria-label={messages.loadingVersions}
13
17
  >
14
18
  <div className="animate-spin flex items-center justify-center">
15
19
  <Spline className="size-4" />
@@ -0,0 +1,77 @@
1
+ 'use client'
2
+
3
+ import { useState, type ComponentProps, type ReactNode } from 'react'
4
+ import { CodeXml, FolderTree, GitCompareArrows, WrapText } from 'lucide-react'
5
+
6
+ import { useMessages } from '../providers/locale-provider'
7
+ import { Button } from './button'
8
+ import {
9
+ CodeBlock,
10
+ CodeBlockActions,
11
+ CodeBlockContent,
12
+ CodeBlockCopyButton,
13
+ CodeBlockHeader,
14
+ CodeBlockTitle,
15
+ } from './code-block'
16
+
17
+ /** The wrap action follows shadcn's stateful Code Block composition. */
18
+ export function MdxCodeBlockView({
19
+ label,
20
+ mode,
21
+ value,
22
+ preProps,
23
+ children,
24
+ }: {
25
+ label: string
26
+ mode?: 'folder' | 'diff'
27
+ value: string
28
+ preProps: ComponentProps<'pre'>
29
+ children: ReactNode
30
+ }) {
31
+ const [wrap, setWrap] = useState(false)
32
+ const messages = useMessages()
33
+ const Icon =
34
+ mode === 'folder'
35
+ ? FolderTree
36
+ : mode === 'diff'
37
+ ? GitCompareArrows
38
+ : CodeXml
39
+ return (
40
+ <CodeBlock
41
+ size="sm"
42
+ data-rdx-code-block
43
+ data-rdx-code-mode={mode}
44
+ data-rdx-code-value={value}
45
+ className="my-4 max-w-full"
46
+ >
47
+ <CodeBlockHeader>
48
+ <CodeBlockTitle>
49
+ <Icon aria-hidden="true" /> {label}
50
+ </CodeBlockTitle>
51
+ <CodeBlockActions>
52
+ <Button
53
+ variant="ghost"
54
+ size="icon-xs"
55
+ className="md:hidden"
56
+ aria-label={messages.wrapCode}
57
+ title={messages.wrapCode}
58
+ aria-pressed={wrap}
59
+ onClick={() => setWrap(!wrap)}
60
+ >
61
+ <WrapText aria-hidden="true" />
62
+ </Button>
63
+ <CodeBlockCopyButton value={value} />
64
+ </CodeBlockActions>
65
+ </CodeBlockHeader>
66
+ <CodeBlockContent
67
+ {...preProps}
68
+ wrap={wrap}
69
+ className={[preProps.className, 'data-wrap:wrap-anywhere']
70
+ .filter(Boolean)
71
+ .join(' ')}
72
+ >
73
+ {children}
74
+ </CodeBlockContent>
75
+ </CodeBlock>
76
+ )
77
+ }