prev-cli 0.23.0 → 0.24.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Transform MDX directories into beautiful documentation websites",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,7 +22,11 @@ const DEVICE_WIDTHS: Record<DeviceMode, number | '100%'> = {
22
22
 
23
23
  export function Preview({ src, height = 400, title, mode = 'wasm', showHeader = false }: PreviewProps) {
24
24
  const [isFullscreen, setIsFullscreen] = useState(false)
25
- const [deviceMode, setDeviceMode] = useState<DeviceMode>('desktop')
25
+ // Default to 'mobile' device mode on mobile viewports to match user's actual environment
26
+ const [deviceMode, setDeviceMode] = useState<DeviceMode>(() => {
27
+ if (typeof window === 'undefined') return 'desktop'
28
+ return window.innerWidth < 768 ? 'mobile' : 'desktop'
29
+ })
26
30
  const [customWidth, setCustomWidth] = useState<number | null>(null)
27
31
  const [showSlider, setShowSlider] = useState(false)
28
32
 
@@ -197,95 +201,124 @@ export function Preview({ src, height = 400, title, mode = 'wasm', showHeader =
197
201
  </button>
198
202
  )
199
203
 
204
+ // Track viewport size for responsive layout
205
+ const [isMobileViewport, setIsMobileViewport] = useState(typeof window !== 'undefined' ? window.innerWidth < 480 : false)
206
+
207
+ useEffect(() => {
208
+ const handleResize = () => setIsMobileViewport(window.innerWidth < 480)
209
+ window.addEventListener('resize', handleResize)
210
+ return () => window.removeEventListener('resize', handleResize)
211
+ }, [])
212
+
200
213
  // DevTools in header - device modes, width slider, fullscreen
201
- const DevTools = () => (
202
- <div style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
203
- <IconButton
204
- onClick={() => handleDeviceChange('mobile')}
205
- active={deviceMode === 'mobile' && customWidth === null}
206
- title="Mobile (375px)"
207
- >
208
- <Icon name="mobile" size={16} />
209
- </IconButton>
210
-
211
- <IconButton
212
- onClick={() => handleDeviceChange('tablet')}
213
- active={deviceMode === 'tablet' && customWidth === null}
214
- title="Tablet (768px)"
215
- >
216
- <Icon name="tablet" size={16} />
217
- </IconButton>
218
-
219
- <IconButton
220
- onClick={() => handleDeviceChange('desktop')}
221
- active={deviceMode === 'desktop' && customWidth === null}
222
- title="Desktop (100%)"
223
- >
224
- <Icon name="desktop" size={16} />
225
- </IconButton>
226
-
227
- <div style={{ width: '1px', height: '16px', backgroundColor: 'var(--fd-border, #e4e4e7)', margin: '0 4px' }} />
228
-
229
- {/* Width slider toggle */}
230
- <div style={{ position: 'relative' }}>
214
+ // On mobile: simplified controls with just fullscreen
215
+ // On larger screens: full device controls
216
+ const DevTools = ({ compact = false }: { compact?: boolean }) => {
217
+ // Mobile viewport or compact mode: show minimal controls
218
+ if (isMobileViewport || compact) {
219
+ return (
220
+ <div style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
221
+ <IconButton
222
+ onClick={() => setIsFullscreen(!isFullscreen)}
223
+ active={isFullscreen}
224
+ title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
225
+ >
226
+ <Icon name={isFullscreen ? 'minimize' : 'maximize'} size={16} />
227
+ </IconButton>
228
+ </div>
229
+ )
230
+ }
231
+
232
+ // Full controls for larger screens
233
+ return (
234
+ <div style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
231
235
  <IconButton
232
- onClick={() => setShowSlider(!showSlider)}
233
- active={showSlider || customWidth !== null}
234
- title="Custom width"
236
+ onClick={() => handleDeviceChange('mobile')}
237
+ active={deviceMode === 'mobile' && customWidth === null}
238
+ title="Mobile (375px)"
235
239
  >
236
- <Icon name="sliders" size={16} />
240
+ <Icon name="mobile" size={16} />
237
241
  </IconButton>
238
242
 
239
- {showSlider && (
240
- <div style={{
241
- position: 'absolute',
242
- top: '100%',
243
- right: 0,
244
- marginTop: '8px',
245
- padding: '12px',
246
- backgroundColor: 'var(--fd-background, #fff)',
247
- borderRadius: '8px',
248
- boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
249
- border: '1px solid var(--fd-border, #e4e4e7)',
250
- minWidth: '192px',
251
- zIndex: 100,
252
- }}>
253
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
254
- <span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #71717a)' }}>
255
- Width: {customWidth ?? currentWidth ?? '100%'}px
256
- </span>
257
- <button
258
- onClick={() => setShowSlider(false)}
259
- style={{ padding: '2px', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fd-muted-foreground, #a1a1aa)' }}
260
- >
261
- <Icon name="x" size={12} />
262
- </button>
263
- </div>
264
- <input
265
- type="range"
266
- min={320}
267
- max={1920}
268
- value={customWidth ?? (typeof currentWidth === 'number' ? currentWidth : 1920)}
269
- onChange={(e) => handleSliderChange(parseInt(e.target.value))}
270
- style={{ width: '100%', accentColor: 'var(--fd-primary, #3b82f6)' }}
271
- />
272
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)', marginTop: '4px' }}>
273
- <span>320px</span>
274
- <span>1920px</span>
243
+ <IconButton
244
+ onClick={() => handleDeviceChange('tablet')}
245
+ active={deviceMode === 'tablet' && customWidth === null}
246
+ title="Tablet (768px)"
247
+ >
248
+ <Icon name="tablet" size={16} />
249
+ </IconButton>
250
+
251
+ <IconButton
252
+ onClick={() => handleDeviceChange('desktop')}
253
+ active={deviceMode === 'desktop' && customWidth === null}
254
+ title="Desktop (100%)"
255
+ >
256
+ <Icon name="desktop" size={16} />
257
+ </IconButton>
258
+
259
+ <div style={{ width: '1px', height: '16px', backgroundColor: 'var(--fd-border, #e4e4e7)', margin: '0 4px' }} />
260
+
261
+ {/* Width slider toggle */}
262
+ <div style={{ position: 'relative' }}>
263
+ <IconButton
264
+ onClick={() => setShowSlider(!showSlider)}
265
+ active={showSlider || customWidth !== null}
266
+ title="Custom width"
267
+ >
268
+ <Icon name="sliders" size={16} />
269
+ </IconButton>
270
+
271
+ {showSlider && (
272
+ <div style={{
273
+ position: 'absolute',
274
+ top: '100%',
275
+ right: 0,
276
+ marginTop: '8px',
277
+ padding: '12px',
278
+ backgroundColor: 'var(--fd-background, #fff)',
279
+ borderRadius: '8px',
280
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
281
+ border: '1px solid var(--fd-border, #e4e4e7)',
282
+ minWidth: '192px',
283
+ zIndex: 100,
284
+ }}>
285
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
286
+ <span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #71717a)' }}>
287
+ Width: {customWidth ?? currentWidth ?? '100%'}px
288
+ </span>
289
+ <button
290
+ onClick={() => setShowSlider(false)}
291
+ style={{ padding: '2px', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fd-muted-foreground, #a1a1aa)' }}
292
+ >
293
+ <Icon name="x" size={12} />
294
+ </button>
295
+ </div>
296
+ <input
297
+ type="range"
298
+ min={320}
299
+ max={1920}
300
+ value={customWidth ?? (typeof currentWidth === 'number' ? currentWidth : 1920)}
301
+ onChange={(e) => handleSliderChange(parseInt(e.target.value))}
302
+ style={{ width: '100%', accentColor: 'var(--fd-primary, #3b82f6)' }}
303
+ />
304
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)', marginTop: '4px' }}>
305
+ <span>320px</span>
306
+ <span>1920px</span>
307
+ </div>
275
308
  </div>
276
- </div>
277
- )}
278
- </div>
309
+ )}
310
+ </div>
279
311
 
280
- <IconButton
281
- onClick={() => setIsFullscreen(!isFullscreen)}
282
- active={isFullscreen}
283
- title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
284
- >
285
- <Icon name={isFullscreen ? 'minimize' : 'maximize'} size={16} />
286
- </IconButton>
287
- </div>
288
- )
312
+ <IconButton
313
+ onClick={() => setIsFullscreen(!isFullscreen)}
314
+ active={isFullscreen}
315
+ title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
316
+ >
317
+ <Icon name={isFullscreen ? 'minimize' : 'maximize'} size={16} />
318
+ </IconButton>
319
+ </div>
320
+ )
321
+ }
289
322
 
290
323
  // Register DevTools in toolbar when on detail page (showHeader mode)
291
324
  useEffect(() => {
@@ -469,34 +502,45 @@ export function Preview({ src, height = 400, title, mode = 'wasm', showHeader =
469
502
  overflow: 'hidden',
470
503
  position: 'relative',
471
504
  }}>
472
- {/* Compact header */}
505
+ {/* Compact header - responsive for mobile */}
473
506
  <div style={{
474
507
  display: 'flex',
475
508
  alignItems: 'center',
476
509
  justifyContent: 'space-between',
477
- padding: '8px 12px',
510
+ padding: isMobileViewport ? '6px 10px' : '8px 12px',
478
511
  backgroundColor: 'var(--fd-card, #fafafa)',
479
512
  borderBottom: '1px solid var(--fd-border, #e4e4e7)',
513
+ gap: '8px',
480
514
  }}>
481
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
482
- <span style={{ fontSize: '14px', fontWeight: 500, color: 'var(--fd-foreground, #52525b)' }}>
515
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, flex: 1 }}>
516
+ <span style={{
517
+ fontSize: isMobileViewport ? '13px' : '14px',
518
+ fontWeight: 500,
519
+ color: 'var(--fd-foreground, #52525b)',
520
+ overflow: 'hidden',
521
+ textOverflow: 'ellipsis',
522
+ whiteSpace: 'nowrap',
523
+ }}>
483
524
  {displayTitle}
484
525
  </span>
485
526
  {mode === 'wasm' && buildStatus === 'building' && (
486
- <Icon name="loader" size={14} style={{ color: 'var(--fd-primary, #3b82f6)', animation: 'spin 1s linear infinite' }} />
527
+ <Icon name="loader" size={14} style={{ color: 'var(--fd-primary, #3b82f6)', animation: 'spin 1s linear infinite', flexShrink: 0 }} />
487
528
  )}
488
529
  {mode === 'wasm' && buildStatus === 'error' && (
489
- <span style={{ fontSize: '12px', color: '#ef4444' }}>Error</span>
530
+ <span style={{ fontSize: '12px', color: '#ef4444', flexShrink: 0 }}>Error</span>
490
531
  )}
491
532
  </div>
492
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
493
- {mode === 'wasm' && buildTime && (
533
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexShrink: 0 }}>
534
+ {/* Hide build time and width on mobile to save space */}
535
+ {!isMobileViewport && mode === 'wasm' && buildTime && (
494
536
  <span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)' }}>{buildTime}ms</span>
495
537
  )}
496
- <span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)' }}>
497
- {currentWidth ? `${currentWidth}px` : '100%'}
498
- </span>
499
- <DevTools />
538
+ {!isMobileViewport && (
539
+ <span style={{ fontSize: '12px', color: 'var(--fd-muted-foreground, #a1a1aa)' }}>
540
+ {currentWidth ? `${currentWidth}px` : '100%'}
541
+ </span>
542
+ )}
543
+ <DevTools compact={isMobileViewport} />
500
544
  </div>
501
545
  </div>
502
546
 
@@ -73,7 +73,39 @@ function MdxLink({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAn
73
73
  )
74
74
  }
75
75
 
76
+ // Responsive table wrapper for horizontal scrolling on mobile
77
+ function MdxTable({ children, ...props }: React.TableHTMLAttributes<HTMLTableElement>) {
78
+ const wrapperRef = React.useRef<HTMLDivElement>(null)
79
+
80
+ React.useEffect(() => {
81
+ const wrapper = wrapperRef.current
82
+ if (!wrapper) return
83
+
84
+ // Check if table overflows and add class for scroll indicator
85
+ const checkOverflow = () => {
86
+ const hasOverflow = wrapper.scrollWidth > wrapper.clientWidth
87
+ wrapper.classList.toggle('has-scroll', hasOverflow && wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth - 1)
88
+ }
89
+
90
+ checkOverflow()
91
+ wrapper.addEventListener('scroll', checkOverflow)
92
+ window.addEventListener('resize', checkOverflow)
93
+
94
+ return () => {
95
+ wrapper.removeEventListener('scroll', checkOverflow)
96
+ window.removeEventListener('resize', checkOverflow)
97
+ }
98
+ }, [])
99
+
100
+ return (
101
+ <div ref={wrapperRef} className="table-wrapper">
102
+ <table {...props}>{children}</table>
103
+ </div>
104
+ )
105
+ }
106
+
76
107
  export const mdxComponents = {
77
108
  Preview,
78
109
  a: MdxLink,
110
+ table: MdxTable,
79
111
  }
@@ -214,6 +214,27 @@ body {
214
214
  padding-bottom: 3rem !important;
215
215
  }
216
216
 
217
+ /* Mobile: reduce content padding for better use of screen space */
218
+ @media (max-width: 640px) {
219
+ .prev-content {
220
+ padding-left: 1rem !important;
221
+ padding-right: 1rem !important;
222
+ font-size: 0.9375rem;
223
+ }
224
+
225
+ .prev-content h1 {
226
+ font-size: 1.75rem;
227
+ }
228
+
229
+ .prev-content h2 {
230
+ font-size: 1.25rem;
231
+ }
232
+
233
+ .prev-content h3 {
234
+ font-size: 1.125rem;
235
+ }
236
+ }
237
+
217
238
  /* Heading styles with better spacing */
218
239
  .prev-content h1 {
219
240
  font-size: 2.25rem;
@@ -340,14 +361,57 @@ body {
340
361
  color: var(--fd-foreground);
341
362
  }
342
363
 
343
- /* Table styling */
344
- .prev-content table {
364
+ /* Table styling - responsive with horizontal scroll on mobile */
365
+ .prev-content .table-wrapper {
345
366
  width: 100%;
346
367
  margin: 1.75rem 0;
368
+ overflow-x: auto;
369
+ -webkit-overflow-scrolling: touch;
370
+ }
371
+
372
+ /* Scroll indicator gradient for tables on mobile */
373
+ @media (max-width: 640px) {
374
+ .prev-content .table-wrapper {
375
+ position: relative;
376
+ }
377
+
378
+ .prev-content .table-wrapper::after {
379
+ content: '';
380
+ position: absolute;
381
+ top: 0;
382
+ right: 0;
383
+ bottom: 0;
384
+ width: 24px;
385
+ background: linear-gradient(to right, transparent, var(--fd-background));
386
+ pointer-events: none;
387
+ opacity: 0;
388
+ transition: opacity 0.2s ease;
389
+ }
390
+
391
+ .prev-content .table-wrapper.has-scroll::after {
392
+ opacity: 1;
393
+ }
394
+ }
395
+
396
+ .prev-content table {
397
+ width: 100%;
398
+ min-width: max-content;
347
399
  border-collapse: collapse;
348
400
  font-size: 0.925rem;
349
401
  }
350
402
 
403
+ /* Mobile: smaller table font and padding */
404
+ @media (max-width: 640px) {
405
+ .prev-content table {
406
+ font-size: 0.85rem;
407
+ }
408
+
409
+ .prev-content th,
410
+ .prev-content td {
411
+ padding: 0.625rem 0.75rem;
412
+ }
413
+ }
414
+
351
415
  .prev-content thead {
352
416
  border-bottom: 2px solid var(--fd-border);
353
417
  }
@@ -357,6 +421,7 @@ body {
357
421
  text-align: left;
358
422
  font-weight: 600;
359
423
  background: var(--fd-muted);
424
+ white-space: nowrap;
360
425
  }
361
426
 
362
427
  .prev-content th:first-child {