@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.
- package/THIRD_PARTY_NOTICES.md +7 -2
- package/package.json +8 -5
- package/src/components/button.tsx +2 -0
- package/src/components/code-block.tsx +274 -46
- package/src/components/copy-code-button.tsx +1 -79
- package/src/components/dialog.tsx +123 -36
- package/src/components/docs-search-dialog.tsx +6 -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 +47 -13
- package/src/components/mdx-image.tsx +73 -0
- package/src/components/tabs.tsx +7 -2
- package/src/hooks/use-highlighted-code.ts +75 -0
- package/src/lib/highlighter.tsx +124 -0
- package/src/lib/i18n.ts +12 -0
- package/src/styles/globals.css +1 -0
- package/src/styles/shadcn.css +650 -0
|
@@ -1,79 +1,166 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useMessages } from '../providers/locale-provider'
|
|
4
|
-
|
|
5
|
-
// shadcn/ui Base Nova Dialog, using the shared RDX theme and buttons.
|
|
6
3
|
import * as React from 'react'
|
|
7
4
|
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
|
|
8
|
-
import {
|
|
9
|
-
|
|
5
|
+
import { cn, mergeStateClassName } from '../lib/utils'
|
|
6
|
+
|
|
10
7
|
import { Button } from './button'
|
|
8
|
+
import { XIcon } from 'lucide-react'
|
|
9
|
+
import { useMessages } from '../providers/locale-provider'
|
|
11
10
|
|
|
12
|
-
function Dialog(props:
|
|
13
|
-
return <DialogPrimitive.Root {...props} />
|
|
11
|
+
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
12
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
) {
|
|
14
|
+
|
|
15
|
+
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
|
18
16
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
19
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
|
+
|
|
20
43
|
function DialogContent({
|
|
21
44
|
className,
|
|
22
45
|
children,
|
|
46
|
+
showCloseButton = true,
|
|
47
|
+
closeLabel,
|
|
48
|
+
overlayClassName,
|
|
23
49
|
...props
|
|
24
|
-
}:
|
|
50
|
+
}: DialogPrimitive.Popup.Props & {
|
|
51
|
+
showCloseButton?: boolean
|
|
52
|
+
closeLabel?: string
|
|
53
|
+
overlayClassName?: DialogPrimitive.Backdrop.Props['className']
|
|
54
|
+
}) {
|
|
25
55
|
const messages = useMessages()
|
|
26
56
|
return (
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
data-slot="dialog-overlay"
|
|
30
|
-
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"
|
|
31
|
-
/>
|
|
57
|
+
<DialogPortal>
|
|
58
|
+
<DialogOverlay className={overlayClassName} />
|
|
32
59
|
<DialogPrimitive.Popup
|
|
33
60
|
data-slot="dialog-content"
|
|
34
61
|
className={mergeStateClassName(
|
|
35
|
-
'fixed top-
|
|
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',
|
|
36
63
|
className
|
|
37
64
|
)}
|
|
38
65
|
{...props}
|
|
39
66
|
>
|
|
40
67
|
{children}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
)}
|
|
53
83
|
</DialogPrimitive.Popup>
|
|
54
|
-
</
|
|
84
|
+
</DialogPortal>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
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
|
+
/>
|
|
55
95
|
)
|
|
56
96
|
}
|
|
57
|
-
|
|
97
|
+
|
|
98
|
+
function DialogFooter({
|
|
58
99
|
className,
|
|
100
|
+
showCloseButton = false,
|
|
101
|
+
children,
|
|
59
102
|
...props
|
|
60
|
-
}: React.ComponentProps<
|
|
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) {
|
|
61
127
|
return (
|
|
62
128
|
<DialogPrimitive.Title
|
|
63
|
-
|
|
129
|
+
data-slot="dialog-title"
|
|
130
|
+
className={mergeStateClassName(
|
|
131
|
+
'cn-font-heading text-base leading-none font-medium',
|
|
132
|
+
className
|
|
133
|
+
)}
|
|
64
134
|
{...props}
|
|
65
135
|
/>
|
|
66
136
|
)
|
|
67
137
|
}
|
|
138
|
+
|
|
68
139
|
function DialogDescription({
|
|
69
140
|
className,
|
|
70
141
|
...props
|
|
71
|
-
}:
|
|
142
|
+
}: DialogPrimitive.Description.Props) {
|
|
72
143
|
return (
|
|
73
144
|
<DialogPrimitive.Description
|
|
74
|
-
|
|
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
|
+
)}
|
|
75
150
|
{...props}
|
|
76
151
|
/>
|
|
77
152
|
)
|
|
78
153
|
}
|
|
79
|
-
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
Dialog,
|
|
157
|
+
DialogClose,
|
|
158
|
+
DialogContent,
|
|
159
|
+
DialogDescription,
|
|
160
|
+
DialogFooter,
|
|
161
|
+
DialogHeader,
|
|
162
|
+
DialogOverlay,
|
|
163
|
+
DialogPortal,
|
|
164
|
+
DialogTitle,
|
|
165
|
+
DialogTrigger,
|
|
166
|
+
}
|
|
@@ -99,7 +99,12 @@ export function DocsSearchDialog({
|
|
|
99
99
|
}
|
|
100
100
|
return (
|
|
101
101
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
102
|
-
<DialogContent
|
|
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
|
+
>
|
|
103
108
|
<DialogTitle className="sr-only">
|
|
104
109
|
{messages.searchDocumentation}
|
|
105
110
|
</DialogTitle>
|
|
@@ -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
|
+
}
|
|
@@ -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,7 +1,14 @@
|
|
|
1
1
|
import { getMessages } from '../lib/i18n'
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
Children,
|
|
4
|
+
cloneElement,
|
|
5
|
+
isValidElement,
|
|
6
|
+
type ComponentProps,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react'
|
|
3
9
|
|
|
4
10
|
import { cn } from '../lib/utils'
|
|
11
|
+
import { MdxImage } from './mdx-image'
|
|
5
12
|
import {
|
|
6
13
|
Accordion,
|
|
7
14
|
AccordionContent,
|
|
@@ -10,7 +17,17 @@ import {
|
|
|
10
17
|
} from './accordion'
|
|
11
18
|
import { Blockquote } from './blockquote'
|
|
12
19
|
import { Callout } from './callout'
|
|
13
|
-
import {
|
|
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'
|
|
14
31
|
import { Kbd, KbdGroup } from './kbd'
|
|
15
32
|
import {
|
|
16
33
|
Table,
|
|
@@ -25,6 +42,17 @@ import {
|
|
|
25
42
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs'
|
|
26
43
|
import { Separator } from '@lexibase/rdx-ui/components/separator'
|
|
27
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
|
+
|
|
28
56
|
function MdxTable({ lang, className, ...props }: ComponentProps<'table'>) {
|
|
29
57
|
return (
|
|
30
58
|
<Table
|
|
@@ -76,14 +104,16 @@ export const mdxComponents = {
|
|
|
76
104
|
{...props}
|
|
77
105
|
/>
|
|
78
106
|
),
|
|
79
|
-
a: ({ className, ...props }: ComponentProps<'a'>) => (
|
|
107
|
+
a: ({ className, children, ...props }: ComponentProps<'a'>) => (
|
|
80
108
|
<a
|
|
81
109
|
className={cn('underline text-chart-1 text-blue-500', className)}
|
|
82
110
|
{...props}
|
|
83
|
-
|
|
111
|
+
>
|
|
112
|
+
{linkedImages(children)}
|
|
113
|
+
</a>
|
|
84
114
|
),
|
|
85
115
|
blockquote: Blockquote,
|
|
86
|
-
pre:
|
|
116
|
+
pre: MdxCodeBlock,
|
|
87
117
|
code: ({ className, ...props }: ComponentProps<'code'>) => (
|
|
88
118
|
<code
|
|
89
119
|
className={cn(
|
|
@@ -106,13 +136,7 @@ export const mdxComponents = {
|
|
|
106
136
|
hr: ({ className, ...props }: ComponentProps<'hr'>) => (
|
|
107
137
|
<hr className={cn('my-8 border-border', className)} {...props} />
|
|
108
138
|
),
|
|
109
|
-
img:
|
|
110
|
-
<img
|
|
111
|
-
alt={alt}
|
|
112
|
-
className={cn('max-w-full h-auto rounded-md my-4', className)}
|
|
113
|
-
{...props}
|
|
114
|
-
/>
|
|
115
|
-
),
|
|
139
|
+
img: MdxImage,
|
|
116
140
|
table: MdxTable,
|
|
117
141
|
thead: TableHeader,
|
|
118
142
|
tbody: TableBody,
|
|
@@ -123,6 +147,14 @@ export const mdxComponents = {
|
|
|
123
147
|
<TableHead scope={scope} className={className} {...props} />
|
|
124
148
|
),
|
|
125
149
|
td: TableCell,
|
|
150
|
+
CodeBlock,
|
|
151
|
+
CodeBlockActions,
|
|
152
|
+
CodeBlockContent,
|
|
153
|
+
CodeBlockCopyButton,
|
|
154
|
+
CodeBlockFooter,
|
|
155
|
+
CodeBlockHeader,
|
|
156
|
+
CodeBlockLine,
|
|
157
|
+
CodeBlockTitle,
|
|
126
158
|
Kbd,
|
|
127
159
|
KbdGroup,
|
|
128
160
|
Table,
|
|
@@ -158,7 +190,9 @@ export const mdxComponents = {
|
|
|
158
190
|
export function getMdxComponents(lang?: string) {
|
|
159
191
|
return {
|
|
160
192
|
...mdxComponents,
|
|
161
|
-
pre: (props: ComponentProps<'pre'>) =>
|
|
193
|
+
pre: (props: ComponentProps<'pre'>) => (
|
|
194
|
+
<MdxCodeBlock {...props} lang={lang} />
|
|
195
|
+
),
|
|
162
196
|
table: (props: ComponentProps<'table'>) => (
|
|
163
197
|
<MdxTable {...props} lang={lang} />
|
|
164
198
|
),
|
|
@@ -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
|
+
}
|
package/src/components/tabs.tsx
CHANGED
|
@@ -25,8 +25,13 @@ function panelText(node: Node): string {
|
|
|
25
25
|
)
|
|
26
26
|
)
|
|
27
27
|
return ''
|
|
28
|
-
if (node.matches('[data-rdx-code-block]')) {
|
|
29
|
-
|
|
28
|
+
if (node.matches('[data-rdx-code-block], [data-slot="code-block"]')) {
|
|
29
|
+
const pre = node.querySelector('pre')
|
|
30
|
+
const lines = pre?.querySelectorAll('[data-slot="code-block-line-content"]')
|
|
31
|
+
const text = lines?.length
|
|
32
|
+
? Array.from(lines, (line) => line.textContent ?? '').join('\n')
|
|
33
|
+
: pre?.textContent
|
|
34
|
+
return `${node.dataset.rdxCodeValue ?? text ?? ''}\n`
|
|
30
35
|
}
|
|
31
36
|
if (node.tagName === 'PRE') return `${node.textContent ?? ''}\n`
|
|
32
37
|
if (node.tagName === 'BR') return '\n'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { loadHighlighter } from '../lib/highlighter'
|
|
4
|
+
|
|
5
|
+
export function useHighlightedCode(value: string, language: string) {
|
|
6
|
+
// A trailing newline would render as an extra empty line.
|
|
7
|
+
const code = value.endsWith('\n') ? value.slice(0, -1) : value
|
|
8
|
+
const [loaded, setLoaded] = React.useState<{
|
|
9
|
+
language: string
|
|
10
|
+
highlighter: Awaited<ReturnType<typeof loadHighlighter>>
|
|
11
|
+
} | null>(null)
|
|
12
|
+
const highlighter = loaded?.language === language ? loaded.highlighter : null
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
let cancelled = false
|
|
16
|
+
|
|
17
|
+
loadHighlighter(language)
|
|
18
|
+
.then((next) => {
|
|
19
|
+
if (!cancelled) {
|
|
20
|
+
setLoaded({ language, highlighter: next })
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.catch(() => {})
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
cancelled = true
|
|
27
|
+
}
|
|
28
|
+
}, [language])
|
|
29
|
+
|
|
30
|
+
// The complete lines, highlighted. While the code streams, only the new
|
|
31
|
+
// lines are tokenized, starting from the grammar state of the last one.
|
|
32
|
+
const [done, setDone] = React.useState<{
|
|
33
|
+
highlighter: typeof highlighter
|
|
34
|
+
text: string
|
|
35
|
+
lines: React.ReactNode[]
|
|
36
|
+
state?: Parameters<NonNullable<typeof highlighter>['highlight']>[1]
|
|
37
|
+
}>({ highlighter: null, text: '', lines: [] })
|
|
38
|
+
|
|
39
|
+
let next = done
|
|
40
|
+
|
|
41
|
+
if (next.highlighter !== highlighter || !code.startsWith(next.text)) {
|
|
42
|
+
next = { highlighter, text: '', lines: [] }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const end = code.lastIndexOf('\n') + 1
|
|
46
|
+
|
|
47
|
+
if (highlighter && end > next.text.length) {
|
|
48
|
+
const result = highlighter.highlight(
|
|
49
|
+
code.slice(next.text.length, end - 1),
|
|
50
|
+
next.state
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
next = {
|
|
54
|
+
highlighter,
|
|
55
|
+
text: code.slice(0, end),
|
|
56
|
+
lines: [...next.lines, ...result.lines],
|
|
57
|
+
state: result.state,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (next !== done) {
|
|
62
|
+
setDone(next)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The last line is still being written, so it is tokenized on every render.
|
|
66
|
+
const tail = code.slice(end)
|
|
67
|
+
|
|
68
|
+
return React.useMemo(() => {
|
|
69
|
+
if (!highlighter) {
|
|
70
|
+
return code.split('\n')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [...next.lines, highlighter.highlight(tail, next.state).lines[0]]
|
|
74
|
+
}, [code, highlighter, next, tail])
|
|
75
|
+
}
|