@lexibase/rdx-ui 2.0.0 → 3.1.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 +8 -2
- package/package.json +8 -3
- 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-page-transition.tsx +178 -0
- package/src/components/docs-search-dialog.tsx +6 -1
- package/src/components/docs-sidebar.tsx +103 -30
- package/src/components/docs-toc.tsx +6 -6
- package/src/components/mdx-code-block-view.tsx +203 -0
- package/src/components/mdx-code-block.tsx +161 -0
- package/src/components/mdx-components.tsx +47 -16
- package/src/components/mdx-folder-tree.tsx +58 -0
- package/src/components/mdx-image.tsx +73 -0
- package/src/components/mdx-mermaid.tsx +252 -0
- package/src/components/sheet.tsx +146 -0
- package/src/components/tabs.tsx +12 -3
- package/src/hooks/use-highlighted-code.ts +75 -0
- package/src/lib/highlighter.tsx +124 -0
- package/src/lib/i18n.ts +32 -0
- package/src/lib/mermaid.ts +114 -0
- package/src/styles/globals.css +91 -14
- package/src/styles/shadcn.css +650 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ComponentProps,
|
|
9
|
+
type CSSProperties,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import { CodeXml, FolderTree, GitCompareArrows, WrapText } from 'lucide-react'
|
|
13
|
+
import { motion } from 'motion/react'
|
|
14
|
+
|
|
15
|
+
import { useMessages } from '../providers/locale-provider'
|
|
16
|
+
import { Button } from './button'
|
|
17
|
+
import {
|
|
18
|
+
CodeBlock,
|
|
19
|
+
CodeBlockActions,
|
|
20
|
+
CodeBlockContent,
|
|
21
|
+
CodeBlockCopyButton,
|
|
22
|
+
CodeBlockHeader,
|
|
23
|
+
CodeBlockTitle,
|
|
24
|
+
CodeBlockFooter,
|
|
25
|
+
} from './code-block'
|
|
26
|
+
|
|
27
|
+
// Keep native pre events separate from Motion's gesture event props.
|
|
28
|
+
const AnimatedCodeBlockContent = motion.create(function PreviewContent({
|
|
29
|
+
preProps,
|
|
30
|
+
...props
|
|
31
|
+
}: ComponentProps<typeof CodeBlockContent> & {
|
|
32
|
+
preProps: ComponentProps<'pre'>
|
|
33
|
+
}) {
|
|
34
|
+
return <CodeBlockContent {...preProps} {...props} />
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** The wrap action follows shadcn's stateful Code Block composition. */
|
|
38
|
+
export function MdxCodeBlockView({
|
|
39
|
+
label,
|
|
40
|
+
mode,
|
|
41
|
+
value,
|
|
42
|
+
lineNumbers,
|
|
43
|
+
startLine,
|
|
44
|
+
preProps,
|
|
45
|
+
children,
|
|
46
|
+
}: {
|
|
47
|
+
label: string
|
|
48
|
+
mode?: 'folder' | 'diff'
|
|
49
|
+
value: string
|
|
50
|
+
lineNumbers?: boolean
|
|
51
|
+
startLine?: number
|
|
52
|
+
preProps: ComponentProps<'pre'>
|
|
53
|
+
children: ReactNode
|
|
54
|
+
}) {
|
|
55
|
+
const [wrap, setWrap] = useState(false)
|
|
56
|
+
const [expanded, setExpanded] = useState(false)
|
|
57
|
+
const [previewHeight, setPreviewHeight] = useState<number | null>(null)
|
|
58
|
+
const [fullHeight, setFullHeight] = useState(0)
|
|
59
|
+
const [animateSize, setAnimateSize] = useState(false)
|
|
60
|
+
const blockRef = useRef<HTMLDivElement>(null)
|
|
61
|
+
const generatedId = useId()
|
|
62
|
+
const contentId = preProps.id ?? generatedId
|
|
63
|
+
const collapsed = previewHeight !== null && !expanded
|
|
64
|
+
const contentStyle: CSSProperties = {
|
|
65
|
+
...preProps.style,
|
|
66
|
+
...(previewHeight !== null && {
|
|
67
|
+
height: 'var(--rdx-code-preview-height)',
|
|
68
|
+
}),
|
|
69
|
+
}
|
|
70
|
+
const messages = useMessages()
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const pre = blockRef.current?.querySelector<HTMLElement>(
|
|
74
|
+
'[data-slot="code-block-content"]'
|
|
75
|
+
)
|
|
76
|
+
const code = pre?.querySelector('code')
|
|
77
|
+
if (!pre || !code) return
|
|
78
|
+
const desktop = window.matchMedia('(min-width: 768px)')
|
|
79
|
+
let frame = 0
|
|
80
|
+
let observer: ResizeObserver | undefined
|
|
81
|
+
const measure = () => {
|
|
82
|
+
if (desktop.matches) return
|
|
83
|
+
const style = getComputedStyle(pre)
|
|
84
|
+
const lineHeight = parseFloat(style.lineHeight)
|
|
85
|
+
const padding =
|
|
86
|
+
parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)
|
|
87
|
+
if (!Number.isFinite(lineHeight)) return
|
|
88
|
+
const limit = lineHeight * 12 + padding
|
|
89
|
+
const height = code.getBoundingClientRect().height + padding
|
|
90
|
+
setFullHeight(height)
|
|
91
|
+
setPreviewHeight(height > limit + 1 ? limit : null)
|
|
92
|
+
}
|
|
93
|
+
const schedule = () => {
|
|
94
|
+
cancelAnimationFrame(frame)
|
|
95
|
+
frame = requestAnimationFrame(measure)
|
|
96
|
+
}
|
|
97
|
+
const updateViewport = () => {
|
|
98
|
+
setAnimateSize(false)
|
|
99
|
+
observer?.disconnect()
|
|
100
|
+
cancelAnimationFrame(frame)
|
|
101
|
+
if (desktop.matches) {
|
|
102
|
+
setPreviewHeight(null)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
observer = new ResizeObserver(schedule)
|
|
106
|
+
observer.observe(code)
|
|
107
|
+
observer.observe(pre)
|
|
108
|
+
schedule()
|
|
109
|
+
}
|
|
110
|
+
updateViewport()
|
|
111
|
+
desktop.addEventListener('change', updateViewport)
|
|
112
|
+
return () => {
|
|
113
|
+
observer?.disconnect()
|
|
114
|
+
cancelAnimationFrame(frame)
|
|
115
|
+
desktop.removeEventListener('change', updateViewport)
|
|
116
|
+
}
|
|
117
|
+
}, [value])
|
|
118
|
+
const Icon =
|
|
119
|
+
mode === 'folder'
|
|
120
|
+
? FolderTree
|
|
121
|
+
: mode === 'diff'
|
|
122
|
+
? GitCompareArrows
|
|
123
|
+
: CodeXml
|
|
124
|
+
return (
|
|
125
|
+
<CodeBlock
|
|
126
|
+
size="sm"
|
|
127
|
+
ref={blockRef}
|
|
128
|
+
data-rdx-code-block
|
|
129
|
+
data-rdx-code-mode={mode}
|
|
130
|
+
data-rdx-code-value={value}
|
|
131
|
+
data-rdx-code-collapsed={collapsed || undefined}
|
|
132
|
+
className="my-4 max-w-full"
|
|
133
|
+
>
|
|
134
|
+
<CodeBlockHeader>
|
|
135
|
+
<CodeBlockTitle title={label}>
|
|
136
|
+
<Icon aria-hidden="true" /> {label}
|
|
137
|
+
</CodeBlockTitle>
|
|
138
|
+
<CodeBlockActions>
|
|
139
|
+
<Button
|
|
140
|
+
variant="ghost"
|
|
141
|
+
size="icon-xs"
|
|
142
|
+
className="md:hidden"
|
|
143
|
+
aria-label={messages.wrapCode}
|
|
144
|
+
title={messages.wrapCode}
|
|
145
|
+
aria-pressed={wrap}
|
|
146
|
+
onClick={() => {
|
|
147
|
+
setAnimateSize(false)
|
|
148
|
+
setWrap(!wrap)
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<WrapText aria-hidden="true" />
|
|
152
|
+
</Button>
|
|
153
|
+
<CodeBlockCopyButton value={value} />
|
|
154
|
+
</CodeBlockActions>
|
|
155
|
+
</CodeBlockHeader>
|
|
156
|
+
<AnimatedCodeBlockContent
|
|
157
|
+
preProps={preProps}
|
|
158
|
+
initial={false}
|
|
159
|
+
animate={{
|
|
160
|
+
'--rdx-code-preview-height': `${previewHeight === null ? 0 : expanded ? fullHeight : previewHeight}px`,
|
|
161
|
+
}}
|
|
162
|
+
transition={{
|
|
163
|
+
duration: animateSize ? 0.28 : 0,
|
|
164
|
+
ease: [0.22, 1, 0.36, 1],
|
|
165
|
+
}}
|
|
166
|
+
id={contentId}
|
|
167
|
+
style={contentStyle}
|
|
168
|
+
wrap={wrap}
|
|
169
|
+
lineNumbers={lineNumbers}
|
|
170
|
+
startLine={startLine}
|
|
171
|
+
className={[
|
|
172
|
+
preProps.className,
|
|
173
|
+
'data-wrap:wrap-anywhere',
|
|
174
|
+
previewHeight !== null && 'overflow-y-hidden',
|
|
175
|
+
collapsed && 'scroll-fade-y',
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join(' ')}
|
|
179
|
+
>
|
|
180
|
+
{children}
|
|
181
|
+
</AnimatedCodeBlockContent>
|
|
182
|
+
{previewHeight !== null && (
|
|
183
|
+
<CodeBlockFooter className="justify-center md:hidden">
|
|
184
|
+
<Button
|
|
185
|
+
variant="ghost"
|
|
186
|
+
size="sm"
|
|
187
|
+
className="h-6 px-2 text-xs"
|
|
188
|
+
aria-expanded={expanded}
|
|
189
|
+
aria-controls={contentId}
|
|
190
|
+
onClick={() => {
|
|
191
|
+
setAnimateSize(
|
|
192
|
+
!window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
193
|
+
)
|
|
194
|
+
setExpanded(!expanded)
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{expanded ? messages.showLess : messages.showMore}
|
|
198
|
+
</Button>
|
|
199
|
+
</CodeBlockFooter>
|
|
200
|
+
)}
|
|
201
|
+
</CodeBlock>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
import { MdxFolderTree } from './mdx-folder-tree'
|
|
13
|
+
import { MdxMermaid } from './mdx-mermaid'
|
|
14
|
+
|
|
15
|
+
function textOf(children: ReactNode): string {
|
|
16
|
+
return Children.toArray(children)
|
|
17
|
+
.map((child) =>
|
|
18
|
+
typeof child === 'string' || typeof child === 'number'
|
|
19
|
+
? String(child)
|
|
20
|
+
: isValidElement<{ children?: ReactNode }>(child)
|
|
21
|
+
? textOf(child.props.children)
|
|
22
|
+
: ''
|
|
23
|
+
)
|
|
24
|
+
.join('')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Shadcn draws diff markers itself. Strip the original marker from highlighted
|
|
28
|
+
// tokens while keeping the original patch as the clipboard value.
|
|
29
|
+
function withoutMarker(children: ReactNode): ReactNode {
|
|
30
|
+
let remaining = 1
|
|
31
|
+
function strip(nodes: ReactNode): ReactNode {
|
|
32
|
+
return Children.map(nodes, (node) => {
|
|
33
|
+
if (!remaining) return node
|
|
34
|
+
if (typeof node === 'string') {
|
|
35
|
+
const value = node.slice(remaining)
|
|
36
|
+
remaining = Math.max(0, remaining - node.length)
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
return isValidElement<{ children?: ReactNode }>(node)
|
|
40
|
+
? cloneElement(node, {}, strip(node.props.children))
|
|
41
|
+
: node
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
return strip(children)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Translate Markdown fences to the complete shadcn Code Block composition. */
|
|
48
|
+
export function MdxCodeBlock({
|
|
49
|
+
lang,
|
|
50
|
+
children,
|
|
51
|
+
'data-rdx-code-mode': mode,
|
|
52
|
+
'data-rdx-code-title': title,
|
|
53
|
+
'data-rdx-code-line-numbers': numbered,
|
|
54
|
+
'data-rdx-code-start-line': firstLine,
|
|
55
|
+
...props
|
|
56
|
+
}: ComponentProps<'pre'> & {
|
|
57
|
+
'data-rdx-code-mode'?: 'folder' | 'diff' | 'mermaid'
|
|
58
|
+
'data-rdx-code-title'?: string
|
|
59
|
+
'data-rdx-code-line-numbers'?: string
|
|
60
|
+
'data-rdx-code-start-line'?: string
|
|
61
|
+
}) {
|
|
62
|
+
const messages = getMessages(lang)
|
|
63
|
+
const code = Children.toArray(children).find((child) => isValidElement(child))
|
|
64
|
+
const codeProps = isValidElement<{
|
|
65
|
+
className?: string
|
|
66
|
+
children?: ReactNode
|
|
67
|
+
}>(code)
|
|
68
|
+
? code.props
|
|
69
|
+
: undefined
|
|
70
|
+
const language = codeProps?.className?.match(/(?:^|\s)language-([^\s]+)/)?.[1]
|
|
71
|
+
const value = textOf(codeProps?.children ?? children)
|
|
72
|
+
if (mode === 'mermaid' || language?.toLowerCase() === 'mermaid') {
|
|
73
|
+
return (
|
|
74
|
+
<MdxMermaid
|
|
75
|
+
value={value}
|
|
76
|
+
title={title}
|
|
77
|
+
lineNumbers={numbered === 'true'}
|
|
78
|
+
startLine={firstLine === undefined ? undefined : Number(firstLine)}
|
|
79
|
+
preProps={props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
const label =
|
|
84
|
+
title ||
|
|
85
|
+
(mode === 'folder' ? messages.folderTree : (language ?? messages.code))
|
|
86
|
+
const lineNumbers = numbered === 'true'
|
|
87
|
+
const raw = codeProps?.children ?? children
|
|
88
|
+
const nodes = Children.toArray(raw)
|
|
89
|
+
type LineProps = {
|
|
90
|
+
children?: ReactNode
|
|
91
|
+
className?: string
|
|
92
|
+
'data-rdx-line-variant'?: string
|
|
93
|
+
}
|
|
94
|
+
const lines = nodes.filter(
|
|
95
|
+
(node) =>
|
|
96
|
+
isValidElement<LineProps>(node) &&
|
|
97
|
+
(node.props['data-rdx-line-variant'] ||
|
|
98
|
+
node.props.className?.split(/\s+/).includes('line'))
|
|
99
|
+
)
|
|
100
|
+
const lastLine = lines.at(-1)
|
|
101
|
+
const content =
|
|
102
|
+
mode === 'folder' ? (
|
|
103
|
+
<MdxFolderTree value={value} />
|
|
104
|
+
) : (mode === 'diff' || lineNumbers) && typeof raw !== 'string' ? (
|
|
105
|
+
nodes.map((node) => {
|
|
106
|
+
if (lineNumbers && node === '\n') return null
|
|
107
|
+
if (!isValidElement<LineProps>(node)) return node
|
|
108
|
+
const variant = node.props['data-rdx-line-variant']
|
|
109
|
+
if (
|
|
110
|
+
!variant &&
|
|
111
|
+
!(lineNumbers && node.props.className?.split(/\s+/).includes('line'))
|
|
112
|
+
)
|
|
113
|
+
return node
|
|
114
|
+
// A final newline terminates the last source line; intentional blank
|
|
115
|
+
// lines before it still receive the official component's own number.
|
|
116
|
+
if (
|
|
117
|
+
lineNumbers &&
|
|
118
|
+
lines.length > 1 &&
|
|
119
|
+
node === lastLine &&
|
|
120
|
+
value.endsWith('\n') &&
|
|
121
|
+
textOf(node.props.children) === ''
|
|
122
|
+
)
|
|
123
|
+
return null
|
|
124
|
+
return (
|
|
125
|
+
<CodeBlockLine
|
|
126
|
+
key={node.key}
|
|
127
|
+
variant={
|
|
128
|
+
variant === 'added' || variant === 'removed'
|
|
129
|
+
? variant
|
|
130
|
+
: variant === 'hunk'
|
|
131
|
+
? 'highlighted'
|
|
132
|
+
: 'default'
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
{variant === 'added' || variant === 'removed'
|
|
136
|
+
? withoutMarker(node.props.children)
|
|
137
|
+
: node.props.children}
|
|
138
|
+
</CodeBlockLine>
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
) : (
|
|
142
|
+
raw
|
|
143
|
+
)
|
|
144
|
+
return (
|
|
145
|
+
<MdxCodeBlockView
|
|
146
|
+
label={label}
|
|
147
|
+
mode={mode}
|
|
148
|
+
value={value}
|
|
149
|
+
lineNumbers={lineNumbers}
|
|
150
|
+
startLine={firstLine === undefined ? undefined : Number(firstLine)}
|
|
151
|
+
preProps={{
|
|
152
|
+
...props,
|
|
153
|
+
className: [props.className, language && `language-${language}`]
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.join(' '),
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{content}
|
|
159
|
+
</MdxCodeBlockView>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -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,13 @@ export const mdxComponents = {
|
|
|
76
104
|
{...props}
|
|
77
105
|
/>
|
|
78
106
|
),
|
|
79
|
-
a: ({ className, ...props }: ComponentProps<'a'>) => (
|
|
80
|
-
<a
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/>
|
|
107
|
+
a: ({ className, children, ...props }: ComponentProps<'a'>) => (
|
|
108
|
+
<a className={cn('underline text-rdx-highlighting', className)} {...props}>
|
|
109
|
+
{linkedImages(children)}
|
|
110
|
+
</a>
|
|
84
111
|
),
|
|
85
112
|
blockquote: Blockquote,
|
|
86
|
-
pre:
|
|
113
|
+
pre: MdxCodeBlock,
|
|
87
114
|
code: ({ className, ...props }: ComponentProps<'code'>) => (
|
|
88
115
|
<code
|
|
89
116
|
className={cn(
|
|
@@ -106,13 +133,7 @@ export const mdxComponents = {
|
|
|
106
133
|
hr: ({ className, ...props }: ComponentProps<'hr'>) => (
|
|
107
134
|
<hr className={cn('my-8 border-border', className)} {...props} />
|
|
108
135
|
),
|
|
109
|
-
img:
|
|
110
|
-
<img
|
|
111
|
-
alt={alt}
|
|
112
|
-
className={cn('max-w-full h-auto rounded-md my-4', className)}
|
|
113
|
-
{...props}
|
|
114
|
-
/>
|
|
115
|
-
),
|
|
136
|
+
img: MdxImage,
|
|
116
137
|
table: MdxTable,
|
|
117
138
|
thead: TableHeader,
|
|
118
139
|
tbody: TableBody,
|
|
@@ -123,6 +144,14 @@ export const mdxComponents = {
|
|
|
123
144
|
<TableHead scope={scope} className={className} {...props} />
|
|
124
145
|
),
|
|
125
146
|
td: TableCell,
|
|
147
|
+
CodeBlock,
|
|
148
|
+
CodeBlockActions,
|
|
149
|
+
CodeBlockContent,
|
|
150
|
+
CodeBlockCopyButton,
|
|
151
|
+
CodeBlockFooter,
|
|
152
|
+
CodeBlockHeader,
|
|
153
|
+
CodeBlockLine,
|
|
154
|
+
CodeBlockTitle,
|
|
126
155
|
Kbd,
|
|
127
156
|
KbdGroup,
|
|
128
157
|
Table,
|
|
@@ -158,7 +187,9 @@ export const mdxComponents = {
|
|
|
158
187
|
export function getMdxComponents(lang?: string) {
|
|
159
188
|
return {
|
|
160
189
|
...mdxComponents,
|
|
161
|
-
pre: (props: ComponentProps<'pre'>) =>
|
|
190
|
+
pre: (props: ComponentProps<'pre'>) => (
|
|
191
|
+
<MdxCodeBlock {...props} lang={lang} />
|
|
192
|
+
),
|
|
162
193
|
table: (props: ComponentProps<'table'>) => (
|
|
163
194
|
<MdxTable {...props} lang={lang} />
|
|
164
195
|
),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
File,
|
|
3
|
+
FileCode2,
|
|
4
|
+
FileImage,
|
|
5
|
+
FileJson,
|
|
6
|
+
FileText,
|
|
7
|
+
Folder,
|
|
8
|
+
} from 'lucide-react'
|
|
9
|
+
|
|
10
|
+
import { CodeBlockLine } from './code-block'
|
|
11
|
+
|
|
12
|
+
function entryIcon(name: string) {
|
|
13
|
+
if (name.endsWith('/')) return { Icon: Folder, kind: 'folder' }
|
|
14
|
+
const extension = name.includes('.')
|
|
15
|
+
? name.split('.').at(-1)?.toLowerCase()
|
|
16
|
+
: undefined
|
|
17
|
+
if (extension === 'json' || extension === 'jsonc')
|
|
18
|
+
return { Icon: FileJson, kind: 'json' }
|
|
19
|
+
if (extension && /^(?:png|jpe?g|gif|webp|svg|avif|ico|bmp)$/.test(extension))
|
|
20
|
+
return { Icon: FileImage, kind: 'image' }
|
|
21
|
+
if (extension && /^(?:mdx?|txt|log|rst)$/.test(extension))
|
|
22
|
+
return { Icon: FileText, kind: 'text' }
|
|
23
|
+
if (
|
|
24
|
+
extension &&
|
|
25
|
+
/^(?:[cm]?[jt]sx?|html|css|scss|vue|svelte|py|rb|go|rs|java|c|cpp|h|cs|php|sql|sh|bash|ya?ml|toml)$/.test(
|
|
26
|
+
extension
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
return { Icon: FileCode2, kind: 'code' }
|
|
30
|
+
return { Icon: File, kind: 'file' }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Decorate generated text with icons inside the official shadcn line component. */
|
|
34
|
+
export function MdxFolderTree({ value }: { value: string }) {
|
|
35
|
+
const trailingNewline = value.endsWith('\n')
|
|
36
|
+
const lines = value.replace(/\n$/, '').split('\n')
|
|
37
|
+
return lines.map((line, index) => {
|
|
38
|
+
const match = line.match(/^([ │\t]*(?:├──|└──)\s*)(.*)$/)
|
|
39
|
+
const prefix = match?.[1] ?? line.match(/^[ │\t]*/)?.[0] ?? ''
|
|
40
|
+
const name = match?.[2] ?? line.slice(prefix.length)
|
|
41
|
+
const { Icon, kind } = entryIcon(name)
|
|
42
|
+
return (
|
|
43
|
+
<CodeBlockLine key={index} data-rdx-folder-kind={name ? kind : undefined}>
|
|
44
|
+
{prefix && <span aria-hidden="true">{prefix}</span>}
|
|
45
|
+
{name && (
|
|
46
|
+
<>
|
|
47
|
+
<Icon
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
className="mr-1.5 inline-block size-3.5 align-[-2px] text-muted-foreground"
|
|
50
|
+
/>
|
|
51
|
+
{name}
|
|
52
|
+
</>
|
|
53
|
+
)}
|
|
54
|
+
{index < lines.length - 1 || trailingNewline ? '\n' : null}
|
|
55
|
+
</CodeBlockLine>
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
@@ -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
|
+
}
|