@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.
@@ -0,0 +1,108 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ isValidElement,
5
+ type ComponentProps,
6
+ type ReactNode,
7
+ } from 'react'
8
+
9
+ import { getMessages } from '../lib/i18n'
10
+ import { CodeBlockLine } from './code-block'
11
+ import { MdxCodeBlockView } from './mdx-code-block-view'
12
+
13
+ function textOf(children: ReactNode): string {
14
+ return Children.toArray(children)
15
+ .map((child) =>
16
+ typeof child === 'string' || typeof child === 'number'
17
+ ? String(child)
18
+ : isValidElement<{ children?: ReactNode }>(child)
19
+ ? textOf(child.props.children)
20
+ : ''
21
+ )
22
+ .join('')
23
+ }
24
+
25
+ // Shadcn draws diff markers itself. Strip the original marker from highlighted
26
+ // tokens while keeping the original patch as the clipboard value.
27
+ function withoutMarker(children: ReactNode): ReactNode {
28
+ let remaining = 1
29
+ function strip(nodes: ReactNode): ReactNode {
30
+ return Children.map(nodes, (node) => {
31
+ if (!remaining) return node
32
+ if (typeof node === 'string') {
33
+ const value = node.slice(remaining)
34
+ remaining = Math.max(0, remaining - node.length)
35
+ return value
36
+ }
37
+ return isValidElement<{ children?: ReactNode }>(node)
38
+ ? cloneElement(node, {}, strip(node.props.children))
39
+ : node
40
+ })
41
+ }
42
+ return strip(children)
43
+ }
44
+
45
+ /** Translate Markdown fences to the complete shadcn Code Block composition. */
46
+ export function MdxCodeBlock({
47
+ lang,
48
+ children,
49
+ 'data-rdx-code-mode': mode,
50
+ ...props
51
+ }: ComponentProps<'pre'> & { 'data-rdx-code-mode'?: 'folder' | 'diff' }) {
52
+ const messages = getMessages(lang)
53
+ const code = Children.toArray(children).find((child) => isValidElement(child))
54
+ const codeProps = isValidElement<{
55
+ className?: string
56
+ children?: ReactNode
57
+ }>(code)
58
+ ? code.props
59
+ : undefined
60
+ const language = codeProps?.className?.match(/(?:^|\s)language-([^\s]+)/)?.[1]
61
+ const value = textOf(codeProps?.children ?? children)
62
+ const label =
63
+ mode === 'folder' ? messages.folderTree : (language ?? messages.code)
64
+ const content =
65
+ mode === 'diff'
66
+ ? Children.map(codeProps?.children, (node) => {
67
+ if (
68
+ !isValidElement<{
69
+ children?: ReactNode
70
+ 'data-rdx-line-variant'?: string
71
+ }>(node)
72
+ )
73
+ return node
74
+ const variant = node.props['data-rdx-line-variant']
75
+ if (!variant) return node
76
+ return (
77
+ <CodeBlockLine
78
+ variant={
79
+ variant === 'added' || variant === 'removed'
80
+ ? variant
81
+ : variant === 'hunk'
82
+ ? 'highlighted'
83
+ : 'default'
84
+ }
85
+ >
86
+ {variant === 'added' || variant === 'removed'
87
+ ? withoutMarker(node.props.children)
88
+ : node.props.children}
89
+ </CodeBlockLine>
90
+ )
91
+ })
92
+ : (codeProps?.children ?? children)
93
+ return (
94
+ <MdxCodeBlockView
95
+ label={label}
96
+ mode={mode}
97
+ value={value}
98
+ preProps={{
99
+ ...props,
100
+ className: [props.className, language && `language-${language}`]
101
+ .filter(Boolean)
102
+ .join(' '),
103
+ }}
104
+ >
105
+ {content}
106
+ </MdxCodeBlockView>
107
+ )
108
+ }
@@ -1,6 +1,14 @@
1
- import type { ComponentProps } from 'react'
1
+ import { getMessages } from '../lib/i18n'
2
+ import {
3
+ Children,
4
+ cloneElement,
5
+ isValidElement,
6
+ type ComponentProps,
7
+ type ReactNode,
8
+ } from 'react'
2
9
 
3
10
  import { cn } from '../lib/utils'
11
+ import { MdxImage } from './mdx-image'
4
12
  import {
5
13
  Accordion,
6
14
  AccordionContent,
@@ -9,10 +17,59 @@ import {
9
17
  } from './accordion'
10
18
  import { Blockquote } from './blockquote'
11
19
  import { Callout } from './callout'
12
- import { CodeBlock } from './code-block'
20
+ import {
21
+ CodeBlock,
22
+ CodeBlockActions,
23
+ CodeBlockContent,
24
+ CodeBlockCopyButton,
25
+ CodeBlockFooter,
26
+ CodeBlockHeader,
27
+ CodeBlockLine,
28
+ CodeBlockTitle,
29
+ } from './code-block'
30
+ import { MdxCodeBlock } from './mdx-code-block'
31
+ import { Kbd, KbdGroup } from './kbd'
32
+ import {
33
+ Table,
34
+ TableBody,
35
+ TableCaption,
36
+ TableCell,
37
+ TableFooter,
38
+ TableHead,
39
+ TableHeader,
40
+ TableRow,
41
+ } from './table'
13
42
  import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs'
14
43
  import { Separator } from '@lexibase/rdx-ui/components/separator'
15
44
 
45
+ function linkedImages(children: ReactNode): ReactNode {
46
+ return Children.map(children, (child) => {
47
+ if (!isValidElement<{ children?: ReactNode; zoom?: boolean }>(child))
48
+ return child
49
+ if (child.type === MdxImage) return cloneElement(child, { zoom: false })
50
+ return child.props.children
51
+ ? cloneElement(child, {}, linkedImages(child.props.children))
52
+ : child
53
+ })
54
+ }
55
+
56
+ function MdxTable({ lang, className, ...props }: ComponentProps<'table'>) {
57
+ return (
58
+ <Table
59
+ containerProps={{
60
+ 'data-slot': 'mdx-table-scroll',
61
+ role: 'region',
62
+ 'aria-label': getMessages(lang).table,
63
+ tabIndex: 0,
64
+ className:
65
+ 'my-6 min-w-0 max-w-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
66
+ }}
67
+ className={className}
68
+ {...props}
69
+ />
70
+ )
71
+ }
72
+
16
73
  export const mdxComponents = {
17
74
  h1: ({ className, ...props }: ComponentProps<'h1'>) => (
18
75
  <h1 className={cn('text-4xl font-bold my-6', className)} {...props} />
@@ -47,14 +104,16 @@ export const mdxComponents = {
47
104
  {...props}
48
105
  />
49
106
  ),
50
- a: ({ className, ...props }: ComponentProps<'a'>) => (
107
+ a: ({ className, children, ...props }: ComponentProps<'a'>) => (
51
108
  <a
52
109
  className={cn('underline text-chart-1 text-blue-500', className)}
53
110
  {...props}
54
- />
111
+ >
112
+ {linkedImages(children)}
113
+ </a>
55
114
  ),
56
115
  blockquote: Blockquote,
57
- pre: CodeBlock,
116
+ pre: MdxCodeBlock,
58
117
  code: ({ className, ...props }: ComponentProps<'code'>) => (
59
118
  <code
60
119
  className={cn(
@@ -77,49 +136,35 @@ export const mdxComponents = {
77
136
  hr: ({ className, ...props }: ComponentProps<'hr'>) => (
78
137
  <hr className={cn('my-8 border-border', className)} {...props} />
79
138
  ),
80
- img: ({ className, alt = '', ...props }: ComponentProps<'img'>) => (
81
- <img
82
- alt={alt}
83
- className={cn('max-w-full h-auto rounded-md my-4', className)}
84
- {...props}
85
- />
86
- ),
87
- table: ({ className, ...props }: ComponentProps<'table'>) => (
88
- <div
89
- data-slot="mdx-table-scroll"
90
- role="region"
91
- aria-label="Table"
92
- tabIndex={0}
93
- className="my-6 min-w-0 max-w-full overflow-x-auto rounded-lg border border-border bg-background focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
94
- >
95
- <table
96
- className={cn(
97
- 'w-full border-separate border-spacing-0 text-sm [&_tbody>tr]:transition-colors motion-reduce:[&_tbody>tr]:transition-none [&_tbody>tr:nth-child(even)]:bg-muted/20 [&_tbody>tr:hover]:bg-muted/40 [&_tbody>tr:last-child>td]:border-b-0',
98
- className
99
- )}
100
- {...props}
101
- />
102
- </div>
103
- ),
139
+ img: MdxImage,
140
+ table: MdxTable,
141
+ thead: TableHeader,
142
+ tbody: TableBody,
143
+ tfoot: TableFooter,
144
+ tr: TableRow,
145
+ caption: TableCaption,
104
146
  th: ({ className, scope = 'col', ...props }: ComponentProps<'th'>) => (
105
- <th
106
- scope={scope}
107
- className={cn(
108
- 'border-b border-r border-border bg-muted/40 px-4 py-3 text-left text-xs font-semibold tracking-wide text-foreground/90 last:border-r-0',
109
- className
110
- )}
111
- {...props}
112
- />
113
- ),
114
- td: ({ className, ...props }: ComponentProps<'td'>) => (
115
- <td
116
- className={cn(
117
- 'min-w-40 border-b border-r border-border/70 px-4 py-3 align-top leading-6 last:border-r-0',
118
- className
119
- )}
120
- {...props}
121
- />
122
- ),
147
+ <TableHead scope={scope} className={className} {...props} />
148
+ ),
149
+ td: TableCell,
150
+ CodeBlock,
151
+ CodeBlockActions,
152
+ CodeBlockContent,
153
+ CodeBlockCopyButton,
154
+ CodeBlockFooter,
155
+ CodeBlockHeader,
156
+ CodeBlockLine,
157
+ CodeBlockTitle,
158
+ Kbd,
159
+ KbdGroup,
160
+ Table,
161
+ TableHeader,
162
+ TableBody,
163
+ TableFooter,
164
+ TableRow,
165
+ TableHead,
166
+ TableCell,
167
+ TableCaption,
123
168
  Callout,
124
169
  Accordion,
125
170
  AccordionItem,
@@ -140,3 +185,16 @@ export const mdxComponents = {
140
185
  />
141
186
  ),
142
187
  }
188
+
189
+ /** Bind server-rendered MDX labels to the same locale as interactive components. */
190
+ export function getMdxComponents(lang?: string) {
191
+ return {
192
+ ...mdxComponents,
193
+ pre: (props: ComponentProps<'pre'>) => (
194
+ <MdxCodeBlock {...props} lang={lang} />
195
+ ),
196
+ table: (props: ComponentProps<'table'>) => (
197
+ <MdxTable {...props} lang={lang} />
198
+ ),
199
+ }
200
+ }
@@ -0,0 +1,73 @@
1
+ 'use client'
2
+
3
+ import type { ComponentProps } from 'react'
4
+
5
+ import { formatMessage } from '../lib/i18n'
6
+ import { cn } from '../lib/utils'
7
+ import { useMessages } from '../providers/locale-provider'
8
+ import { Button } from './button'
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogTitle,
14
+ DialogTrigger,
15
+ } from './dialog'
16
+
17
+ /** Markdown images keep their normal appearance and use shadcn Dialog to expand. */
18
+ export function MdxImage({
19
+ className,
20
+ alt = '',
21
+ zoom = true,
22
+ ...props
23
+ }: ComponentProps<'img'> & { zoom?: boolean }) {
24
+ const messages = useMessages()
25
+ const image = (
26
+ <img
27
+ {...props}
28
+ alt={alt}
29
+ className={cn('max-w-full h-auto rounded-md my-4', className)}
30
+ />
31
+ )
32
+ // Decorative images and images inside links retain their original semantics.
33
+ if (!zoom || !alt.trim() || !props.src) return image
34
+ return (
35
+ <Dialog>
36
+ <DialogTrigger
37
+ data-rdx-image-trigger
38
+ aria-label={formatMessage(messages.enlargeImage, { alt })}
39
+ title={formatMessage(messages.enlargeImage, { alt })}
40
+ render={
41
+ <Button
42
+ variant="ghost"
43
+ className="h-auto max-w-full cursor-zoom-in p-0 align-middle hover:bg-transparent dark:hover:bg-transparent"
44
+ />
45
+ }
46
+ >
47
+ {image}
48
+ </DialogTrigger>
49
+ <DialogContent
50
+ data-rdx-image-dialog
51
+ closeLabel={messages.closeImage}
52
+ overlayClassName="bg-black/70"
53
+ className="flex max-h-[calc(100dvh-2rem)] w-max max-w-[calc(100vw-2rem)] flex-col gap-0 p-2 sm:max-w-[calc(100vw-4rem)] [&>[data-slot=dialog-close]]:bg-popover/90"
54
+ >
55
+ <DialogTitle className="sr-only">{alt}</DialogTitle>
56
+ <DialogDescription className="sr-only">
57
+ {messages.imagePreviewDescription}
58
+ </DialogDescription>
59
+ <img
60
+ src={props.src}
61
+ srcSet={props.srcSet}
62
+ sizes={props.srcSet ? 'calc(100vw - 4rem)' : undefined}
63
+ crossOrigin={props.crossOrigin}
64
+ referrerPolicy={props.referrerPolicy}
65
+ alt={alt}
66
+ loading="eager"
67
+ decoding="async"
68
+ className="h-auto max-h-[calc(100dvh-4rem)] w-auto max-w-full self-center rounded-md object-contain"
69
+ />
70
+ </DialogContent>
71
+ </Dialog>
72
+ )
73
+ }
@@ -1,10 +1,13 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import { SunMoon } from 'lucide-react'
4
6
  import { useTheme } from 'next-themes'
5
7
  import { Button } from '@lexibase/rdx-ui/components/button'
6
8
 
7
9
  export function ModeToggle() {
10
+ const messages = useMessages()
8
11
  const { theme, setTheme } = useTheme()
9
12
 
10
13
  const toggleTheme = () => {
@@ -17,10 +20,10 @@ export function ModeToggle() {
17
20
  size="icon"
18
21
  variant="ghost"
19
22
  onClick={toggleTheme}
20
- aria-label="Mode Toggle"
23
+ aria-label={messages.modeToggle}
21
24
  >
22
25
  <SunMoon className="h-[1.2rem] w-[1.2rem]" />
23
- <span className="sr-only">colorTheme</span>
26
+ <span className="sr-only">{messages.modeToggle}</span>
24
27
  </Button>
25
28
  )
26
29
  }
@@ -1,13 +1,15 @@
1
+ import { getMessages } from '../lib/i18n'
1
2
  import type { ReactNode } from 'react'
2
3
  import { Github } from 'lucide-react'
3
4
 
4
- import { ButtonLink } from './button-link'
5
+ import { Button } from './button'
5
6
  import { Logo } from './logo'
6
7
  import { Separator } from './separator'
7
8
  import { ModeToggle } from './mode-toggle'
8
9
 
9
10
  /** Shared composition extracted from apps/web/components/layout/Navbar. */
10
11
  export function Navbar({
12
+ lang,
11
13
  title = '',
12
14
  siteTitle = 'RDX',
13
15
  logo = <Logo className="size-5" />,
@@ -19,6 +21,7 @@ export function Navbar({
19
21
  github,
20
22
  showToggleMode = false,
21
23
  }: {
24
+ lang?: string
22
25
  title?: string
23
26
  siteTitle?: string
24
27
  logo?: ReactNode
@@ -30,14 +33,15 @@ export function Navbar({
30
33
  github?: { visible: boolean; url: string }
31
34
  showToggleMode?: boolean
32
35
  }) {
36
+ const messages = getMessages(lang)
33
37
  return (
34
38
  <nav
35
39
  data-rdx-navbar
36
- aria-label="Main navigation"
40
+ aria-label={messages.mainNavigation}
37
41
  className="w-full flex flex-wrap shrink-0 items-center justify-between px-6 py-2 border-b border-border gap-y-2"
38
42
  >
39
43
  <div className="flex items-center gap-1">
40
- <ButtonLink
44
+ <Button
41
45
  variant="ghost"
42
46
  size={title === '' ? 'icon' : 'default'}
43
47
  aria-label={siteTitle}
@@ -46,20 +50,20 @@ export function Navbar({
46
50
  >
47
51
  {logo}
48
52
  {title !== '' && <span>{title}</span>}
49
- </ButtonLink>
53
+ </Button>
50
54
  </div>
51
55
  <div className="flex items-center gap-x-1 md:gap-x-2 h-4">
52
56
  {search}
53
57
  {versionMenu}
54
58
  {docsLink ?? (
55
- <ButtonLink variant="link" size="sm" href={docsHref}>
56
- Docs
57
- </ButtonLink>
59
+ <Button variant="link" size="sm" href={docsHref}>
60
+ {messages.docs}
61
+ </Button>
58
62
  )}
59
63
  {github?.visible && github.url && (
60
64
  <>
61
65
  <Separator orientation="vertical" />
62
- <ButtonLink
66
+ <Button
63
67
  variant="ghost"
64
68
  size="icon"
65
69
  title="GitHub"
@@ -71,7 +75,7 @@ export function Navbar({
71
75
  >
72
76
  <Github className="h-[1.2rem] w-[1.2rem]" />
73
77
  <span className="sr-only">GitHub</span>
74
- </ButtonLink>
78
+ </Button>
75
79
  </>
76
80
  )}
77
81
  {showToggleMode && (
@@ -0,0 +1,36 @@
1
+ import { getMessages } from '../lib/i18n'
2
+ import { Button } from './button'
3
+
4
+ /** The development website's 404 layout, shared with generated projects. */
5
+ export function NotFound({
6
+ href = '/docs',
7
+ lang,
8
+ }: {
9
+ href?: string
10
+ lang?: string
11
+ }) {
12
+ const messages = getMessages(lang)
13
+
14
+ return (
15
+ <main
16
+ id="rdx-main"
17
+ data-rdx-not-found
18
+ tabIndex={-1}
19
+ className="bg-transparent text-primary h-full min-h-0 w-full flex flex-1 items-center justify-center cursor-default p-7 outline-none"
20
+ >
21
+ <div className="w-full md:w-1/2">
22
+ <h1 className="text-9xl font-bold font-mono text-center">404</h1>
23
+ <article className="flex flex-col gap-y-5">
24
+ <p className="text-3xl font-bold font-mono text-center">
25
+ {messages.notFound}
26
+ </p>
27
+ <p className="text-sm">{messages.notFoundDescription}</p>
28
+ <p className="text-sm">{messages.brokenLink}</p>
29
+ <Button variant="primary" href={href}>
30
+ {messages.backToDocs}
31
+ </Button>
32
+ </article>
33
+ </div>
34
+ </main>
35
+ )
36
+ }
@@ -0,0 +1,122 @@
1
+ 'use client'
2
+
3
+ import type { ComponentProps } from 'react'
4
+
5
+ import { cn } from '../lib/utils'
6
+
7
+ // shadcn/ui base-nova Table; see THIRD_PARTY_NOTICES.md for the upstream MIT license.
8
+ type TableProps = ComponentProps<'table'> & {
9
+ /** Configure the scroll container for documentation layout and keyboard access. */
10
+ containerProps?: ComponentProps<'div'> & { 'data-slot'?: string }
11
+ }
12
+
13
+ function Table({ className, containerProps, ...props }: TableProps) {
14
+ const { className: containerClassName, ...containerAttributes } =
15
+ containerProps ?? {}
16
+ return (
17
+ <div
18
+ data-slot="table-container"
19
+ {...containerAttributes}
20
+ className={cn('relative w-full overflow-x-auto', containerClassName)}
21
+ >
22
+ <table
23
+ data-slot="table"
24
+ className={cn('w-full caption-bottom text-sm', className)}
25
+ {...props}
26
+ />
27
+ </div>
28
+ )
29
+ }
30
+
31
+ function TableHeader({ className, ...props }: ComponentProps<'thead'>) {
32
+ return (
33
+ <thead
34
+ data-slot="table-header"
35
+ className={cn('[&_tr]:border-b', className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function TableBody({ className, ...props }: ComponentProps<'tbody'>) {
42
+ return (
43
+ <tbody
44
+ data-slot="table-body"
45
+ className={cn('[&_tr:last-child]:border-0', className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function TableFooter({ className, ...props }: ComponentProps<'tfoot'>) {
52
+ return (
53
+ <tfoot
54
+ data-slot="table-footer"
55
+ className={cn(
56
+ 'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function TableRow({ className, ...props }: ComponentProps<'tr'>) {
65
+ return (
66
+ <tr
67
+ data-slot="table-row"
68
+ className={cn(
69
+ 'border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted',
70
+ className
71
+ )}
72
+ {...props}
73
+ />
74
+ )
75
+ }
76
+
77
+ function TableHead({ className, ...props }: ComponentProps<'th'>) {
78
+ return (
79
+ <th
80
+ data-slot="table-head"
81
+ className={cn(
82
+ 'h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0',
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ )
88
+ }
89
+
90
+ function TableCell({ className, ...props }: ComponentProps<'td'>) {
91
+ return (
92
+ <td
93
+ data-slot="table-cell"
94
+ className={cn(
95
+ 'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ function TableCaption({ className, ...props }: ComponentProps<'caption'>) {
104
+ return (
105
+ <caption
106
+ data-slot="table-caption"
107
+ className={cn('mt-4 text-sm text-muted-foreground', className)}
108
+ {...props}
109
+ />
110
+ )
111
+ }
112
+
113
+ export {
114
+ Table,
115
+ TableHeader,
116
+ TableBody,
117
+ TableFooter,
118
+ TableHead,
119
+ TableRow,
120
+ TableCell,
121
+ TableCaption,
122
+ }