@mdxui/payload 6.0.1
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/.turbo/turbo-typecheck.log +5 -0
- package/CHANGELOG.md +136 -0
- package/package.json +58 -0
- package/src/app-preview/AppPreview.tsx +304 -0
- package/src/app-preview/index.ts +16 -0
- package/src/auth/AdminAuthWrapper.tsx +20 -0
- package/src/auth/AuthProvider.tsx +241 -0
- package/src/auth/AutoLogin.tsx +70 -0
- package/src/auth/index.ts +4 -0
- package/src/authkit/ApiKeys.tsx +77 -0
- package/src/authkit/UserProfile.tsx +77 -0
- package/src/authkit/UserSecurity.tsx +77 -0
- package/src/authkit/WorkOSProvider.tsx +70 -0
- package/src/authkit/index.ts +34 -0
- package/src/authkit/theme.ts +102 -0
- package/src/authkit/useWidgetToken.ts +67 -0
- package/src/dev-tools/Browser.tsx +364 -0
- package/src/dev-tools/DevTools.tsx +106 -0
- package/src/dev-tools/Terminal.tsx +216 -0
- package/src/dev-tools/index.ts +4 -0
- package/src/index.ts +39 -0
- package/src/mdx-preview/MDXPreview.tsx +183 -0
- package/src/mdx-preview/MDXProvider.tsx +62 -0
- package/src/mdx-preview/PayloadMDXField.tsx +120 -0
- package/src/mdx-preview/index.ts +28 -0
- package/src/mdx-preview/useMDXCompiler.ts +95 -0
- package/src/site-preview/PayloadSiteField.tsx +167 -0
- package/src/site-preview/SitePreview.tsx +194 -0
- package/src/site-preview/index.ts +31 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react'
|
|
4
|
+
|
|
5
|
+
interface TerminalProps {
|
|
6
|
+
/** Initial working directory */
|
|
7
|
+
cwd?: string
|
|
8
|
+
/** API endpoint base URL */
|
|
9
|
+
apiUrl?: string
|
|
10
|
+
/** Session ID for persistent sessions */
|
|
11
|
+
sessionId?: string
|
|
12
|
+
/** Height of the terminal */
|
|
13
|
+
height?: string | number
|
|
14
|
+
/** Called when command is executed */
|
|
15
|
+
onCommand?: (command: string, output: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TerminalLine {
|
|
19
|
+
type: 'input' | 'output' | 'error'
|
|
20
|
+
content: string
|
|
21
|
+
timestamp: Date
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Terminal: React.FC<TerminalProps> = ({
|
|
25
|
+
cwd = '~',
|
|
26
|
+
apiUrl = 'https://api.sb',
|
|
27
|
+
sessionId,
|
|
28
|
+
height = 400,
|
|
29
|
+
onCommand,
|
|
30
|
+
}) => {
|
|
31
|
+
const [lines, setLines] = useState<TerminalLine[]>([
|
|
32
|
+
{ type: 'output', content: `Connected to sandbox. Type 'help' for commands.`, timestamp: new Date() },
|
|
33
|
+
])
|
|
34
|
+
const [input, setInput] = useState('')
|
|
35
|
+
const [currentDir, _setCurrentDir] = useState(cwd)
|
|
36
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
37
|
+
const [history, setHistory] = useState<string[]>([])
|
|
38
|
+
const [historyIndex, setHistoryIndex] = useState(-1)
|
|
39
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
40
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
41
|
+
const [currentSessionId, setCurrentSessionId] = useState(sessionId)
|
|
42
|
+
|
|
43
|
+
// Auto-scroll to bottom
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (containerRef.current) {
|
|
46
|
+
containerRef.current.scrollTop = containerRef.current.scrollHeight
|
|
47
|
+
}
|
|
48
|
+
}, [lines])
|
|
49
|
+
|
|
50
|
+
// Focus input on click
|
|
51
|
+
const handleContainerClick = useCallback(() => {
|
|
52
|
+
inputRef.current?.focus()
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const executeCommand = useCallback(async (command: string) => {
|
|
56
|
+
if (!command.trim()) return
|
|
57
|
+
|
|
58
|
+
// Add to history
|
|
59
|
+
setHistory(prev => [...prev, command])
|
|
60
|
+
setHistoryIndex(-1)
|
|
61
|
+
|
|
62
|
+
// Show input line
|
|
63
|
+
setLines(prev => [...prev, { type: 'input', content: `${currentDir} $ ${command}`, timestamp: new Date() }])
|
|
64
|
+
setInput('')
|
|
65
|
+
setIsLoading(true)
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Handle built-in commands
|
|
69
|
+
if (command === 'clear') {
|
|
70
|
+
setLines([])
|
|
71
|
+
setIsLoading(false)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (command === 'help') {
|
|
76
|
+
setLines(prev => [...prev, {
|
|
77
|
+
type: 'output',
|
|
78
|
+
content: `Available commands:
|
|
79
|
+
clear - Clear terminal
|
|
80
|
+
help - Show this help
|
|
81
|
+
exit - Close session
|
|
82
|
+
|
|
83
|
+
All other commands are executed in the sandbox environment.`,
|
|
84
|
+
timestamp: new Date(),
|
|
85
|
+
}])
|
|
86
|
+
setIsLoading(false)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Execute via API
|
|
91
|
+
const response = await fetch(`${apiUrl}/rpc`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
method: 'executeSandbox',
|
|
96
|
+
params: {
|
|
97
|
+
sessionId: currentSessionId,
|
|
98
|
+
command,
|
|
99
|
+
cwd: currentDir,
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const result = await response.json()
|
|
105
|
+
|
|
106
|
+
if (result.error) {
|
|
107
|
+
setLines(prev => [...prev, { type: 'error', content: result.error.message || 'Command failed', timestamp: new Date() }])
|
|
108
|
+
} else {
|
|
109
|
+
const output = result.result?.stdout || result.result?.stderr || ''
|
|
110
|
+
if (output) {
|
|
111
|
+
setLines(prev => [...prev, { type: 'output', content: output, timestamp: new Date() }])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update session ID if new session was created
|
|
115
|
+
if (result.result?.sessionId && !currentSessionId) {
|
|
116
|
+
setCurrentSessionId(result.result.sessionId)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
onCommand?.(command, result.result?.stdout || '')
|
|
121
|
+
} catch (error) {
|
|
122
|
+
setLines(prev => [...prev, {
|
|
123
|
+
type: 'error',
|
|
124
|
+
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
125
|
+
timestamp: new Date()
|
|
126
|
+
}])
|
|
127
|
+
} finally {
|
|
128
|
+
setIsLoading(false)
|
|
129
|
+
}
|
|
130
|
+
}, [apiUrl, currentDir, currentSessionId, onCommand])
|
|
131
|
+
|
|
132
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
133
|
+
if (e.key === 'Enter' && !isLoading) {
|
|
134
|
+
executeCommand(input)
|
|
135
|
+
} else if (e.key === 'ArrowUp') {
|
|
136
|
+
e.preventDefault()
|
|
137
|
+
if (history.length > 0) {
|
|
138
|
+
const newIndex = historyIndex === -1 ? history.length - 1 : Math.max(0, historyIndex - 1)
|
|
139
|
+
setHistoryIndex(newIndex)
|
|
140
|
+
setInput(history[newIndex])
|
|
141
|
+
}
|
|
142
|
+
} else if (e.key === 'ArrowDown') {
|
|
143
|
+
e.preventDefault()
|
|
144
|
+
if (historyIndex !== -1) {
|
|
145
|
+
const newIndex = historyIndex + 1
|
|
146
|
+
if (newIndex >= history.length) {
|
|
147
|
+
setHistoryIndex(-1)
|
|
148
|
+
setInput('')
|
|
149
|
+
} else {
|
|
150
|
+
setHistoryIndex(newIndex)
|
|
151
|
+
setInput(history[newIndex])
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else if (e.key === 'c' && e.ctrlKey) {
|
|
155
|
+
setInput('')
|
|
156
|
+
setLines(prev => [...prev, { type: 'input', content: `${currentDir} $ ^C`, timestamp: new Date() }])
|
|
157
|
+
}
|
|
158
|
+
}, [input, isLoading, executeCommand, history, historyIndex, currentDir])
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
ref={containerRef}
|
|
163
|
+
onClick={handleContainerClick}
|
|
164
|
+
style={{
|
|
165
|
+
backgroundColor: '#1e1e1e',
|
|
166
|
+
color: '#d4d4d4',
|
|
167
|
+
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
|
|
168
|
+
fontSize: '13px',
|
|
169
|
+
lineHeight: '1.5',
|
|
170
|
+
padding: '12px',
|
|
171
|
+
borderRadius: '8px',
|
|
172
|
+
height: typeof height === 'number' ? `${height}px` : height,
|
|
173
|
+
overflowY: 'auto',
|
|
174
|
+
cursor: 'text',
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
{lines.map((line, i) => (
|
|
178
|
+
<div
|
|
179
|
+
key={i}
|
|
180
|
+
style={{
|
|
181
|
+
color: line.type === 'error' ? '#f14c4c' : line.type === 'input' ? '#569cd6' : '#d4d4d4',
|
|
182
|
+
whiteSpace: 'pre-wrap',
|
|
183
|
+
wordBreak: 'break-all',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{line.content}
|
|
187
|
+
</div>
|
|
188
|
+
))}
|
|
189
|
+
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
190
|
+
<span style={{ color: '#569cd6' }}>{currentDir} $ </span>
|
|
191
|
+
<input
|
|
192
|
+
ref={inputRef}
|
|
193
|
+
type="text"
|
|
194
|
+
value={input}
|
|
195
|
+
onChange={(e) => setInput(e.target.value)}
|
|
196
|
+
onKeyDown={handleKeyDown}
|
|
197
|
+
disabled={isLoading}
|
|
198
|
+
autoFocus
|
|
199
|
+
style={{
|
|
200
|
+
backgroundColor: 'transparent',
|
|
201
|
+
border: 'none',
|
|
202
|
+
color: '#d4d4d4',
|
|
203
|
+
fontFamily: 'inherit',
|
|
204
|
+
fontSize: 'inherit',
|
|
205
|
+
outline: 'none',
|
|
206
|
+
flex: 1,
|
|
207
|
+
caretColor: '#d4d4d4',
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
{isLoading && <span style={{ color: '#6a9955' }}>⏳</span>}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default Terminal
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/payload - Payload CMS admin components and field UI
|
|
3
|
+
*
|
|
4
|
+
* This package provides components for extending Payload CMS admin UI:
|
|
5
|
+
* - Authentication wrappers and auto-login
|
|
6
|
+
* - WorkOS AuthKit integration widgets
|
|
7
|
+
* - Dev tools (Terminal, Browser)
|
|
8
|
+
* - MDX preview components
|
|
9
|
+
* - Site and App preview components
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* // Auth components
|
|
14
|
+
* import { AdminAuthWrapper, useAuth } from '@mdxui/payload/auth'
|
|
15
|
+
*
|
|
16
|
+
* // WorkOS AuthKit widgets
|
|
17
|
+
* import { UserProfile, UserSecurity, ApiKeys } from '@mdxui/payload/authkit'
|
|
18
|
+
*
|
|
19
|
+
* // Dev tools
|
|
20
|
+
* import { DevTools, Terminal, Browser } from '@mdxui/payload/dev-tools'
|
|
21
|
+
*
|
|
22
|
+
* // MDX preview
|
|
23
|
+
* import { MDXPreview, MDXProvider } from '@mdxui/payload/mdx-preview'
|
|
24
|
+
*
|
|
25
|
+
* // Site preview (uses @mdxui/beacon components)
|
|
26
|
+
* import { SitePreview, PayloadSiteField } from '@mdxui/payload/site-preview'
|
|
27
|
+
*
|
|
28
|
+
* // App preview (uses @mdxui/cockpit components)
|
|
29
|
+
* import { AppPreview, createAppPreviewField } from '@mdxui/payload/app-preview'
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Re-export from submodules for convenience
|
|
34
|
+
export * from './auth'
|
|
35
|
+
export * from './authkit'
|
|
36
|
+
export * from './dev-tools'
|
|
37
|
+
export * from './mdx-preview'
|
|
38
|
+
export * from './site-preview'
|
|
39
|
+
export * from './app-preview'
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from 'react'
|
|
4
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
5
|
+
import { useMDXCompiler } from './useMDXCompiler'
|
|
6
|
+
import { useMDXComponents } from './MDXProvider'
|
|
7
|
+
import type { MDXComponents } from './MDXProvider'
|
|
8
|
+
|
|
9
|
+
export interface MDXPreviewProps {
|
|
10
|
+
/** MDX source code to render */
|
|
11
|
+
source: string
|
|
12
|
+
/** Component overrides (merged with provider components) */
|
|
13
|
+
components?: MDXComponents
|
|
14
|
+
/** Debounce delay in milliseconds (default: 300) */
|
|
15
|
+
debounceMs?: number
|
|
16
|
+
/** Custom error fallback component */
|
|
17
|
+
errorFallback?: React.ComponentType<{ error: Error; resetErrorBoundary: () => void }>
|
|
18
|
+
/** Custom loading component */
|
|
19
|
+
loadingComponent?: React.ReactNode
|
|
20
|
+
/** Whether to show compilation status (default: false) */
|
|
21
|
+
showStatus?: boolean
|
|
22
|
+
/** Development mode (default: true) */
|
|
23
|
+
development?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default error fallback component
|
|
28
|
+
*/
|
|
29
|
+
function DefaultErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
style={{
|
|
33
|
+
padding: '1rem',
|
|
34
|
+
backgroundColor: '#fee',
|
|
35
|
+
border: '1px solid #c00',
|
|
36
|
+
borderRadius: '0.25rem',
|
|
37
|
+
fontFamily: 'monospace',
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<h3 style={{ margin: '0 0 0.5rem 0', color: '#c00' }}>
|
|
41
|
+
MDX Compilation Error
|
|
42
|
+
</h3>
|
|
43
|
+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
44
|
+
{error.message}
|
|
45
|
+
</pre>
|
|
46
|
+
<button
|
|
47
|
+
onClick={resetErrorBoundary}
|
|
48
|
+
style={{
|
|
49
|
+
marginTop: '0.5rem',
|
|
50
|
+
padding: '0.25rem 0.5rem',
|
|
51
|
+
backgroundColor: '#fff',
|
|
52
|
+
border: '1px solid #c00',
|
|
53
|
+
borderRadius: '0.25rem',
|
|
54
|
+
cursor: 'pointer',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
Retry
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Default loading component
|
|
65
|
+
*/
|
|
66
|
+
function DefaultLoadingComponent() {
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
style={{
|
|
70
|
+
padding: '1rem',
|
|
71
|
+
color: '#666',
|
|
72
|
+
fontStyle: 'italic',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
Compiling MDX...
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* MDX live preview component that renders MDX source in real-time.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* import { MDXPreview } from '@mdxui/payload/mdx-preview'
|
|
86
|
+
* import { Hero, Features } from '@mdxui/beacon'
|
|
87
|
+
*
|
|
88
|
+
* <MDXPreview
|
|
89
|
+
* source={mdxSource}
|
|
90
|
+
* components={{ Hero, Features }}
|
|
91
|
+
* debounceMs={300}
|
|
92
|
+
* />
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function MDXPreview({
|
|
96
|
+
source,
|
|
97
|
+
components: propsComponents = {},
|
|
98
|
+
debounceMs = 300,
|
|
99
|
+
errorFallback: ErrorFallback = DefaultErrorFallback,
|
|
100
|
+
loadingComponent = <DefaultLoadingComponent />,
|
|
101
|
+
showStatus = false,
|
|
102
|
+
development = true,
|
|
103
|
+
}: MDXPreviewProps) {
|
|
104
|
+
const contextComponents = useMDXComponents()
|
|
105
|
+
const { code, error: compileError, isCompiling } = useMDXCompiler({
|
|
106
|
+
source,
|
|
107
|
+
debounceMs,
|
|
108
|
+
development,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Merge components from context and props
|
|
112
|
+
const mergedComponents = useMemo(
|
|
113
|
+
() => ({ ...contextComponents, ...propsComponents }),
|
|
114
|
+
[contextComponents, propsComponents]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Render compilation error
|
|
118
|
+
if (compileError) {
|
|
119
|
+
return <ErrorFallback error={compileError} resetErrorBoundary={() => {}} />
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Render loading state
|
|
123
|
+
if (isCompiling) {
|
|
124
|
+
return <>{loadingComponent}</>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Render empty state
|
|
128
|
+
if (!code) {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Render compiled MDX
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
{showStatus && (
|
|
136
|
+
<div
|
|
137
|
+
style={{
|
|
138
|
+
padding: '0.25rem 0.5rem',
|
|
139
|
+
fontSize: '0.75rem',
|
|
140
|
+
color: '#666',
|
|
141
|
+
borderBottom: '1px solid #e0e0e0',
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{isCompiling ? 'Compiling...' : 'Ready'}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
148
|
+
<MDXContent code={code} components={mergedComponents} />
|
|
149
|
+
</ErrorBoundary>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Component that executes and renders compiled MDX code
|
|
156
|
+
*/
|
|
157
|
+
function MDXContent({ code, components }: { code: string; components: MDXComponents }) {
|
|
158
|
+
const Content = useMemo(() => {
|
|
159
|
+
try {
|
|
160
|
+
// Create a function from the compiled code
|
|
161
|
+
// The compiled code expects certain globals to be available
|
|
162
|
+
const fn = new Function(
|
|
163
|
+
'React',
|
|
164
|
+
...Object.keys(components),
|
|
165
|
+
code
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
// Execute the function with React and components as arguments
|
|
169
|
+
const { default: MDXComponent } = fn(React, ...Object.values(components))
|
|
170
|
+
|
|
171
|
+
return MDXComponent
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Error executing MDX code:', error)
|
|
174
|
+
throw error
|
|
175
|
+
}
|
|
176
|
+
}, [code, components])
|
|
177
|
+
|
|
178
|
+
if (!Content) {
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return <Content components={components} />
|
|
183
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useMemo } from 'react'
|
|
4
|
+
import { MDXProvider as BaseMDXProvider } from '@mdx-js/react'
|
|
5
|
+
import type { SiteComponents, AppComponents } from 'mdxui'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Combined components type that accepts both Site and App components
|
|
9
|
+
*/
|
|
10
|
+
export type MDXComponents = Partial<SiteComponents & AppComponents> & {
|
|
11
|
+
// Allow additional custom components
|
|
12
|
+
[key: string]: React.ComponentType<any>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MDXContextValue {
|
|
16
|
+
components: MDXComponents
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MDXContext = createContext<MDXContextValue | null>(null)
|
|
20
|
+
|
|
21
|
+
export interface MDXProviderProps {
|
|
22
|
+
/** Component overrides for MDX rendering */
|
|
23
|
+
components?: MDXComponents
|
|
24
|
+
/** Child elements */
|
|
25
|
+
children: React.ReactNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Provider for MDX components.
|
|
30
|
+
* Wraps the application and provides component overrides to MDX content.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* import { Hero, Features } from '@mdxui/beacon'
|
|
35
|
+
*
|
|
36
|
+
* <MDXProvider components={{ Hero, Features }}>
|
|
37
|
+
* <MDXPreview source={mdxSource} />
|
|
38
|
+
* </MDXProvider>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function MDXProvider({ components = {}, children }: MDXProviderProps) {
|
|
42
|
+
const value = useMemo(() => ({ components }), [components])
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<MDXContext.Provider value={value}>
|
|
46
|
+
<BaseMDXProvider components={components}>
|
|
47
|
+
{children}
|
|
48
|
+
</BaseMDXProvider>
|
|
49
|
+
</MDXContext.Provider>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hook to access MDX components from context.
|
|
55
|
+
*/
|
|
56
|
+
export function useMDXComponents(): MDXComponents {
|
|
57
|
+
const context = useContext(MDXContext)
|
|
58
|
+
if (!context) {
|
|
59
|
+
return {}
|
|
60
|
+
}
|
|
61
|
+
return context.components
|
|
62
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useField } from '@payloadcms/ui'
|
|
4
|
+
import { MDXPreview } from './MDXPreview'
|
|
5
|
+
import type { MDXComponents } from './MDXProvider'
|
|
6
|
+
|
|
7
|
+
export interface PayloadMDXFieldProps {
|
|
8
|
+
/** Field path in Payload document */
|
|
9
|
+
path: string
|
|
10
|
+
/** Component overrides for MDX rendering */
|
|
11
|
+
components?: MDXComponents
|
|
12
|
+
/** Whether to show the preview (default: true) */
|
|
13
|
+
showPreview?: boolean
|
|
14
|
+
/** Custom label for the preview section */
|
|
15
|
+
previewLabel?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Payload CMS custom field component with live MDX preview.
|
|
20
|
+
*
|
|
21
|
+
* Use this as a custom component in your Payload field configuration.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // In your Payload collection config:
|
|
26
|
+
* {
|
|
27
|
+
* name: 'content',
|
|
28
|
+
* type: 'textarea',
|
|
29
|
+
* admin: {
|
|
30
|
+
* components: {
|
|
31
|
+
* Field: (props) => (
|
|
32
|
+
* <PayloadMDXField
|
|
33
|
+
* {...props}
|
|
34
|
+
* components={{ Hero, Features }}
|
|
35
|
+
* />
|
|
36
|
+
* ),
|
|
37
|
+
* },
|
|
38
|
+
* },
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function PayloadMDXField({
|
|
43
|
+
path,
|
|
44
|
+
components,
|
|
45
|
+
showPreview = true,
|
|
46
|
+
previewLabel = 'Preview',
|
|
47
|
+
}: PayloadMDXFieldProps) {
|
|
48
|
+
const { value } = useField<string>({ path })
|
|
49
|
+
|
|
50
|
+
if (!showPreview || !value) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
style={{
|
|
57
|
+
marginTop: '1rem',
|
|
58
|
+
border: '1px solid #e0e0e0',
|
|
59
|
+
borderRadius: '0.25rem',
|
|
60
|
+
overflow: 'hidden',
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
style={{
|
|
65
|
+
padding: '0.5rem 1rem',
|
|
66
|
+
backgroundColor: '#f5f5f5',
|
|
67
|
+
borderBottom: '1px solid #e0e0e0',
|
|
68
|
+
fontWeight: 600,
|
|
69
|
+
fontSize: '0.875rem',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{previewLabel}
|
|
73
|
+
</div>
|
|
74
|
+
<div
|
|
75
|
+
style={{
|
|
76
|
+
padding: '1rem',
|
|
77
|
+
backgroundColor: '#fff',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<MDXPreview
|
|
81
|
+
source={value}
|
|
82
|
+
components={components}
|
|
83
|
+
showStatus={true}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Factory function to create a Payload field component with preset components.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* import { Hero, Features } from '@mdxui/beacon'
|
|
96
|
+
*
|
|
97
|
+
* const SiteMDXField = createPayloadMDXField({
|
|
98
|
+
* components: { Hero, Features },
|
|
99
|
+
* previewLabel: 'Site Preview',
|
|
100
|
+
* })
|
|
101
|
+
*
|
|
102
|
+
* // Use in collection config:
|
|
103
|
+
* {
|
|
104
|
+
* name: 'content',
|
|
105
|
+
* type: 'textarea',
|
|
106
|
+
* admin: {
|
|
107
|
+
* components: {
|
|
108
|
+
* Field: SiteMDXField,
|
|
109
|
+
* },
|
|
110
|
+
* },
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export function createPayloadMDXField(
|
|
115
|
+
defaultProps: Omit<PayloadMDXFieldProps, 'path'>
|
|
116
|
+
) {
|
|
117
|
+
return function PayloadMDXFieldComponent(props: { path: string }) {
|
|
118
|
+
return <PayloadMDXField {...defaultProps} {...props} />
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MDX Live Preview Components
|
|
3
|
+
*
|
|
4
|
+
* Real-time MDX rendering for Payload CMS admin panel.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { MDXProvider, MDXPreview } from '@mdxui/payload/mdx-preview'
|
|
9
|
+
* import { Hero, Features } from '@mdxui/beacon'
|
|
10
|
+
*
|
|
11
|
+
* // Wrap your app with MDXProvider
|
|
12
|
+
* <MDXProvider components={{ Hero, Features }}>
|
|
13
|
+
* <MDXPreview source={mdxSource} />
|
|
14
|
+
* </MDXProvider>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export { MDXPreview } from './MDXPreview'
|
|
19
|
+
export type { MDXPreviewProps } from './MDXPreview'
|
|
20
|
+
|
|
21
|
+
export { MDXProvider, useMDXComponents } from './MDXProvider'
|
|
22
|
+
export type { MDXProviderProps, MDXComponents } from './MDXProvider'
|
|
23
|
+
|
|
24
|
+
export { useMDXCompiler } from './useMDXCompiler'
|
|
25
|
+
export type { MDXCompilerOptions, MDXCompilerResult } from './useMDXCompiler'
|
|
26
|
+
|
|
27
|
+
export { PayloadMDXField, createPayloadMDXField } from './PayloadMDXField'
|
|
28
|
+
export type { PayloadMDXFieldProps } from './PayloadMDXField'
|