prev-cli 0.24.11 → 0.24.13

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,528 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import type { PreviewUnit, AtlasDefinition } from '../../vite/preview-types'
3
+
4
+ interface AtlasPreviewProps {
5
+ unit: PreviewUnit
6
+ }
7
+
8
+ type ViewMode = 'tree' | 'map' | 'navigate'
9
+
10
+ export function AtlasPreview({ unit }: AtlasPreviewProps) {
11
+ const [atlas, setAtlas] = useState<AtlasDefinition | null>(null)
12
+ const [viewMode, setViewMode] = useState<ViewMode>('tree')
13
+ const [selectedArea, setSelectedArea] = useState<string | null>(null)
14
+ const [loading, setLoading] = useState(true)
15
+
16
+ // Load atlas definition
17
+ useEffect(() => {
18
+ fetch(`/_preview-config/atlas/${unit.name}`)
19
+ .then(res => res.json())
20
+ .then(data => {
21
+ setAtlas(data)
22
+ setSelectedArea(data.hierarchy?.root || null)
23
+ setLoading(false)
24
+ })
25
+ .catch(() => setLoading(false))
26
+ }, [unit.name])
27
+
28
+ if (loading) {
29
+ return (
30
+ <div style={{
31
+ padding: '32px',
32
+ textAlign: 'center',
33
+ color: 'var(--fd-muted-foreground)',
34
+ }}>
35
+ Loading atlas...
36
+ </div>
37
+ )
38
+ }
39
+
40
+ if (!atlas || !atlas.hierarchy) {
41
+ return (
42
+ <div style={{
43
+ padding: '32px',
44
+ textAlign: 'center',
45
+ color: 'oklch(0.65 0.15 85)',
46
+ }}>
47
+ <h2 style={{
48
+ margin: '0 0 8px 0',
49
+ fontSize: '18px',
50
+ fontWeight: 600,
51
+ }}>
52
+ {unit.config?.title || unit.name}
53
+ </h2>
54
+ <p style={{ margin: 0 }}>
55
+ {atlas ? 'This atlas has no hierarchy defined.' : 'Failed to load atlas definition.'}
56
+ </p>
57
+ </div>
58
+ )
59
+ }
60
+
61
+ // Fix 8: Tree view with cycle detection
62
+ const renderTree = (areaId: string, depth = 0, visited = new Set<string>()): React.ReactNode => {
63
+ // Cycle detection
64
+ if (visited.has(areaId)) {
65
+ return (
66
+ <div
67
+ key={`cycle-${areaId}-${depth}`}
68
+ style={{
69
+ marginLeft: `${depth * 24}px`,
70
+ padding: '8px 12px',
71
+ color: 'oklch(0.65 0.20 25)',
72
+ fontSize: '14px',
73
+ }}
74
+ >
75
+ Cycle detected: {areaId}
76
+ </div>
77
+ )
78
+ }
79
+ visited.add(areaId)
80
+
81
+ const area = atlas.hierarchy.areas[areaId]
82
+ if (!area) {
83
+ return (
84
+ <div
85
+ key={`missing-${areaId}-${depth}`}
86
+ style={{
87
+ marginLeft: `${depth * 24}px`,
88
+ padding: '8px 12px',
89
+ color: 'var(--fd-muted-foreground)',
90
+ fontSize: '14px',
91
+ fontStyle: 'italic',
92
+ }}
93
+ >
94
+ Missing area: {areaId}
95
+ </div>
96
+ )
97
+ }
98
+
99
+ const hasChildren = area.children && area.children.length > 0
100
+ const isSelected = selectedArea === areaId
101
+
102
+ return (
103
+ <div key={`${areaId}-${depth}`}>
104
+ <button
105
+ onClick={() => setSelectedArea(areaId)}
106
+ style={{
107
+ display: 'flex',
108
+ alignItems: 'center',
109
+ gap: '8px',
110
+ width: '100%',
111
+ textAlign: 'left',
112
+ marginLeft: `${depth * 24}px`,
113
+ padding: '8px 12px',
114
+ border: 'none',
115
+ borderRadius: '4px',
116
+ cursor: 'pointer',
117
+ backgroundColor: isSelected ? 'var(--fd-primary)' : 'transparent',
118
+ color: isSelected ? 'var(--fd-primary-foreground)' : 'var(--fd-foreground)',
119
+ fontSize: '14px',
120
+ fontWeight: isSelected ? 500 : 400,
121
+ transition: 'background-color 0.15s, color 0.15s',
122
+ }}
123
+ onMouseEnter={(e) => {
124
+ if (!isSelected) {
125
+ e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
126
+ }
127
+ }}
128
+ onMouseLeave={(e) => {
129
+ if (!isSelected) {
130
+ e.currentTarget.style.backgroundColor = 'transparent'
131
+ }
132
+ }}
133
+ >
134
+ <span style={{
135
+ width: '16px',
136
+ textAlign: 'center',
137
+ color: isSelected ? 'var(--fd-primary-foreground)' : 'var(--fd-muted-foreground)',
138
+ }}>
139
+ {hasChildren ? (depth === 0 ? '/' : '+') : '-'}
140
+ </span>
141
+ <span>{area.title}</span>
142
+ {area.access && (
143
+ <span style={{
144
+ marginLeft: 'auto',
145
+ padding: '2px 6px',
146
+ fontSize: '11px',
147
+ borderRadius: '3px',
148
+ backgroundColor: isSelected ? 'rgba(255,255,255,0.2)' : 'var(--fd-muted)',
149
+ color: isSelected ? 'var(--fd-primary-foreground)' : 'var(--fd-muted-foreground)',
150
+ }}>
151
+ {area.access}
152
+ </span>
153
+ )}
154
+ </button>
155
+ {hasChildren && area.children?.map(childId =>
156
+ renderTree(childId, depth + 1, new Set(visited))
157
+ )}
158
+ </div>
159
+ )
160
+ }
161
+
162
+ // Navigate view: sidebar + screen preview
163
+ const renderNavigateView = () => {
164
+ const selectedAreaData = selectedArea ? atlas.hierarchy.areas[selectedArea] : null
165
+ const routes = atlas.routes || {}
166
+ const areaRoutes = Object.entries(routes).filter(([, r]) => r.area === selectedArea)
167
+
168
+ return (
169
+ <div style={{
170
+ display: 'flex',
171
+ gap: '24px',
172
+ padding: '24px',
173
+ backgroundColor: 'var(--fd-muted)',
174
+ minHeight: '400px',
175
+ }}>
176
+ {/* Sidebar */}
177
+ <div style={{
178
+ width: '280px',
179
+ flexShrink: 0,
180
+ backgroundColor: 'var(--fd-background)',
181
+ borderRadius: '8px',
182
+ padding: '16px',
183
+ overflow: 'auto',
184
+ }}>
185
+ <h3 style={{
186
+ margin: '0 0 12px 0',
187
+ fontSize: '14px',
188
+ fontWeight: 600,
189
+ color: 'var(--fd-foreground)',
190
+ }}>
191
+ Areas
192
+ </h3>
193
+ {renderTree(atlas.hierarchy.root)}
194
+ </div>
195
+
196
+ {/* Screen preview area */}
197
+ <div style={{
198
+ flex: 1,
199
+ backgroundColor: 'var(--fd-background)',
200
+ borderRadius: '8px',
201
+ padding: '24px',
202
+ display: 'flex',
203
+ flexDirection: 'column',
204
+ }}>
205
+ {selectedAreaData ? (
206
+ <>
207
+ <div style={{ marginBottom: '16px' }}>
208
+ <h3 style={{
209
+ margin: '0 0 4px 0',
210
+ fontSize: '18px',
211
+ fontWeight: 600,
212
+ color: 'var(--fd-foreground)',
213
+ }}>
214
+ {selectedAreaData.title}
215
+ </h3>
216
+ {selectedAreaData.description && (
217
+ <p style={{
218
+ margin: 0,
219
+ fontSize: '14px',
220
+ color: 'var(--fd-muted-foreground)',
221
+ }}>
222
+ {selectedAreaData.description}
223
+ </p>
224
+ )}
225
+ </div>
226
+
227
+ {/* Routes in this area */}
228
+ {areaRoutes.length > 0 ? (
229
+ <div>
230
+ <h4 style={{
231
+ margin: '0 0 12px 0',
232
+ fontSize: '14px',
233
+ fontWeight: 500,
234
+ color: 'var(--fd-foreground)',
235
+ }}>
236
+ Routes
237
+ </h4>
238
+ <div style={{
239
+ display: 'flex',
240
+ flexDirection: 'column',
241
+ gap: '8px',
242
+ }}>
243
+ {areaRoutes.map(([path, route]) => (
244
+ <div
245
+ key={path}
246
+ style={{
247
+ padding: '12px',
248
+ backgroundColor: 'var(--fd-muted)',
249
+ borderRadius: '4px',
250
+ fontSize: '14px',
251
+ }}
252
+ >
253
+ <div style={{
254
+ fontFamily: 'var(--fd-font-mono)',
255
+ color: 'var(--fd-foreground)',
256
+ marginBottom: '4px',
257
+ }}>
258
+ {path}
259
+ </div>
260
+ <div style={{
261
+ display: 'flex',
262
+ gap: '8px',
263
+ color: 'var(--fd-muted-foreground)',
264
+ fontSize: '13px',
265
+ }}>
266
+ <span>Screen: {route.screen}</span>
267
+ {route.guard && <span>Guard: {route.guard}</span>}
268
+ </div>
269
+ </div>
270
+ ))}
271
+ </div>
272
+ </div>
273
+ ) : (
274
+ <div style={{
275
+ flex: 1,
276
+ display: 'flex',
277
+ alignItems: 'center',
278
+ justifyContent: 'center',
279
+ color: 'var(--fd-muted-foreground)',
280
+ fontSize: '14px',
281
+ }}>
282
+ No routes defined for this area
283
+ </div>
284
+ )}
285
+ </>
286
+ ) : (
287
+ <div style={{
288
+ flex: 1,
289
+ display: 'flex',
290
+ alignItems: 'center',
291
+ justifyContent: 'center',
292
+ color: 'var(--fd-muted-foreground)',
293
+ fontSize: '14px',
294
+ }}>
295
+ Select an area to view details
296
+ </div>
297
+ )}
298
+ </div>
299
+ </div>
300
+ )
301
+ }
302
+
303
+ // Map view: placeholder for future D2/Mermaid diagram
304
+ const renderMapView = () => {
305
+ return (
306
+ <div style={{
307
+ padding: '48px',
308
+ display: 'flex',
309
+ flexDirection: 'column',
310
+ alignItems: 'center',
311
+ justifyContent: 'center',
312
+ backgroundColor: 'var(--fd-muted)',
313
+ minHeight: '400px',
314
+ }}>
315
+ <div style={{
316
+ padding: '24px 32px',
317
+ backgroundColor: 'var(--fd-background)',
318
+ borderRadius: '8px',
319
+ textAlign: 'center',
320
+ }}>
321
+ <h3 style={{
322
+ margin: '0 0 8px 0',
323
+ fontSize: '16px',
324
+ fontWeight: 600,
325
+ color: 'var(--fd-foreground)',
326
+ }}>
327
+ Map View
328
+ </h3>
329
+ <p style={{
330
+ margin: 0,
331
+ fontSize: '14px',
332
+ color: 'var(--fd-muted-foreground)',
333
+ }}>
334
+ D2/Mermaid diagram visualization coming soon
335
+ </p>
336
+ </div>
337
+ </div>
338
+ )
339
+ }
340
+
341
+ // Tree view: full hierarchy
342
+ const renderTreeView = () => {
343
+ return (
344
+ <div style={{
345
+ padding: '24px',
346
+ backgroundColor: 'var(--fd-muted)',
347
+ }}>
348
+ <div style={{
349
+ backgroundColor: 'var(--fd-background)',
350
+ borderRadius: '8px',
351
+ padding: '16px',
352
+ }}>
353
+ {renderTree(atlas.hierarchy.root)}
354
+ </div>
355
+ </div>
356
+ )
357
+ }
358
+
359
+ const viewModeButtons: { mode: ViewMode; label: string }[] = [
360
+ { mode: 'tree', label: 'Tree' },
361
+ { mode: 'map', label: 'Map' },
362
+ { mode: 'navigate', label: 'Navigate' },
363
+ ]
364
+
365
+ return (
366
+ <div style={{
367
+ display: 'flex',
368
+ flexDirection: 'column',
369
+ border: '1px solid var(--fd-border)',
370
+ borderRadius: '8px',
371
+ overflow: 'hidden',
372
+ backgroundColor: 'var(--fd-background)',
373
+ }}>
374
+ {/* Header */}
375
+ <div style={{
376
+ display: 'flex',
377
+ alignItems: 'center',
378
+ justifyContent: 'space-between',
379
+ padding: '12px 16px',
380
+ backgroundColor: 'var(--fd-muted)',
381
+ borderBottom: '1px solid var(--fd-border)',
382
+ }}>
383
+ <div>
384
+ <h2 style={{
385
+ margin: 0,
386
+ fontSize: '18px',
387
+ fontWeight: 600,
388
+ color: 'var(--fd-foreground)',
389
+ }}>
390
+ {atlas.name}
391
+ </h2>
392
+ {atlas.description && (
393
+ <p style={{
394
+ margin: '4px 0 0 0',
395
+ fontSize: '14px',
396
+ color: 'var(--fd-muted-foreground)',
397
+ }}>
398
+ {atlas.description}
399
+ </p>
400
+ )}
401
+ </div>
402
+
403
+ {/* View mode toggle */}
404
+ <div style={{
405
+ display: 'flex',
406
+ gap: '4px',
407
+ }}>
408
+ {viewModeButtons.map(({ mode, label }) => (
409
+ <button
410
+ key={mode}
411
+ onClick={() => setViewMode(mode)}
412
+ style={{
413
+ padding: '6px 12px',
414
+ fontSize: '13px',
415
+ border: 'none',
416
+ borderRadius: '4px',
417
+ cursor: 'pointer',
418
+ backgroundColor: viewMode === mode ? 'var(--fd-primary)' : 'transparent',
419
+ color: viewMode === mode ? 'var(--fd-primary-foreground)' : 'var(--fd-muted-foreground)',
420
+ fontWeight: viewMode === mode ? 500 : 400,
421
+ transition: 'background-color 0.15s, color 0.15s',
422
+ }}
423
+ onMouseEnter={(e) => {
424
+ if (viewMode !== mode) {
425
+ e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
426
+ e.currentTarget.style.color = 'var(--fd-foreground)'
427
+ }
428
+ }}
429
+ onMouseLeave={(e) => {
430
+ if (viewMode !== mode) {
431
+ e.currentTarget.style.backgroundColor = 'transparent'
432
+ e.currentTarget.style.color = 'var(--fd-muted-foreground)'
433
+ }
434
+ }}
435
+ >
436
+ {label}
437
+ </button>
438
+ ))}
439
+ </div>
440
+ </div>
441
+
442
+ {/* Content based on view mode */}
443
+ {viewMode === 'tree' && renderTreeView()}
444
+ {viewMode === 'map' && renderMapView()}
445
+ {viewMode === 'navigate' && renderNavigateView()}
446
+
447
+ {/* Relationships section */}
448
+ {atlas.relationships && atlas.relationships.length > 0 && (
449
+ <div style={{
450
+ padding: '16px',
451
+ borderTop: '1px solid var(--fd-border)',
452
+ backgroundColor: 'var(--fd-muted)',
453
+ }}>
454
+ <h3 style={{
455
+ margin: '0 0 12px 0',
456
+ fontSize: '14px',
457
+ fontWeight: 500,
458
+ color: 'var(--fd-foreground)',
459
+ }}>
460
+ Relationships
461
+ </h3>
462
+ <div style={{
463
+ display: 'flex',
464
+ flexWrap: 'wrap',
465
+ gap: '8px',
466
+ }}>
467
+ {atlas.relationships.map((rel, i) => (
468
+ <div
469
+ key={i}
470
+ style={{
471
+ display: 'flex',
472
+ alignItems: 'center',
473
+ gap: '8px',
474
+ padding: '8px 12px',
475
+ backgroundColor: 'var(--fd-background)',
476
+ borderRadius: '4px',
477
+ fontSize: '13px',
478
+ }}
479
+ >
480
+ <span style={{ color: 'var(--fd-foreground)' }}>
481
+ {atlas.hierarchy.areas[rel.from]?.title || rel.from}
482
+ </span>
483
+ <span style={{
484
+ padding: '2px 6px',
485
+ backgroundColor: 'var(--fd-secondary)',
486
+ borderRadius: '3px',
487
+ color: 'var(--fd-secondary-foreground)',
488
+ fontSize: '11px',
489
+ }}>
490
+ {rel.type}
491
+ </span>
492
+ <span style={{ color: 'var(--fd-foreground)' }}>
493
+ {atlas.hierarchy.areas[rel.to]?.title || rel.to}
494
+ </span>
495
+ </div>
496
+ ))}
497
+ </div>
498
+ </div>
499
+ )}
500
+
501
+ {/* Tags */}
502
+ {unit.config?.tags && unit.config.tags.length > 0 && (
503
+ <div style={{
504
+ padding: '12px 16px',
505
+ borderTop: '1px solid var(--fd-border)',
506
+ display: 'flex',
507
+ gap: '8px',
508
+ flexWrap: 'wrap',
509
+ }}>
510
+ {unit.config.tags.map(tag => (
511
+ <span
512
+ key={tag}
513
+ style={{
514
+ padding: '2px 8px',
515
+ fontSize: '12px',
516
+ backgroundColor: 'var(--fd-secondary)',
517
+ color: 'var(--fd-secondary-foreground)',
518
+ borderRadius: '4px',
519
+ }}
520
+ >
521
+ {tag}
522
+ </span>
523
+ ))}
524
+ </div>
525
+ )}
526
+ </div>
527
+ )
528
+ }
@@ -0,0 +1,180 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import type { PreviewUnit } from '../../vite/preview-types'
3
+
4
+ interface ComponentPreviewProps {
5
+ unit: PreviewUnit
6
+ }
7
+
8
+ export function ComponentPreview({ unit }: ComponentPreviewProps) {
9
+ const [props, setProps] = useState<Record<string, unknown>>({})
10
+ const [schema, setSchema] = useState<unknown>(null)
11
+
12
+ // Load schema if available
13
+ useEffect(() => {
14
+ if (unit.files.schema) {
15
+ import(`/_preview/components/${unit.name}/${unit.files.schema}`)
16
+ .then(mod => setSchema(mod.schema))
17
+ .catch(() => {})
18
+ }
19
+ }, [unit])
20
+
21
+ const iframeUrl = `/_preview-runtime?preview=components/${unit.name}`
22
+
23
+ // Status badge colors
24
+ const getStatusStyle = (status: string): React.CSSProperties => {
25
+ switch (status) {
26
+ case 'stable':
27
+ return { backgroundColor: 'oklch(0.85 0.15 145)', color: 'oklch(0.30 0.10 145)' }
28
+ case 'deprecated':
29
+ return { backgroundColor: 'oklch(0.85 0.15 25)', color: 'oklch(0.35 0.15 25)' }
30
+ default: // draft
31
+ return { backgroundColor: 'oklch(0.85 0.15 85)', color: 'oklch(0.35 0.10 85)' }
32
+ }
33
+ }
34
+
35
+ return (
36
+ <div style={{
37
+ display: 'flex',
38
+ flexDirection: 'column',
39
+ border: '1px solid var(--fd-border)',
40
+ borderRadius: '8px',
41
+ overflow: 'hidden',
42
+ backgroundColor: 'var(--fd-background)',
43
+ }}>
44
+ {/* Header */}
45
+ <div style={{
46
+ display: 'flex',
47
+ alignItems: 'center',
48
+ justifyContent: 'space-between',
49
+ padding: '12px 16px',
50
+ backgroundColor: 'var(--fd-muted)',
51
+ borderBottom: '1px solid var(--fd-border)',
52
+ }}>
53
+ <div>
54
+ <h2 style={{
55
+ margin: 0,
56
+ fontSize: '18px',
57
+ fontWeight: 600,
58
+ color: 'var(--fd-foreground)',
59
+ }}>
60
+ {unit.config?.title || unit.name}
61
+ </h2>
62
+ {unit.config?.description && (
63
+ <p style={{
64
+ margin: '4px 0 0 0',
65
+ fontSize: '14px',
66
+ color: 'var(--fd-muted-foreground)',
67
+ }}>
68
+ {unit.config.description}
69
+ </p>
70
+ )}
71
+ </div>
72
+ {unit.config?.status && (
73
+ <span style={{
74
+ padding: '4px 8px',
75
+ fontSize: '12px',
76
+ fontWeight: 500,
77
+ borderRadius: '4px',
78
+ ...getStatusStyle(unit.config.status),
79
+ }}>
80
+ {unit.config.status}
81
+ </span>
82
+ )}
83
+ </div>
84
+
85
+ {/* Preview area */}
86
+ <div style={{
87
+ padding: '24px',
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ minHeight: '200px',
92
+ backgroundColor: 'var(--fd-background)',
93
+ backgroundImage: 'linear-gradient(45deg, var(--fd-border) 25%, transparent 25%), linear-gradient(-45deg, var(--fd-border) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--fd-border) 75%), linear-gradient(-45deg, transparent 75%, var(--fd-border) 75%)',
94
+ backgroundSize: '16px 16px',
95
+ backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
96
+ }}>
97
+ <div style={{
98
+ width: '100%',
99
+ height: '100%',
100
+ minHeight: '200px',
101
+ backgroundColor: 'var(--fd-background)',
102
+ }}>
103
+ <iframe
104
+ src={iframeUrl}
105
+ style={{
106
+ border: 'none',
107
+ width: '100%',
108
+ height: '100%',
109
+ minHeight: '200px',
110
+ }}
111
+ title={`Preview: ${unit.name}`}
112
+ />
113
+ </div>
114
+ </div>
115
+
116
+ {/* Props panel */}
117
+ {schema && (
118
+ <div style={{
119
+ padding: '16px',
120
+ borderTop: '1px solid var(--fd-border)',
121
+ backgroundColor: 'var(--fd-muted)',
122
+ }}>
123
+ <h3 style={{
124
+ margin: '0 0 8px 0',
125
+ fontSize: '14px',
126
+ fontWeight: 500,
127
+ color: 'var(--fd-foreground)',
128
+ }}>
129
+ Props
130
+ </h3>
131
+ <div style={{
132
+ display: 'grid',
133
+ gridTemplateColumns: 'repeat(2, 1fr)',
134
+ gap: '16px',
135
+ fontSize: '14px',
136
+ }}>
137
+ {/* TODO: Generate controls from schema */}
138
+ <pre style={{
139
+ margin: 0,
140
+ padding: '8px',
141
+ fontSize: '12px',
142
+ backgroundColor: 'var(--fd-card)',
143
+ borderRadius: '4px',
144
+ fontFamily: 'var(--fd-font-mono)',
145
+ overflow: 'auto',
146
+ }}>
147
+ {JSON.stringify(props, null, 2)}
148
+ </pre>
149
+ </div>
150
+ </div>
151
+ )}
152
+
153
+ {/* Tags */}
154
+ {unit.config?.tags && unit.config.tags.length > 0 && (
155
+ <div style={{
156
+ padding: '12px 16px',
157
+ borderTop: '1px solid var(--fd-border)',
158
+ display: 'flex',
159
+ gap: '8px',
160
+ flexWrap: 'wrap',
161
+ }}>
162
+ {unit.config.tags.map(tag => (
163
+ <span
164
+ key={tag}
165
+ style={{
166
+ padding: '2px 8px',
167
+ fontSize: '12px',
168
+ backgroundColor: 'var(--fd-secondary)',
169
+ color: 'var(--fd-secondary-foreground)',
170
+ borderRadius: '4px',
171
+ }}
172
+ >
173
+ {tag}
174
+ </span>
175
+ ))}
176
+ </div>
177
+ )}
178
+ </div>
179
+ )
180
+ }