prev-cli 0.16.0 → 0.16.2
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/dist/cli.js +17 -3
- package/package.json +1 -1
- package/src/theme/Preview.tsx +263 -280
- package/src/theme/entry.tsx +94 -33
package/dist/cli.js
CHANGED
|
@@ -961,6 +961,12 @@ async function createViteConfig(options) {
|
|
|
961
961
|
"mermaid",
|
|
962
962
|
"dayjs",
|
|
963
963
|
"@terrastruct/d2"
|
|
964
|
+
],
|
|
965
|
+
exclude: [
|
|
966
|
+
"virtual:prev-config",
|
|
967
|
+
"virtual:prev-previews",
|
|
968
|
+
"virtual:prev-pages",
|
|
969
|
+
"@prev/theme"
|
|
964
970
|
]
|
|
965
971
|
},
|
|
966
972
|
ssr: {
|
|
@@ -1279,10 +1285,18 @@ Examples:
|
|
|
1279
1285
|
prev clean -d 7 Remove caches older than 7 days
|
|
1280
1286
|
`);
|
|
1281
1287
|
}
|
|
1282
|
-
function clearViteCache(rootDir2) {
|
|
1288
|
+
async function clearViteCache(rootDir2) {
|
|
1289
|
+
let cleared = 0;
|
|
1290
|
+
try {
|
|
1291
|
+
const prevCacheDir = await getCacheDir(rootDir2);
|
|
1292
|
+
if (existsSync7(prevCacheDir)) {
|
|
1293
|
+
rmSync3(prevCacheDir, { recursive: true });
|
|
1294
|
+
cleared++;
|
|
1295
|
+
console.log(` ✓ Removed ${prevCacheDir}`);
|
|
1296
|
+
}
|
|
1297
|
+
} catch {}
|
|
1283
1298
|
const viteCacheDir = path9.join(rootDir2, ".vite");
|
|
1284
1299
|
const nodeModulesVite = path9.join(rootDir2, "node_modules", ".vite");
|
|
1285
|
-
let cleared = 0;
|
|
1286
1300
|
if (existsSync7(viteCacheDir)) {
|
|
1287
1301
|
rmSync3(viteCacheDir, { recursive: true });
|
|
1288
1302
|
cleared++;
|
|
@@ -1476,7 +1490,7 @@ async function main() {
|
|
|
1476
1490
|
createPreview(rootDir, previewName);
|
|
1477
1491
|
break;
|
|
1478
1492
|
case "clearcache":
|
|
1479
|
-
clearViteCache(rootDir);
|
|
1493
|
+
await clearViteCache(rootDir);
|
|
1480
1494
|
break;
|
|
1481
1495
|
default:
|
|
1482
1496
|
console.error(`Unknown command: ${command}`);
|
package/package.json
CHANGED
package/src/theme/Preview.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef
|
|
2
|
-
import {
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { IconDeviceMobile, IconDeviceTablet, IconDeviceDesktop, IconArrowsMaximize, IconArrowsMinimize, IconAdjustmentsHorizontal, IconX, IconLoader2, IconArrowLeft } from '@tabler/icons-react'
|
|
3
|
+
import { Link } from '@tanstack/react-router'
|
|
3
4
|
import type { PreviewConfig, PreviewMessage, BuildResult } from '../preview-runtime/types'
|
|
4
5
|
|
|
5
6
|
interface PreviewProps {
|
|
6
7
|
src: string
|
|
7
8
|
height?: string | number
|
|
8
9
|
title?: string
|
|
9
|
-
mode?: 'wasm' | 'legacy'
|
|
10
|
+
mode?: 'wasm' | 'legacy'
|
|
11
|
+
showHeader?: boolean // Show full header with back button and devtools
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
type DeviceMode = 'mobile' | 'tablet' | 'desktop'
|
|
@@ -17,20 +19,11 @@ const DEVICE_WIDTHS: Record<DeviceMode, number | '100%'> = {
|
|
|
17
19
|
desktop: '100%',
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
x: number
|
|
22
|
-
y: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProps) {
|
|
22
|
+
export function Preview({ src, height = 400, title, mode = 'wasm', showHeader = false }: PreviewProps) {
|
|
26
23
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
27
24
|
const [deviceMode, setDeviceMode] = useState<DeviceMode>('desktop')
|
|
28
25
|
const [customWidth, setCustomWidth] = useState<number | null>(null)
|
|
29
|
-
const [isDarkMode, setIsDarkMode] = useState(false)
|
|
30
26
|
const [showSlider, setShowSlider] = useState(false)
|
|
31
|
-
const [pillPosition, setPillPosition] = useState<Position>({ x: 0, y: 0 })
|
|
32
|
-
const [isDragging, setIsDragging] = useState(false)
|
|
33
|
-
const [dragOffset, setDragOffset] = useState<Position>({ x: 0, y: 0 })
|
|
34
27
|
|
|
35
28
|
// WASM preview state
|
|
36
29
|
const [buildStatus, setBuildStatus] = useState<'loading' | 'building' | 'ready' | 'error'>('loading')
|
|
@@ -38,8 +31,6 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
38
31
|
const [buildError, setBuildError] = useState<string | null>(null)
|
|
39
32
|
|
|
40
33
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
41
|
-
const pillRef = useRef<HTMLDivElement>(null)
|
|
42
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
43
34
|
|
|
44
35
|
// URL depends on mode
|
|
45
36
|
const previewUrl = mode === 'wasm' ? '/_preview-runtime' : `/_preview/${src}`
|
|
@@ -48,6 +39,9 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
48
39
|
// Calculate current width
|
|
49
40
|
const currentWidth = customWidth ?? (DEVICE_WIDTHS[deviceMode] === '100%' ? null : DEVICE_WIDTHS[deviceMode] as number)
|
|
50
41
|
|
|
42
|
+
// Use master dark mode from document
|
|
43
|
+
const isDarkMode = typeof document !== 'undefined' && document.documentElement.classList.contains('dark')
|
|
44
|
+
|
|
51
45
|
// Handle escape key and body scroll lock
|
|
52
46
|
useEffect(() => {
|
|
53
47
|
if (!isFullscreen) return
|
|
@@ -67,7 +61,7 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
67
61
|
}
|
|
68
62
|
}, [isFullscreen])
|
|
69
63
|
|
|
70
|
-
//
|
|
64
|
+
// Apply dark mode to iframe content
|
|
71
65
|
useEffect(() => {
|
|
72
66
|
const iframe = iframeRef.current
|
|
73
67
|
if (!iframe) return
|
|
@@ -83,11 +77,10 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
83
77
|
}
|
|
84
78
|
}
|
|
85
79
|
} catch {
|
|
86
|
-
// Cross-origin iframe
|
|
80
|
+
// Cross-origin iframe
|
|
87
81
|
}
|
|
88
82
|
}
|
|
89
83
|
|
|
90
|
-
// Apply on load and when mode changes
|
|
91
84
|
iframe.addEventListener('load', applyDarkMode)
|
|
92
85
|
applyDarkMode()
|
|
93
86
|
|
|
@@ -105,12 +98,10 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
105
98
|
|
|
106
99
|
let configSent = false
|
|
107
100
|
|
|
108
|
-
// Handle messages from iframe
|
|
109
101
|
const handleMessage = (event: MessageEvent) => {
|
|
110
102
|
const msg = event.data as PreviewMessage
|
|
111
103
|
|
|
112
104
|
if (msg.type === 'ready' && !configSent) {
|
|
113
|
-
// Iframe is ready, fetch and send config
|
|
114
105
|
configSent = true
|
|
115
106
|
setBuildStatus('building')
|
|
116
107
|
|
|
@@ -150,62 +141,6 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
150
141
|
}
|
|
151
142
|
}, [mode, src])
|
|
152
143
|
|
|
153
|
-
// Drag handlers
|
|
154
|
-
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
155
|
-
if (!pillRef.current) return
|
|
156
|
-
e.preventDefault()
|
|
157
|
-
|
|
158
|
-
const rect = pillRef.current.getBoundingClientRect()
|
|
159
|
-
setDragOffset({
|
|
160
|
-
x: e.clientX - rect.left,
|
|
161
|
-
y: e.clientY - rect.top,
|
|
162
|
-
})
|
|
163
|
-
setIsDragging(true)
|
|
164
|
-
}, [])
|
|
165
|
-
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
if (!isDragging) return
|
|
168
|
-
|
|
169
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
170
|
-
const container = isFullscreen ? document.body : containerRef.current
|
|
171
|
-
if (!container) return
|
|
172
|
-
|
|
173
|
-
const containerRect = isFullscreen
|
|
174
|
-
? { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }
|
|
175
|
-
: container.getBoundingClientRect()
|
|
176
|
-
|
|
177
|
-
const pillWidth = pillRef.current?.offsetWidth || 0
|
|
178
|
-
const pillHeight = pillRef.current?.offsetHeight || 0
|
|
179
|
-
|
|
180
|
-
// Calculate position relative to container
|
|
181
|
-
let newX = e.clientX - containerRect.left - dragOffset.x
|
|
182
|
-
let newY = e.clientY - containerRect.top - dragOffset.y
|
|
183
|
-
|
|
184
|
-
// Constrain to container bounds
|
|
185
|
-
newX = Math.max(0, Math.min(newX, containerRect.width - pillWidth))
|
|
186
|
-
newY = Math.max(0, Math.min(newY, containerRect.height - pillHeight))
|
|
187
|
-
|
|
188
|
-
setPillPosition({ x: newX, y: newY })
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const handleMouseUp = () => {
|
|
192
|
-
setIsDragging(false)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
document.addEventListener('mousemove', handleMouseMove)
|
|
196
|
-
document.addEventListener('mouseup', handleMouseUp)
|
|
197
|
-
|
|
198
|
-
return () => {
|
|
199
|
-
document.removeEventListener('mousemove', handleMouseMove)
|
|
200
|
-
document.removeEventListener('mouseup', handleMouseUp)
|
|
201
|
-
}
|
|
202
|
-
}, [isDragging, dragOffset, isFullscreen])
|
|
203
|
-
|
|
204
|
-
// Reset pill position when entering/exiting fullscreen
|
|
205
|
-
useEffect(() => {
|
|
206
|
-
setPillPosition({ x: 0, y: 0 })
|
|
207
|
-
}, [isFullscreen])
|
|
208
|
-
|
|
209
144
|
const handleDeviceChange = (mode: DeviceMode) => {
|
|
210
145
|
setDeviceMode(mode)
|
|
211
146
|
setCustomWidth(null)
|
|
@@ -214,10 +149,10 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
214
149
|
|
|
215
150
|
const handleSliderChange = (value: number) => {
|
|
216
151
|
setCustomWidth(value)
|
|
217
|
-
setDeviceMode('desktop')
|
|
152
|
+
setDeviceMode('desktop')
|
|
218
153
|
}
|
|
219
154
|
|
|
220
|
-
// Icon button component
|
|
155
|
+
// Icon button component
|
|
221
156
|
const IconButton = ({
|
|
222
157
|
onClick,
|
|
223
158
|
active,
|
|
@@ -240,8 +175,8 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
240
175
|
alignItems: 'center',
|
|
241
176
|
justifyContent: 'center',
|
|
242
177
|
transition: 'background-color 0.15s, color 0.15s',
|
|
243
|
-
backgroundColor: active ? '#3b82f6' : 'transparent',
|
|
244
|
-
color: active ? '#fff' : '#71717a',
|
|
178
|
+
backgroundColor: active ? 'var(--fd-primary, #3b82f6)' : 'transparent',
|
|
179
|
+
color: active ? '#fff' : 'var(--fd-muted-foreground, #71717a)',
|
|
245
180
|
}}
|
|
246
181
|
title={btnTitle}
|
|
247
182
|
aria-label={btnTitle}
|
|
@@ -250,147 +185,95 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
250
185
|
</button>
|
|
251
186
|
)
|
|
252
187
|
|
|
253
|
-
//
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
gap: '2px',
|
|
261
|
-
padding: '4px 6px',
|
|
262
|
-
backgroundColor: '#fff',
|
|
263
|
-
borderRadius: '8px',
|
|
264
|
-
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06)',
|
|
265
|
-
border: '1px solid #e4e4e7',
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const positionStyle: React.CSSProperties = pillPosition.x === 0 && pillPosition.y === 0
|
|
269
|
-
? { bottom: 12, right: 12 }
|
|
270
|
-
: { left: pillPosition.x, top: pillPosition.y }
|
|
271
|
-
|
|
272
|
-
const dividerStyle: React.CSSProperties = {
|
|
273
|
-
width: '1px',
|
|
274
|
-
height: '16px',
|
|
275
|
-
backgroundColor: '#e4e4e7',
|
|
276
|
-
margin: '0 2px',
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return (
|
|
280
|
-
<div
|
|
281
|
-
ref={pillRef}
|
|
282
|
-
style={{ ...baseStyle, ...positionStyle, cursor: isDragging ? 'grabbing' : undefined }}
|
|
188
|
+
// DevTools in header - device modes, width slider, fullscreen
|
|
189
|
+
const DevTools = () => (
|
|
190
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
|
|
191
|
+
<IconButton
|
|
192
|
+
onClick={() => handleDeviceChange('mobile')}
|
|
193
|
+
active={deviceMode === 'mobile' && customWidth === null}
|
|
194
|
+
title="Mobile (375px)"
|
|
283
195
|
>
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
onMouseDown={handleMouseDown}
|
|
287
|
-
style={{ padding: '4px', cursor: 'grab', color: '#a1a1aa' }}
|
|
288
|
-
title="Drag to move"
|
|
289
|
-
>
|
|
290
|
-
<GripVertical style={{ width: 12, height: 12 }} />
|
|
291
|
-
</div>
|
|
196
|
+
<IconDeviceMobile size={16} />
|
|
197
|
+
</IconButton>
|
|
292
198
|
|
|
293
|
-
|
|
199
|
+
<IconButton
|
|
200
|
+
onClick={() => handleDeviceChange('tablet')}
|
|
201
|
+
active={deviceMode === 'tablet' && customWidth === null}
|
|
202
|
+
title="Tablet (768px)"
|
|
203
|
+
>
|
|
204
|
+
<IconDeviceTablet size={16} />
|
|
205
|
+
</IconButton>
|
|
294
206
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
</IconButton>
|
|
207
|
+
<IconButton
|
|
208
|
+
onClick={() => handleDeviceChange('desktop')}
|
|
209
|
+
active={deviceMode === 'desktop' && customWidth === null}
|
|
210
|
+
title="Desktop (100%)"
|
|
211
|
+
>
|
|
212
|
+
<IconDeviceDesktop size={16} />
|
|
213
|
+
</IconButton>
|
|
303
214
|
|
|
304
|
-
|
|
305
|
-
onClick={() => handleDeviceChange('tablet')}
|
|
306
|
-
active={deviceMode === 'tablet' && customWidth === null}
|
|
307
|
-
title="Tablet (768px)"
|
|
308
|
-
>
|
|
309
|
-
<Tablet style={{ width: 14, height: 14 }} />
|
|
310
|
-
</IconButton>
|
|
215
|
+
<div style={{ width: '1px', height: '16px', backgroundColor: 'var(--fd-border, #e4e4e7)', margin: '0 4px' }} />
|
|
311
216
|
|
|
217
|
+
{/* Width slider toggle */}
|
|
218
|
+
<div style={{ position: 'relative' }}>
|
|
312
219
|
<IconButton
|
|
313
|
-
onClick={() =>
|
|
314
|
-
active={
|
|
315
|
-
title="
|
|
220
|
+
onClick={() => setShowSlider(!showSlider)}
|
|
221
|
+
active={showSlider || customWidth !== null}
|
|
222
|
+
title="Custom width"
|
|
316
223
|
>
|
|
317
|
-
<
|
|
224
|
+
<IconAdjustmentsHorizontal size={16} />
|
|
318
225
|
</IconButton>
|
|
319
226
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<div style={{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
border: '1px solid #e4e4e7',
|
|
345
|
-
minWidth: '192px',
|
|
346
|
-
}}>
|
|
347
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
|
348
|
-
<span style={{ fontSize: '12px', color: '#71717a' }}>Width: {customWidth ?? currentWidth ?? '100%'}px</span>
|
|
349
|
-
<button
|
|
350
|
-
onClick={() => setShowSlider(false)}
|
|
351
|
-
style={{ padding: '2px', background: 'none', border: 'none', cursor: 'pointer', color: '#a1a1aa' }}
|
|
352
|
-
>
|
|
353
|
-
<X style={{ width: 12, height: 12 }} />
|
|
354
|
-
</button>
|
|
355
|
-
</div>
|
|
356
|
-
<input
|
|
357
|
-
type="range"
|
|
358
|
-
min={320}
|
|
359
|
-
max={1920}
|
|
360
|
-
value={customWidth ?? (typeof currentWidth === 'number' ? currentWidth : 1920)}
|
|
361
|
-
onChange={(e) => handleSliderChange(parseInt(e.target.value))}
|
|
362
|
-
style={{ width: '100%', accentColor: '#3b82f6' }}
|
|
363
|
-
/>
|
|
364
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: '#a1a1aa', marginTop: '4px' }}>
|
|
365
|
-
<span>320px</span>
|
|
366
|
-
<span>1920px</span>
|
|
367
|
-
</div>
|
|
227
|
+
{showSlider && (
|
|
228
|
+
<div style={{
|
|
229
|
+
position: 'absolute',
|
|
230
|
+
top: '100%',
|
|
231
|
+
right: 0,
|
|
232
|
+
marginTop: '8px',
|
|
233
|
+
padding: '12px',
|
|
234
|
+
backgroundColor: 'var(--fd-background, #fff)',
|
|
235
|
+
borderRadius: '8px',
|
|
236
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
237
|
+
border: '1px solid var(--fd-border, #e4e4e7)',
|
|
238
|
+
minWidth: '192px',
|
|
239
|
+
zIndex: 100,
|
|
240
|
+
}}>
|
|
241
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
|
242
|
+
<span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #71717a)' }}>
|
|
243
|
+
Width: {customWidth ?? currentWidth ?? '100%'}px
|
|
244
|
+
</span>
|
|
245
|
+
<button
|
|
246
|
+
onClick={() => setShowSlider(false)}
|
|
247
|
+
style={{ padding: '2px', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fd-muted-foreground, #a1a1aa)' }}
|
|
248
|
+
>
|
|
249
|
+
<IconX size={12} />
|
|
250
|
+
</button>
|
|
368
251
|
</div>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
{/* Fullscreen toggle */}
|
|
384
|
-
<IconButton
|
|
385
|
-
onClick={() => setIsFullscreen(!isFullscreen)}
|
|
386
|
-
active={isFullscreen}
|
|
387
|
-
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
388
|
-
>
|
|
389
|
-
{isFullscreen ? <Minimize2 style={{ width: 14, height: 14 }} /> : <Maximize2 style={{ width: 14, height: 14 }} />}
|
|
390
|
-
</IconButton>
|
|
252
|
+
<input
|
|
253
|
+
type="range"
|
|
254
|
+
min={320}
|
|
255
|
+
max={1920}
|
|
256
|
+
value={customWidth ?? (typeof currentWidth === 'number' ? currentWidth : 1920)}
|
|
257
|
+
onChange={(e) => handleSliderChange(parseInt(e.target.value))}
|
|
258
|
+
style={{ width: '100%', accentColor: 'var(--fd-primary, #3b82f6)' }}
|
|
259
|
+
/>
|
|
260
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)', marginTop: '4px' }}>
|
|
261
|
+
<span>320px</span>
|
|
262
|
+
<span>1920px</span>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
391
266
|
</div>
|
|
392
|
-
|
|
393
|
-
|
|
267
|
+
|
|
268
|
+
<IconButton
|
|
269
|
+
onClick={() => setIsFullscreen(!isFullscreen)}
|
|
270
|
+
active={isFullscreen}
|
|
271
|
+
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
272
|
+
>
|
|
273
|
+
{isFullscreen ? <IconArrowsMinimize size={16} /> : <IconArrowsMaximize size={16} />}
|
|
274
|
+
</IconButton>
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
394
277
|
|
|
395
278
|
// Calculate iframe style
|
|
396
279
|
const getIframeContainerStyle = (): React.CSSProperties => {
|
|
@@ -409,75 +292,179 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
409
292
|
<div style={{
|
|
410
293
|
position: 'fixed',
|
|
411
294
|
inset: 0,
|
|
412
|
-
zIndex:
|
|
413
|
-
backgroundColor: '#f4f4f5',
|
|
295
|
+
zIndex: 9999,
|
|
296
|
+
backgroundColor: 'var(--fd-muted, #f4f4f5)',
|
|
414
297
|
display: 'flex',
|
|
415
|
-
|
|
416
|
-
justifyContent: 'center',
|
|
417
|
-
overflow: 'auto',
|
|
298
|
+
flexDirection: 'column',
|
|
418
299
|
}}>
|
|
419
|
-
{/*
|
|
420
|
-
<div
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
300
|
+
{/* Fullscreen header */}
|
|
301
|
+
<div style={{
|
|
302
|
+
display: 'flex',
|
|
303
|
+
alignItems: 'center',
|
|
304
|
+
justifyContent: 'space-between',
|
|
305
|
+
padding: '12px 16px',
|
|
306
|
+
backgroundColor: 'var(--fd-background, #fff)',
|
|
307
|
+
borderBottom: '1px solid var(--fd-border, #e4e4e7)',
|
|
308
|
+
}}>
|
|
309
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
310
|
+
<span style={{ fontSize: '16px', fontWeight: 600, color: 'var(--fd-foreground)' }}>
|
|
311
|
+
{displayTitle}
|
|
312
|
+
</span>
|
|
313
|
+
{mode === 'wasm' && buildTime && (
|
|
314
|
+
<span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)' }}>{buildTime}ms</span>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
<DevTools />
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{/* Checkered background */}
|
|
321
|
+
<div style={{
|
|
322
|
+
flex: 1,
|
|
323
|
+
position: 'relative',
|
|
324
|
+
backgroundImage: 'linear-gradient(45deg, var(--fd-border, #e5e5e5) 25%, transparent 25%), linear-gradient(-45deg, var(--fd-border, #e5e5e5) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--fd-border, #e5e5e5) 75%), linear-gradient(-45deg, transparent 75%, var(--fd-border, #e5e5e5) 75%)',
|
|
325
|
+
backgroundSize: '20px 20px',
|
|
326
|
+
backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px',
|
|
327
|
+
display: 'flex',
|
|
328
|
+
justifyContent: 'center',
|
|
329
|
+
}}>
|
|
330
|
+
<div style={{
|
|
331
|
+
backgroundColor: 'var(--fd-background, #fff)',
|
|
332
|
+
boxShadow: '0 4px 24px rgba(0,0,0,0.1)',
|
|
438
333
|
height: '100%',
|
|
439
334
|
...getIframeContainerStyle(),
|
|
440
|
-
}}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
335
|
+
}}>
|
|
336
|
+
<iframe
|
|
337
|
+
ref={iframeRef}
|
|
338
|
+
src={previewUrl}
|
|
339
|
+
style={{ width: '100%', height: '100%', border: 'none' }}
|
|
340
|
+
title={displayTitle}
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
448
343
|
</div>
|
|
344
|
+
</div>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Non-fullscreen with showHeader (individual preview page)
|
|
349
|
+
if (showHeader) {
|
|
350
|
+
return (
|
|
351
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
352
|
+
{/* Header with back button and devtools */}
|
|
353
|
+
<div style={{
|
|
354
|
+
display: 'flex',
|
|
355
|
+
alignItems: 'center',
|
|
356
|
+
justifyContent: 'space-between',
|
|
357
|
+
padding: '12px 16px',
|
|
358
|
+
backgroundColor: 'var(--fd-card, #fafafa)',
|
|
359
|
+
borderBottom: '1px solid var(--fd-border, #e4e4e7)',
|
|
360
|
+
borderRadius: '8px 8px 0 0',
|
|
361
|
+
}}>
|
|
362
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
363
|
+
<Link
|
|
364
|
+
to="/previews"
|
|
365
|
+
style={{
|
|
366
|
+
display: 'flex',
|
|
367
|
+
alignItems: 'center',
|
|
368
|
+
gap: '4px',
|
|
369
|
+
color: 'var(--fd-muted-foreground, #71717a)',
|
|
370
|
+
textDecoration: 'none',
|
|
371
|
+
fontSize: '14px',
|
|
372
|
+
}}
|
|
373
|
+
>
|
|
374
|
+
<IconArrowLeft size={16} />
|
|
375
|
+
Back
|
|
376
|
+
</Link>
|
|
377
|
+
<div style={{ width: '1px', height: '20px', backgroundColor: 'var(--fd-border, #e4e4e7)' }} />
|
|
378
|
+
<span style={{ fontSize: '16px', fontWeight: 600, color: 'var(--fd-foreground)' }}>
|
|
379
|
+
{displayTitle}
|
|
380
|
+
</span>
|
|
381
|
+
{mode === 'wasm' && buildStatus === 'building' && (
|
|
382
|
+
<IconLoader2 size={16} style={{ color: 'var(--fd-primary, #3b82f6)', animation: 'spin 1s linear infinite' }} />
|
|
383
|
+
)}
|
|
384
|
+
{mode === 'wasm' && buildTime && (
|
|
385
|
+
<span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)' }}>{buildTime}ms</span>
|
|
386
|
+
)}
|
|
387
|
+
</div>
|
|
388
|
+
<DevTools />
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
{/* Build error display */}
|
|
392
|
+
{mode === 'wasm' && buildError && (
|
|
393
|
+
<div style={{
|
|
394
|
+
padding: '8px 12px',
|
|
395
|
+
backgroundColor: '#fef2f2',
|
|
396
|
+
borderBottom: '1px solid #fecaca',
|
|
397
|
+
}}>
|
|
398
|
+
<pre style={{
|
|
399
|
+
fontSize: '12px',
|
|
400
|
+
color: '#dc2626',
|
|
401
|
+
whiteSpace: 'pre-wrap',
|
|
402
|
+
fontFamily: 'monospace',
|
|
403
|
+
overflow: 'auto',
|
|
404
|
+
maxHeight: '128px',
|
|
405
|
+
margin: 0,
|
|
406
|
+
}}>
|
|
407
|
+
{buildError}
|
|
408
|
+
</pre>
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
449
411
|
|
|
450
|
-
|
|
412
|
+
{/* Preview area with checkered background */}
|
|
413
|
+
<div style={{
|
|
414
|
+
flex: 1,
|
|
415
|
+
position: 'relative',
|
|
416
|
+
backgroundColor: 'var(--fd-muted, #f4f4f5)',
|
|
417
|
+
backgroundImage: 'linear-gradient(45deg, var(--fd-border, #e5e5e5) 25%, transparent 25%), linear-gradient(-45deg, var(--fd-border, #e5e5e5) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--fd-border, #e5e5e5) 75%), linear-gradient(-45deg, transparent 75%, var(--fd-border, #e5e5e5) 75%)',
|
|
418
|
+
backgroundSize: '16px 16px',
|
|
419
|
+
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
|
|
420
|
+
display: 'flex',
|
|
421
|
+
justifyContent: 'center',
|
|
422
|
+
}}>
|
|
423
|
+
<div style={{
|
|
424
|
+
backgroundColor: 'var(--fd-background, #fff)',
|
|
425
|
+
transition: 'all 0.3s',
|
|
426
|
+
...getIframeContainerStyle(),
|
|
427
|
+
}}>
|
|
428
|
+
<iframe
|
|
429
|
+
ref={iframeRef}
|
|
430
|
+
src={previewUrl}
|
|
431
|
+
style={{
|
|
432
|
+
height: typeof height === 'number' ? `${height}px` : height,
|
|
433
|
+
width: '100%',
|
|
434
|
+
border: 'none',
|
|
435
|
+
}}
|
|
436
|
+
title={displayTitle}
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
451
440
|
</div>
|
|
452
441
|
)
|
|
453
442
|
}
|
|
454
443
|
|
|
444
|
+
// Embedded preview (no header, compact)
|
|
455
445
|
return (
|
|
456
|
-
<div
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}}
|
|
465
|
-
>
|
|
466
|
-
{/* Header */}
|
|
446
|
+
<div style={{
|
|
447
|
+
margin: '16px 0',
|
|
448
|
+
border: '1px solid var(--fd-border, #e4e4e7)',
|
|
449
|
+
borderRadius: '8px',
|
|
450
|
+
overflow: 'hidden',
|
|
451
|
+
position: 'relative',
|
|
452
|
+
}}>
|
|
453
|
+
{/* Compact header */}
|
|
467
454
|
<div style={{
|
|
468
455
|
display: 'flex',
|
|
469
456
|
alignItems: 'center',
|
|
470
457
|
justifyContent: 'space-between',
|
|
471
458
|
padding: '8px 12px',
|
|
472
|
-
backgroundColor: '#fafafa',
|
|
473
|
-
borderBottom: '1px solid #e4e4e7',
|
|
459
|
+
backgroundColor: 'var(--fd-card, #fafafa)',
|
|
460
|
+
borderBottom: '1px solid var(--fd-border, #e4e4e7)',
|
|
474
461
|
}}>
|
|
475
462
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
476
|
-
<span style={{ fontSize: '14px', fontWeight: 500, color: '#52525b' }}>
|
|
463
|
+
<span style={{ fontSize: '14px', fontWeight: 500, color: 'var(--fd-foreground, #52525b)' }}>
|
|
477
464
|
{displayTitle}
|
|
478
465
|
</span>
|
|
479
466
|
{mode === 'wasm' && buildStatus === 'building' && (
|
|
480
|
-
<
|
|
467
|
+
<IconLoader2 size={14} style={{ color: 'var(--fd-primary, #3b82f6)', animation: 'spin 1s linear infinite' }} />
|
|
481
468
|
)}
|
|
482
469
|
{mode === 'wasm' && buildStatus === 'error' && (
|
|
483
470
|
<span style={{ fontSize: '12px', color: '#ef4444' }}>Error</span>
|
|
@@ -485,11 +472,12 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
485
472
|
</div>
|
|
486
473
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
487
474
|
{mode === 'wasm' && buildTime && (
|
|
488
|
-
<span style={{ fontSize: '12px', color: '#a1a1aa' }}>{buildTime}ms</span>
|
|
475
|
+
<span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)' }}>{buildTime}ms</span>
|
|
489
476
|
)}
|
|
490
|
-
<span style={{ fontSize: '12px', color: '#a1a1aa' }}>
|
|
477
|
+
<span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)' }}>
|
|
491
478
|
{currentWidth ? `${currentWidth}px` : '100%'}
|
|
492
479
|
</span>
|
|
480
|
+
<DevTools />
|
|
493
481
|
</div>
|
|
494
482
|
</div>
|
|
495
483
|
|
|
@@ -514,24 +502,21 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
514
502
|
</div>
|
|
515
503
|
)}
|
|
516
504
|
|
|
517
|
-
{/* Preview area
|
|
518
|
-
<div
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
>
|
|
527
|
-
{
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
...getIframeContainerStyle(),
|
|
533
|
-
}}
|
|
534
|
-
>
|
|
505
|
+
{/* Preview area */}
|
|
506
|
+
<div style={{
|
|
507
|
+
position: 'relative',
|
|
508
|
+
backgroundColor: 'var(--fd-muted, #f4f4f5)',
|
|
509
|
+
backgroundImage: 'linear-gradient(45deg, var(--fd-border, #e5e5e5) 25%, transparent 25%), linear-gradient(-45deg, var(--fd-border, #e5e5e5) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--fd-border, #e5e5e5) 75%), linear-gradient(-45deg, transparent 75%, var(--fd-border, #e5e5e5) 75%)',
|
|
510
|
+
backgroundSize: '16px 16px',
|
|
511
|
+
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
|
|
512
|
+
display: 'flex',
|
|
513
|
+
justifyContent: 'center',
|
|
514
|
+
}}>
|
|
515
|
+
<div style={{
|
|
516
|
+
backgroundColor: 'var(--fd-background, #fff)',
|
|
517
|
+
transition: 'all 0.3s',
|
|
518
|
+
...getIframeContainerStyle(),
|
|
519
|
+
}}>
|
|
535
520
|
<iframe
|
|
536
521
|
ref={iframeRef}
|
|
537
522
|
src={previewUrl}
|
|
@@ -543,8 +528,6 @@ export function Preview({ src, height = 400, title, mode = 'wasm' }: PreviewProp
|
|
|
543
528
|
title={displayTitle}
|
|
544
529
|
/>
|
|
545
530
|
</div>
|
|
546
|
-
|
|
547
|
-
<DevToolsPill />
|
|
548
531
|
</div>
|
|
549
532
|
</div>
|
|
550
533
|
)
|
package/src/theme/entry.tsx
CHANGED
|
@@ -85,7 +85,7 @@ function PageWrapper({ Component, meta }: { Component: React.ComponentType; meta
|
|
|
85
85
|
)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Previews catalog - Storybook-like gallery
|
|
88
|
+
// Previews catalog - Storybook-like gallery with clickable cards
|
|
89
89
|
function PreviewsCatalog() {
|
|
90
90
|
if (!previews || previews.length === 0) {
|
|
91
91
|
return (
|
|
@@ -116,53 +116,31 @@ function PreviewsCatalog() {
|
|
|
116
116
|
Previews
|
|
117
117
|
</h1>
|
|
118
118
|
<p style={{ color: '#666' }}>
|
|
119
|
-
{previews.length} component preview{previews.length !== 1 ? 's' : ''} available
|
|
119
|
+
{previews.length} component preview{previews.length !== 1 ? 's' : ''} available.
|
|
120
|
+
Click any preview to open it.
|
|
120
121
|
</p>
|
|
121
122
|
</div>
|
|
122
123
|
|
|
123
124
|
<div style={{
|
|
124
125
|
display: 'grid',
|
|
125
|
-
gridTemplateColumns: 'repeat(auto-fill, minmax(
|
|
126
|
-
gap: '
|
|
126
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
|
127
|
+
gap: '20px',
|
|
127
128
|
}}>
|
|
128
129
|
{previews.map((preview: { name: string; route: string }) => (
|
|
129
|
-
<
|
|
130
|
-
border: '1px solid #e4e4e7',
|
|
131
|
-
borderRadius: '12px',
|
|
132
|
-
overflow: 'hidden',
|
|
133
|
-
backgroundColor: '#fff',
|
|
134
|
-
}}>
|
|
135
|
-
<div style={{
|
|
136
|
-
padding: '12px 16px',
|
|
137
|
-
borderBottom: '1px solid #e4e4e7',
|
|
138
|
-
backgroundColor: '#fafafa',
|
|
139
|
-
}}>
|
|
140
|
-
<h2 style={{ fontSize: '16px', fontWeight: 600, margin: 0 }}>
|
|
141
|
-
{preview.name}
|
|
142
|
-
</h2>
|
|
143
|
-
<code style={{
|
|
144
|
-
fontSize: '12px',
|
|
145
|
-
color: '#666',
|
|
146
|
-
fontFamily: 'monospace',
|
|
147
|
-
}}>
|
|
148
|
-
previews/{preview.name}/
|
|
149
|
-
</code>
|
|
150
|
-
</div>
|
|
151
|
-
<Preview src={preview.name} height={300} />
|
|
152
|
-
</div>
|
|
130
|
+
<PreviewCard key={preview.name} name={preview.name} />
|
|
153
131
|
))}
|
|
154
132
|
</div>
|
|
155
133
|
|
|
156
134
|
<div style={{
|
|
157
135
|
marginTop: '40px',
|
|
158
136
|
padding: '16px',
|
|
159
|
-
backgroundColor: '
|
|
160
|
-
border: '1px solid
|
|
137
|
+
backgroundColor: 'var(--fd-muted)',
|
|
138
|
+
border: '1px solid var(--fd-border)',
|
|
161
139
|
borderRadius: '8px',
|
|
162
140
|
}}>
|
|
163
|
-
<p style={{ margin: 0, fontSize: '14px', color: '
|
|
141
|
+
<p style={{ margin: 0, fontSize: '14px', color: 'var(--fd-muted-foreground)' }}>
|
|
164
142
|
<strong>Tip:</strong> Embed any preview in your MDX docs with{' '}
|
|
165
|
-
<code style={{ backgroundColor: '
|
|
143
|
+
<code style={{ backgroundColor: 'var(--fd-accent)', padding: '2px 6px', borderRadius: '4px' }}>
|
|
166
144
|
{'<Preview src="name" />'}
|
|
167
145
|
</code>
|
|
168
146
|
</p>
|
|
@@ -171,6 +149,82 @@ function PreviewsCatalog() {
|
|
|
171
149
|
)
|
|
172
150
|
}
|
|
173
151
|
|
|
152
|
+
// Individual preview card - clickable thumbnail
|
|
153
|
+
import { Link, useParams } from '@tanstack/react-router'
|
|
154
|
+
|
|
155
|
+
function PreviewCard({ name }: { name: string }) {
|
|
156
|
+
return (
|
|
157
|
+
<Link
|
|
158
|
+
to={`/previews/${name}`}
|
|
159
|
+
style={{
|
|
160
|
+
display: 'block',
|
|
161
|
+
border: '1px solid var(--fd-border)',
|
|
162
|
+
borderRadius: '12px',
|
|
163
|
+
overflow: 'hidden',
|
|
164
|
+
backgroundColor: 'var(--fd-background)',
|
|
165
|
+
textDecoration: 'none',
|
|
166
|
+
color: 'inherit',
|
|
167
|
+
transition: 'box-shadow 0.2s, transform 0.2s',
|
|
168
|
+
}}
|
|
169
|
+
onMouseOver={(e) => {
|
|
170
|
+
e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.12)'
|
|
171
|
+
e.currentTarget.style.transform = 'translateY(-2px)'
|
|
172
|
+
}}
|
|
173
|
+
onMouseOut={(e) => {
|
|
174
|
+
e.currentTarget.style.boxShadow = ''
|
|
175
|
+
e.currentTarget.style.transform = ''
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{/* Thumbnail preview */}
|
|
179
|
+
<div style={{
|
|
180
|
+
height: '180px',
|
|
181
|
+
overflow: 'hidden',
|
|
182
|
+
position: 'relative',
|
|
183
|
+
backgroundColor: 'var(--fd-muted)',
|
|
184
|
+
pointerEvents: 'none',
|
|
185
|
+
}}>
|
|
186
|
+
<iframe
|
|
187
|
+
src={`/_preview-runtime?src=${name}`}
|
|
188
|
+
style={{
|
|
189
|
+
width: '100%',
|
|
190
|
+
height: '100%',
|
|
191
|
+
border: 'none',
|
|
192
|
+
transform: 'scale(0.5)',
|
|
193
|
+
transformOrigin: 'top left',
|
|
194
|
+
width: '200%',
|
|
195
|
+
height: '200%',
|
|
196
|
+
}}
|
|
197
|
+
title={name}
|
|
198
|
+
loading="lazy"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
{/* Card footer */}
|
|
202
|
+
<div style={{
|
|
203
|
+
padding: '12px 16px',
|
|
204
|
+
borderTop: '1px solid var(--fd-border)',
|
|
205
|
+
backgroundColor: 'var(--fd-card)',
|
|
206
|
+
}}>
|
|
207
|
+
<h3 style={{ fontSize: '14px', fontWeight: 600, margin: 0 }}>
|
|
208
|
+
{name}
|
|
209
|
+
</h3>
|
|
210
|
+
<code style={{
|
|
211
|
+
fontSize: '11px',
|
|
212
|
+
color: 'var(--fd-muted-foreground)',
|
|
213
|
+
fontFamily: 'monospace',
|
|
214
|
+
}}>
|
|
215
|
+
previews/{name}/
|
|
216
|
+
</code>
|
|
217
|
+
</div>
|
|
218
|
+
</Link>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Individual preview page - full view with devtools in header
|
|
223
|
+
function PreviewPage() {
|
|
224
|
+
const { name } = useParams({ from: '/previews/$name' })
|
|
225
|
+
return <Preview src={name} height="calc(100vh - 200px)" showHeader />
|
|
226
|
+
}
|
|
227
|
+
|
|
174
228
|
// Root layout with custom lightweight Layout
|
|
175
229
|
function RootLayout() {
|
|
176
230
|
const pageTree = convertToPageTree(sidebar)
|
|
@@ -196,6 +250,13 @@ const previewsRoute = createRoute({
|
|
|
196
250
|
component: PreviewsCatalog,
|
|
197
251
|
})
|
|
198
252
|
|
|
253
|
+
// Individual preview route
|
|
254
|
+
const previewDetailRoute = createRoute({
|
|
255
|
+
getParentRoute: () => rootRoute,
|
|
256
|
+
path: '/previews/$name',
|
|
257
|
+
component: PreviewPage,
|
|
258
|
+
})
|
|
259
|
+
|
|
199
260
|
// Create routes from pages
|
|
200
261
|
const pageRoutes = pages.map((page: { route: string; file: string; title?: string; description?: string; frontmatter?: Record<string, unknown> }) => {
|
|
201
262
|
const Component = getPageComponent(page.file)
|
|
@@ -212,7 +273,7 @@ const pageRoutes = pages.map((page: { route: string; file: string; title?: strin
|
|
|
212
273
|
})
|
|
213
274
|
|
|
214
275
|
// Create router
|
|
215
|
-
const routeTree = rootRoute.addChildren([previewsRoute, ...pageRoutes])
|
|
276
|
+
const routeTree = rootRoute.addChildren([previewsRoute, previewDetailRoute, ...pageRoutes])
|
|
216
277
|
const router = createRouter({ routeTree })
|
|
217
278
|
|
|
218
279
|
// Mount app - RouterProvider must be outermost so TanStack Router context is available
|