@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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * AuthKit Theme Configuration
3
+ *
4
+ * Configures @mdxui/auth widgets theme to match Payload CMS admin styling.
5
+ * Uses Radix Themes API for customization.
6
+ */
7
+
8
+ /**
9
+ * Theme configuration for WorkOS widgets
10
+ * Matches Payload CMS admin aesthetic
11
+ */
12
+ export const payloadAuthKitTheme = {
13
+ /**
14
+ * Appearance mode - 'light', 'dark', or 'inherit'
15
+ * 'inherit' follows system/Payload dark mode setting
16
+ */
17
+ appearance: 'inherit' as const,
18
+
19
+ /**
20
+ * Accent color for interactive elements
21
+ * Payload uses a blue accent by default
22
+ */
23
+ accentColor: 'blue' as const,
24
+
25
+ /**
26
+ * Border radius for UI elements
27
+ * 'medium' matches Payload's rounded corners
28
+ */
29
+ radius: 'medium' as const,
30
+
31
+ /**
32
+ * Gray color palette
33
+ * 'slate' is close to Payload's neutral grays
34
+ */
35
+ grayColor: 'slate' as const,
36
+
37
+ /**
38
+ * Scaling factor for UI elements
39
+ * '100%' maintains standard sizing
40
+ */
41
+ scaling: '100%' as const,
42
+ }
43
+
44
+ /**
45
+ * CSS custom properties to inject for additional theming
46
+ * These override Radix Themes defaults to match Payload more closely
47
+ */
48
+ export const customThemeStyles = `
49
+ /* Match Payload's font stack */
50
+ .workos-widgets {
51
+ font-family: var(--font-body, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif);
52
+ }
53
+
54
+ /* Adjust card backgrounds for Payload dark mode */
55
+ [data-theme="dark"] .workos-widgets {
56
+ --color-background: var(--theme-elevation-100, #1a1a1a);
57
+ --color-surface: var(--theme-elevation-200, #242424);
58
+ --color-panel-solid: var(--theme-elevation-150, #1f1f1f);
59
+ }
60
+
61
+ /* Match Payload's button styling */
62
+ .workos-widgets button[data-accent-color] {
63
+ font-weight: 500;
64
+ transition: all 150ms ease;
65
+ }
66
+
67
+ /* Ensure proper contrast in light mode */
68
+ [data-theme="light"] .workos-widgets {
69
+ --color-background: var(--theme-elevation-0, #ffffff);
70
+ --color-surface: var(--theme-elevation-50, #f7f7f7);
71
+ }
72
+
73
+ /* Match Payload's input field styling */
74
+ .workos-widgets input,
75
+ .workos-widgets select,
76
+ .workos-widgets textarea {
77
+ border-color: var(--theme-elevation-300, #e0e0e0);
78
+ }
79
+
80
+ /* Widget container max-width for better readability */
81
+ .workos-widget-container {
82
+ max-width: 600px;
83
+ margin: 0 auto;
84
+ padding: 2rem;
85
+ }
86
+ `
87
+
88
+ /**
89
+ * Inject custom theme styles into document
90
+ * Call this in your provider component
91
+ */
92
+ export function injectThemeStyles() {
93
+ if (typeof document === 'undefined') return
94
+
95
+ const styleId = 'workos-widgets-custom-theme'
96
+ if (document.getElementById(styleId)) return
97
+
98
+ const style = document.createElement('style')
99
+ style.id = styleId
100
+ style.textContent = customThemeStyles
101
+ document.head.appendChild(style)
102
+ }
@@ -0,0 +1,67 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useCallback } from 'react'
4
+
5
+ interface UseWidgetTokenResult {
6
+ token: string | null
7
+ loading: boolean
8
+ error: string | null
9
+ refetch: () => Promise<void>
10
+ }
11
+
12
+ /**
13
+ * Hook to fetch authorization tokens for WorkOS widgets
14
+ *
15
+ * @param widget - The widget type (user-profile, user-security, etc.)
16
+ * @param organizationId - Optional organization context
17
+ * @returns Token state and refetch function
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * const { token, loading, error } = useWidgetToken('user-profile')
22
+ * if (loading) return <Spinner />
23
+ * if (error) return <Error message={error} />
24
+ * return <WorkOSWidget authToken={token} />
25
+ * ```
26
+ */
27
+ export function useWidgetToken(
28
+ widget: string,
29
+ organizationId?: string
30
+ ): UseWidgetTokenResult {
31
+ const [token, setToken] = useState<string | null>(null)
32
+ const [loading, setLoading] = useState(true)
33
+ const [error, setError] = useState<string | null>(null)
34
+
35
+ const fetchToken = useCallback(async () => {
36
+ try {
37
+ setLoading(true)
38
+ setError(null)
39
+
40
+ const params = new URLSearchParams({ widget })
41
+ if (organizationId) {
42
+ params.set('organizationId', organizationId)
43
+ }
44
+
45
+ const response = await fetch(`/api/workos/widget-token?${params}`)
46
+ const data = await response.json()
47
+
48
+ if (!response.ok) {
49
+ throw new Error(data.error || 'Failed to fetch token')
50
+ }
51
+
52
+ setToken(data.token)
53
+ } catch (err) {
54
+ setError(err instanceof Error ? err.message : 'Token fetch failed')
55
+ } finally {
56
+ setLoading(false)
57
+ }
58
+ }, [widget, organizationId])
59
+
60
+ useEffect(() => {
61
+ fetchToken()
62
+ }, [fetchToken])
63
+
64
+ return { token, loading, error, refetch: fetchToken }
65
+ }
66
+
67
+ export default useWidgetToken
@@ -0,0 +1,364 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useCallback, useRef } from 'react'
4
+
5
+ interface BrowserProps {
6
+ /** Initial URL to load */
7
+ initialUrl?: string
8
+ /** API endpoint base URL */
9
+ apiUrl?: string
10
+ /** Height of the browser viewport */
11
+ height?: string | number
12
+ /** Width of the browser viewport */
13
+ width?: string | number
14
+ /** Show URL bar */
15
+ showUrlBar?: boolean
16
+ /** Show controls (back, forward, refresh) */
17
+ showControls?: boolean
18
+ /** Called when URL changes */
19
+ onNavigate?: (url: string) => void
20
+ /** Called when screenshot is taken */
21
+ onScreenshot?: (imageData: string) => void
22
+ }
23
+
24
+ type SessionStatus = 'IDLE' | 'CONNECTING' | 'RUNNING' | 'COMPLETED' | 'ERROR' | 'TIMED_OUT'
25
+
26
+ export const Browser: React.FC<BrowserProps> = ({
27
+ initialUrl = 'https://example.com.ai',
28
+ apiUrl = 'https://api.sb',
29
+ height = 600,
30
+ width = '100%',
31
+ showUrlBar = true,
32
+ showControls = true,
33
+ onNavigate,
34
+ onScreenshot,
35
+ }) => {
36
+ const [url, setUrl] = useState(initialUrl)
37
+ const [inputUrl, setInputUrl] = useState(initialUrl)
38
+ const [sessionId, setSessionId] = useState<string | null>(null)
39
+ const [status, setStatus] = useState<SessionStatus>('IDLE')
40
+ const [screenshot, setScreenshot] = useState<string | null>(null)
41
+ const [error, setError] = useState<string | null>(null)
42
+ const [connectUrl, setConnectUrl] = useState<string | null>(null)
43
+ const iframeRef = useRef<HTMLIFrameElement>(null)
44
+
45
+ // Create browser session
46
+ const createSession = useCallback(async () => {
47
+ setStatus('CONNECTING')
48
+ setError(null)
49
+
50
+ try {
51
+ const response = await fetch(`${apiUrl}/rpc`, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({
55
+ method: 'createBrowserSession',
56
+ params: {
57
+ browserSettings: {
58
+ viewport: { width: 1280, height: 720 },
59
+ },
60
+ keepAlive: true,
61
+ },
62
+ }),
63
+ })
64
+
65
+ const result = await response.json()
66
+
67
+ if (result.error) {
68
+ throw new Error(result.error.message || 'Failed to create session')
69
+ }
70
+
71
+ setSessionId(result.result.id)
72
+ setStatus('RUNNING')
73
+
74
+ // Get connect URL for live view
75
+ const connectResponse = await fetch(`${apiUrl}/rpc`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({
79
+ method: 'getBrowserConnectUrl',
80
+ params: { sessionId: result.result.id },
81
+ }),
82
+ })
83
+
84
+ const connectResult = await connectResponse.json()
85
+ if (connectResult.result?.url) {
86
+ setConnectUrl(connectResult.result.url)
87
+ }
88
+
89
+ return result.result.id
90
+ } catch (err) {
91
+ setError(err instanceof Error ? err.message : 'Unknown error')
92
+ setStatus('ERROR')
93
+ return null
94
+ }
95
+ }, [apiUrl])
96
+
97
+ // Take screenshot
98
+ const takeScreenshot = useCallback(async () => {
99
+ if (!sessionId) return
100
+
101
+ try {
102
+ const response = await fetch(`${apiUrl}/rpc`, {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify({
106
+ method: 'runBrowserAction',
107
+ params: {
108
+ sessionId,
109
+ action: 'screenshot',
110
+ url,
111
+ },
112
+ }),
113
+ })
114
+
115
+ const result = await response.json()
116
+
117
+ if (result.result?.data) {
118
+ setScreenshot(result.result.data)
119
+ onScreenshot?.(result.result.data)
120
+ }
121
+ } catch (err) {
122
+ setError(err instanceof Error ? err.message : 'Screenshot failed')
123
+ }
124
+ }, [apiUrl, sessionId, url, onScreenshot])
125
+
126
+ // Navigate to URL
127
+ const navigate = useCallback(async (targetUrl: string) => {
128
+ if (!sessionId) {
129
+ const newSessionId = await createSession()
130
+ if (!newSessionId) return
131
+ }
132
+
133
+ setUrl(targetUrl)
134
+ setInputUrl(targetUrl)
135
+ onNavigate?.(targetUrl)
136
+
137
+ // Take screenshot after navigation
138
+ setTimeout(() => takeScreenshot(), 2000)
139
+ }, [sessionId, createSession, onNavigate, takeScreenshot])
140
+
141
+ const handleUrlSubmit = useCallback((e: React.FormEvent) => {
142
+ e.preventDefault()
143
+ let targetUrl = inputUrl
144
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
145
+ targetUrl = 'https://' + targetUrl
146
+ }
147
+ navigate(targetUrl)
148
+ }, [inputUrl, navigate])
149
+
150
+ const handleRefresh = useCallback(() => {
151
+ takeScreenshot()
152
+ }, [takeScreenshot])
153
+
154
+ // Initialize session on mount
155
+ useEffect(() => {
156
+ if (status === 'IDLE') {
157
+ createSession().then((id) => {
158
+ if (id) {
159
+ setTimeout(() => takeScreenshot(), 1000)
160
+ }
161
+ })
162
+ }
163
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
164
+
165
+ return (
166
+ <div
167
+ style={{
168
+ backgroundColor: '#2d2d2d',
169
+ borderRadius: '8px',
170
+ overflow: 'hidden',
171
+ width: typeof width === 'number' ? `${width}px` : width,
172
+ }}
173
+ >
174
+ {/* Browser Chrome */}
175
+ {(showUrlBar || showControls) && (
176
+ <div
177
+ style={{
178
+ backgroundColor: '#3c3c3c',
179
+ padding: '8px 12px',
180
+ display: 'flex',
181
+ alignItems: 'center',
182
+ gap: '8px',
183
+ borderBottom: '1px solid #1e1e1e',
184
+ }}
185
+ >
186
+ {/* Traffic lights */}
187
+ <div style={{ display: 'flex', gap: '6px' }}>
188
+ <div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#ff5f57' }} />
189
+ <div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#febc2e' }} />
190
+ <div style={{ width: 12, height: 12, borderRadius: '50%', backgroundColor: '#28c840' }} />
191
+ </div>
192
+
193
+ {/* Controls */}
194
+ {showControls && (
195
+ <div style={{ display: 'flex', gap: '4px', marginLeft: '8px' }}>
196
+ <button
197
+ onClick={handleRefresh}
198
+ disabled={status !== 'RUNNING'}
199
+ style={{
200
+ background: 'none',
201
+ border: 'none',
202
+ color: '#888',
203
+ cursor: 'pointer',
204
+ padding: '4px 8px',
205
+ borderRadius: '4px',
206
+ fontSize: '14px',
207
+ }}
208
+ title="Refresh"
209
+ >
210
+
211
+ </button>
212
+ </div>
213
+ )}
214
+
215
+ {/* URL Bar */}
216
+ {showUrlBar && (
217
+ <form onSubmit={handleUrlSubmit} style={{ flex: 1 }}>
218
+ <input
219
+ type="text"
220
+ value={inputUrl}
221
+ onChange={(e) => setInputUrl(e.target.value)}
222
+ placeholder="Enter URL..."
223
+ style={{
224
+ width: '100%',
225
+ backgroundColor: '#1e1e1e',
226
+ border: '1px solid #4a4a4a',
227
+ borderRadius: '6px',
228
+ color: '#d4d4d4',
229
+ padding: '6px 12px',
230
+ fontSize: '13px',
231
+ outline: 'none',
232
+ }}
233
+ />
234
+ </form>
235
+ )}
236
+
237
+ {/* Status indicator */}
238
+ <div
239
+ style={{
240
+ width: 8,
241
+ height: 8,
242
+ borderRadius: '50%',
243
+ backgroundColor:
244
+ status === 'RUNNING' ? '#28c840' :
245
+ status === 'CONNECTING' ? '#febc2e' :
246
+ status === 'ERROR' ? '#ff5f57' :
247
+ '#888',
248
+ }}
249
+ title={status}
250
+ />
251
+ </div>
252
+ )}
253
+
254
+ {/* Browser Viewport */}
255
+ <div
256
+ style={{
257
+ height: typeof height === 'number' ? `${height}px` : height,
258
+ backgroundColor: '#fff',
259
+ position: 'relative',
260
+ overflow: 'hidden',
261
+ }}
262
+ >
263
+ {status === 'CONNECTING' && (
264
+ <div
265
+ style={{
266
+ position: 'absolute',
267
+ inset: 0,
268
+ display: 'flex',
269
+ alignItems: 'center',
270
+ justifyContent: 'center',
271
+ backgroundColor: '#1e1e1e',
272
+ color: '#d4d4d4',
273
+ }}
274
+ >
275
+ <div style={{ textAlign: 'center' }}>
276
+ <div style={{ fontSize: '24px', marginBottom: '8px' }}>🌐</div>
277
+ <div>Starting browser session...</div>
278
+ </div>
279
+ </div>
280
+ )}
281
+
282
+ {error && (
283
+ <div
284
+ style={{
285
+ position: 'absolute',
286
+ inset: 0,
287
+ display: 'flex',
288
+ alignItems: 'center',
289
+ justifyContent: 'center',
290
+ backgroundColor: '#1e1e1e',
291
+ color: '#f14c4c',
292
+ }}
293
+ >
294
+ <div style={{ textAlign: 'center' }}>
295
+ <div style={{ fontSize: '24px', marginBottom: '8px' }}>⚠️</div>
296
+ <div>{error}</div>
297
+ <button
298
+ onClick={() => {
299
+ setError(null)
300
+ setStatus('IDLE')
301
+ createSession()
302
+ }}
303
+ style={{
304
+ marginTop: '12px',
305
+ padding: '8px 16px',
306
+ backgroundColor: '#4a4a4a',
307
+ border: 'none',
308
+ borderRadius: '4px',
309
+ color: '#d4d4d4',
310
+ cursor: 'pointer',
311
+ }}
312
+ >
313
+ Retry
314
+ </button>
315
+ </div>
316
+ </div>
317
+ )}
318
+
319
+ {screenshot && status === 'RUNNING' && (
320
+ <img
321
+ src={`data:image/png;base64,${screenshot}`}
322
+ alt="Browser screenshot"
323
+ style={{
324
+ width: '100%',
325
+ height: '100%',
326
+ objectFit: 'contain',
327
+ }}
328
+ />
329
+ )}
330
+
331
+ {connectUrl && status === 'RUNNING' && !screenshot && (
332
+ <iframe
333
+ ref={iframeRef}
334
+ src={connectUrl}
335
+ style={{
336
+ width: '100%',
337
+ height: '100%',
338
+ border: 'none',
339
+ }}
340
+ title="Browser Session"
341
+ sandbox="allow-scripts allow-same-origin"
342
+ />
343
+ )}
344
+ </div>
345
+
346
+ {/* Status bar */}
347
+ <div
348
+ style={{
349
+ backgroundColor: '#3c3c3c',
350
+ padding: '4px 12px',
351
+ fontSize: '11px',
352
+ color: '#888',
353
+ display: 'flex',
354
+ justifyContent: 'space-between',
355
+ }}
356
+ >
357
+ <span>{status === 'RUNNING' ? `Session: ${sessionId?.slice(0, 8)}...` : status}</span>
358
+ <span>{url}</span>
359
+ </div>
360
+ </div>
361
+ )
362
+ }
363
+
364
+ export default Browser
@@ -0,0 +1,106 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import { Terminal } from './Terminal'
5
+ import { Browser } from './Browser'
6
+
7
+ interface DevToolsProps {
8
+ /** API endpoint base URL */
9
+ apiUrl?: string
10
+ /** Initial tab to show */
11
+ defaultTab?: 'terminal' | 'browser' | 'split'
12
+ /** Height of the dev tools panel */
13
+ height?: string | number
14
+ }
15
+
16
+ type Tab = 'terminal' | 'browser' | 'split'
17
+
18
+ export const DevTools: React.FC<DevToolsProps> = ({
19
+ apiUrl = 'https://api.sb',
20
+ defaultTab = 'split',
21
+ height = 500,
22
+ }) => {
23
+ const [activeTab, setActiveTab] = useState<Tab>(defaultTab)
24
+
25
+ const tabs: { id: Tab; label: string; icon: string }[] = [
26
+ { id: 'terminal', label: 'Terminal', icon: '⌨️' },
27
+ { id: 'browser', label: 'Browser', icon: '🌐' },
28
+ { id: 'split', label: 'Split View', icon: '⬚' },
29
+ ]
30
+
31
+ return (
32
+ <div
33
+ style={{
34
+ backgroundColor: '#1e1e1e',
35
+ borderRadius: '8px',
36
+ overflow: 'hidden',
37
+ border: '1px solid #3c3c3c',
38
+ }}
39
+ >
40
+ {/* Tab bar */}
41
+ <div
42
+ style={{
43
+ backgroundColor: '#2d2d2d',
44
+ display: 'flex',
45
+ borderBottom: '1px solid #3c3c3c',
46
+ }}
47
+ >
48
+ {tabs.map((tab) => (
49
+ <button
50
+ key={tab.id}
51
+ onClick={() => setActiveTab(tab.id)}
52
+ style={{
53
+ padding: '8px 16px',
54
+ backgroundColor: activeTab === tab.id ? '#1e1e1e' : 'transparent',
55
+ border: 'none',
56
+ borderBottom: activeTab === tab.id ? '2px solid #569cd6' : '2px solid transparent',
57
+ color: activeTab === tab.id ? '#d4d4d4' : '#888',
58
+ cursor: 'pointer',
59
+ fontSize: '13px',
60
+ display: 'flex',
61
+ alignItems: 'center',
62
+ gap: '6px',
63
+ transition: 'all 0.2s',
64
+ }}
65
+ >
66
+ <span>{tab.icon}</span>
67
+ <span>{tab.label}</span>
68
+ </button>
69
+ ))}
70
+ </div>
71
+
72
+ {/* Content */}
73
+ <div
74
+ style={{
75
+ height: typeof height === 'number' ? `${height}px` : height,
76
+ display: 'flex',
77
+ }}
78
+ >
79
+ {activeTab === 'terminal' && (
80
+ <div style={{ flex: 1, padding: '8px' }}>
81
+ <Terminal apiUrl={apiUrl} height="100%" />
82
+ </div>
83
+ )}
84
+
85
+ {activeTab === 'browser' && (
86
+ <div style={{ flex: 1, padding: '8px' }}>
87
+ <Browser apiUrl={apiUrl} height="100%" />
88
+ </div>
89
+ )}
90
+
91
+ {activeTab === 'split' && (
92
+ <>
93
+ <div style={{ flex: 1, padding: '8px', borderRight: '1px solid #3c3c3c' }}>
94
+ <Terminal apiUrl={apiUrl} height="100%" />
95
+ </div>
96
+ <div style={{ flex: 1, padding: '8px' }}>
97
+ <Browser apiUrl={apiUrl} height="100%" />
98
+ </div>
99
+ </>
100
+ )}
101
+ </div>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ export default DevTools