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,270 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import type { PreviewUnit, FlowDefinition } from '../../vite/preview-types'
3
+
4
+ interface FlowPreviewProps {
5
+ unit: PreviewUnit
6
+ }
7
+
8
+ export function FlowPreview({ unit }: FlowPreviewProps) {
9
+ const [flow, setFlow] = useState<FlowDefinition | null>(null)
10
+ const [currentStep, setCurrentStep] = useState(0)
11
+ const [loading, setLoading] = useState(true)
12
+
13
+ // Load flow definition
14
+ useEffect(() => {
15
+ fetch(`/_preview-config/flows/${unit.name}`)
16
+ .then(res => res.json())
17
+ .then(data => {
18
+ setFlow(data)
19
+ setLoading(false)
20
+ })
21
+ .catch(() => setLoading(false))
22
+ }, [unit.name])
23
+
24
+ if (loading) {
25
+ return (
26
+ <div style={{
27
+ padding: '32px',
28
+ textAlign: 'center',
29
+ color: 'var(--fd-muted-foreground)',
30
+ }}>
31
+ Loading flow...
32
+ </div>
33
+ )
34
+ }
35
+
36
+ // Fix 3: Zero-step flow handling
37
+ if (!flow || flow.steps.length === 0) {
38
+ return (
39
+ <div style={{
40
+ padding: '32px',
41
+ textAlign: 'center',
42
+ color: 'oklch(0.65 0.15 85)', // yellow-ish warning color
43
+ }}>
44
+ <h2 style={{
45
+ margin: '0 0 8px 0',
46
+ fontSize: '18px',
47
+ fontWeight: 600,
48
+ }}>
49
+ {flow?.name || 'Flow'}
50
+ </h2>
51
+ <p style={{ margin: 0 }}>
52
+ {flow ? 'This flow has no steps defined.' : 'Failed to load flow definition.'}
53
+ </p>
54
+ </div>
55
+ )
56
+ }
57
+
58
+ const step = flow.steps[currentStep]
59
+ const totalSteps = flow.steps.length
60
+
61
+ // Build iframe URL for current step's screen
62
+ const iframeUrl = step
63
+ ? `/_preview-runtime?preview=screens/${step.screen}${step.state ? `&state=${step.state}` : ''}`
64
+ : ''
65
+
66
+ return (
67
+ <div style={{
68
+ display: 'flex',
69
+ flexDirection: 'column',
70
+ border: '1px solid var(--fd-border)',
71
+ borderRadius: '8px',
72
+ overflow: 'hidden',
73
+ backgroundColor: 'var(--fd-background)',
74
+ }}>
75
+ {/* Header */}
76
+ <div style={{
77
+ display: 'flex',
78
+ alignItems: 'center',
79
+ justifyContent: 'space-between',
80
+ padding: '12px 16px',
81
+ backgroundColor: 'var(--fd-muted)',
82
+ borderBottom: '1px solid var(--fd-border)',
83
+ }}>
84
+ <div>
85
+ <h2 style={{
86
+ margin: 0,
87
+ fontSize: '18px',
88
+ fontWeight: 600,
89
+ color: 'var(--fd-foreground)',
90
+ }}>
91
+ {flow.name}
92
+ </h2>
93
+ {flow.description && (
94
+ <p style={{
95
+ margin: '4px 0 0 0',
96
+ fontSize: '14px',
97
+ color: 'var(--fd-muted-foreground)',
98
+ }}>
99
+ {flow.description}
100
+ </p>
101
+ )}
102
+ </div>
103
+ <span style={{
104
+ fontSize: '14px',
105
+ color: 'var(--fd-muted-foreground)',
106
+ }}>
107
+ Step {currentStep + 1} of {totalSteps}
108
+ </span>
109
+ </div>
110
+
111
+ {/* Preview area */}
112
+ <div style={{
113
+ padding: '24px',
114
+ backgroundColor: 'var(--fd-muted)',
115
+ display: 'flex',
116
+ justifyContent: 'center',
117
+ overflow: 'auto',
118
+ }}>
119
+ <div style={{
120
+ width: '100%',
121
+ maxWidth: '896px',
122
+ backgroundColor: 'var(--fd-background)',
123
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
124
+ borderRadius: '4px',
125
+ overflow: 'hidden',
126
+ }}>
127
+ <iframe
128
+ src={iframeUrl}
129
+ style={{
130
+ width: '100%',
131
+ height: '500px',
132
+ border: 'none',
133
+ display: 'block',
134
+ }}
135
+ title={`Flow: ${flow.name} - Step ${currentStep + 1}`}
136
+ />
137
+ </div>
138
+ </div>
139
+
140
+ {/* Navigation */}
141
+ <div style={{
142
+ padding: '16px',
143
+ borderTop: '1px solid var(--fd-border)',
144
+ backgroundColor: 'var(--fd-muted)',
145
+ }}>
146
+ <div style={{
147
+ display: 'flex',
148
+ alignItems: 'center',
149
+ justifyContent: 'space-between',
150
+ maxWidth: '896px',
151
+ margin: '0 auto',
152
+ }}>
153
+ {/* Previous button */}
154
+ <button
155
+ onClick={() => setCurrentStep(s => Math.max(0, s - 1))}
156
+ disabled={currentStep === 0}
157
+ style={{
158
+ padding: '8px 16px',
159
+ fontSize: '14px',
160
+ border: 'none',
161
+ borderRadius: '4px',
162
+ cursor: currentStep === 0 ? 'not-allowed' : 'pointer',
163
+ backgroundColor: 'transparent',
164
+ color: currentStep === 0 ? 'var(--fd-muted-foreground)' : 'var(--fd-foreground)',
165
+ opacity: currentStep === 0 ? 0.5 : 1,
166
+ transition: 'background-color 0.15s',
167
+ }}
168
+ onMouseEnter={(e) => {
169
+ if (currentStep !== 0) {
170
+ e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
171
+ }
172
+ }}
173
+ onMouseLeave={(e) => {
174
+ e.currentTarget.style.backgroundColor = 'transparent'
175
+ }}
176
+ >
177
+ Previous
178
+ </button>
179
+
180
+ {/* Step dots */}
181
+ <div style={{
182
+ display: 'flex',
183
+ gap: '8px',
184
+ }}>
185
+ {flow.steps.map((_, i) => (
186
+ <button
187
+ key={i}
188
+ onClick={() => setCurrentStep(i)}
189
+ style={{
190
+ width: '12px',
191
+ height: '12px',
192
+ padding: 0,
193
+ border: 'none',
194
+ borderRadius: '50%',
195
+ cursor: 'pointer',
196
+ backgroundColor: i === currentStep
197
+ ? 'var(--fd-foreground)'
198
+ : 'var(--fd-border)',
199
+ transition: 'background-color 0.15s',
200
+ }}
201
+ title={`Step ${i + 1}`}
202
+ />
203
+ ))}
204
+ </div>
205
+
206
+ {/* Next button */}
207
+ <button
208
+ onClick={() => setCurrentStep(s => Math.min(totalSteps - 1, s + 1))}
209
+ disabled={currentStep === totalSteps - 1}
210
+ style={{
211
+ padding: '8px 16px',
212
+ fontSize: '14px',
213
+ border: 'none',
214
+ borderRadius: '4px',
215
+ cursor: currentStep === totalSteps - 1 ? 'not-allowed' : 'pointer',
216
+ backgroundColor: 'transparent',
217
+ color: currentStep === totalSteps - 1 ? 'var(--fd-muted-foreground)' : 'var(--fd-foreground)',
218
+ opacity: currentStep === totalSteps - 1 ? 0.5 : 1,
219
+ transition: 'background-color 0.15s',
220
+ }}
221
+ onMouseEnter={(e) => {
222
+ if (currentStep !== totalSteps - 1) {
223
+ e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
224
+ }
225
+ }}
226
+ onMouseLeave={(e) => {
227
+ e.currentTarget.style.backgroundColor = 'transparent'
228
+ }}
229
+ >
230
+ Next
231
+ </button>
232
+ </div>
233
+
234
+ {/* Step info */}
235
+ {step && (step.note || step.trigger) && (
236
+ <div style={{
237
+ marginTop: '16px',
238
+ padding: '12px',
239
+ backgroundColor: 'var(--fd-background)',
240
+ borderRadius: '4px',
241
+ maxWidth: '896px',
242
+ marginLeft: 'auto',
243
+ marginRight: 'auto',
244
+ }}>
245
+ {step.note && (
246
+ <p style={{
247
+ margin: 0,
248
+ fontSize: '14px',
249
+ color: 'var(--fd-foreground)',
250
+ }}>
251
+ <span style={{ marginRight: '8px' }}>Note:</span>
252
+ {step.note}
253
+ </p>
254
+ )}
255
+ {step.trigger && (
256
+ <p style={{
257
+ margin: step.note ? '8px 0 0 0' : 0,
258
+ fontSize: '14px',
259
+ color: 'var(--fd-muted-foreground)',
260
+ }}>
261
+ <span style={{ marginRight: '8px' }}>Trigger:</span>
262
+ {step.trigger}
263
+ </p>
264
+ )}
265
+ </div>
266
+ )}
267
+ </div>
268
+ </div>
269
+ )
270
+ }
@@ -0,0 +1,189 @@
1
+ import React from 'react'
2
+ import { previewUnits, getByType } from 'virtual:prev-previews'
3
+ import { ComponentPreview } from './ComponentPreview'
4
+ import { ScreenPreview } from './ScreenPreview'
5
+ import { FlowPreview } from './FlowPreview'
6
+ import { AtlasPreview } from './AtlasPreview'
7
+ import type { PreviewUnit } from '../../vite/preview-types'
8
+
9
+ interface PreviewRouterProps {
10
+ type: string
11
+ name: string
12
+ }
13
+
14
+ export function PreviewRouter({ type, name }: PreviewRouterProps) {
15
+ const unit = previewUnits.find(u => u.type === type && u.name === name)
16
+
17
+ if (!unit) {
18
+ return (
19
+ <div style={{
20
+ padding: '32px',
21
+ textAlign: 'center',
22
+ }}>
23
+ <h2 style={{
24
+ margin: '0 0 8px 0',
25
+ fontSize: '20px',
26
+ fontWeight: 600,
27
+ color: 'var(--fd-foreground)',
28
+ }}>
29
+ Preview not found
30
+ </h2>
31
+ <p style={{
32
+ margin: 0,
33
+ fontSize: '14px',
34
+ color: 'var(--fd-muted-foreground)',
35
+ }}>
36
+ No {type} named "{name}" found in previews/{type}s/{name}/
37
+ </p>
38
+ </div>
39
+ )
40
+ }
41
+
42
+ switch (unit.type) {
43
+ case 'component':
44
+ return <ComponentPreview unit={unit} />
45
+ case 'screen':
46
+ return <ScreenPreview unit={unit} />
47
+ case 'flow':
48
+ return <FlowPreview unit={unit} />
49
+ case 'atlas':
50
+ return <AtlasPreview unit={unit} />
51
+ default:
52
+ return (
53
+ <div style={{
54
+ padding: '32px',
55
+ textAlign: 'center',
56
+ color: 'var(--fd-muted-foreground)',
57
+ }}>
58
+ Unknown preview type: {unit.type}
59
+ </div>
60
+ )
61
+ }
62
+ }
63
+
64
+ // Preview list component for browsing
65
+ export function PreviewList({ type }: { type?: string }) {
66
+ const units = type ? getByType(type) : previewUnits
67
+
68
+ const grouped = units.reduce((acc, unit) => {
69
+ const key = unit.type
70
+ if (!acc[key]) acc[key] = []
71
+ acc[key].push(unit)
72
+ return acc
73
+ }, {} as Record<string, PreviewUnit[]>)
74
+
75
+ if (units.length === 0) {
76
+ return (
77
+ <div style={{
78
+ padding: '32px',
79
+ textAlign: 'center',
80
+ color: 'var(--fd-muted-foreground)',
81
+ }}>
82
+ {type ? `No ${type} previews found` : 'No previews found'}
83
+ </div>
84
+ )
85
+ }
86
+
87
+ return (
88
+ <div style={{
89
+ padding: '24px',
90
+ display: 'flex',
91
+ flexDirection: 'column',
92
+ gap: '32px',
93
+ }}>
94
+ {Object.entries(grouped).map(([groupType, groupUnits]) => (
95
+ <div key={groupType}>
96
+ <h2 style={{
97
+ margin: '0 0 16px 0',
98
+ fontSize: '20px',
99
+ fontWeight: 600,
100
+ color: 'var(--fd-foreground)',
101
+ textTransform: 'capitalize',
102
+ }}>
103
+ {groupType}s
104
+ </h2>
105
+ <div style={{
106
+ display: 'grid',
107
+ gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
108
+ gap: '16px',
109
+ }}>
110
+ {groupUnits.map(unit => (
111
+ <a
112
+ key={unit.route}
113
+ href={unit.route}
114
+ style={{
115
+ display: 'block',
116
+ padding: '16px',
117
+ border: '1px solid var(--fd-border)',
118
+ borderRadius: '8px',
119
+ backgroundColor: 'var(--fd-background)',
120
+ textDecoration: 'none',
121
+ color: 'inherit',
122
+ transition: 'border-color 0.15s, box-shadow 0.15s',
123
+ }}
124
+ onMouseEnter={(e) => {
125
+ e.currentTarget.style.borderColor = 'var(--fd-foreground)'
126
+ e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)'
127
+ }}
128
+ onMouseLeave={(e) => {
129
+ e.currentTarget.style.borderColor = 'var(--fd-border)'
130
+ e.currentTarget.style.boxShadow = 'none'
131
+ }}
132
+ >
133
+ <h3 style={{
134
+ margin: '0 0 4px 0',
135
+ fontSize: '16px',
136
+ fontWeight: 500,
137
+ color: 'var(--fd-foreground)',
138
+ }}>
139
+ {unit.config?.title || unit.name}
140
+ </h3>
141
+ {unit.config?.description && (
142
+ <p style={{
143
+ margin: '0 0 12px 0',
144
+ fontSize: '14px',
145
+ color: 'var(--fd-muted-foreground)',
146
+ lineHeight: 1.5,
147
+ }}>
148
+ {unit.config.description}
149
+ </p>
150
+ )}
151
+ {unit.config?.tags && unit.config.tags.length > 0 && (
152
+ <div style={{
153
+ display: 'flex',
154
+ gap: '6px',
155
+ flexWrap: 'wrap',
156
+ }}>
157
+ {unit.config.tags.slice(0, 3).map(tag => (
158
+ <span
159
+ key={tag}
160
+ style={{
161
+ padding: '2px 8px',
162
+ fontSize: '12px',
163
+ backgroundColor: 'var(--fd-secondary)',
164
+ color: 'var(--fd-secondary-foreground)',
165
+ borderRadius: '4px',
166
+ }}
167
+ >
168
+ {tag}
169
+ </span>
170
+ ))}
171
+ {unit.config.tags.length > 3 && (
172
+ <span style={{
173
+ padding: '2px 8px',
174
+ fontSize: '12px',
175
+ color: 'var(--fd-muted-foreground)',
176
+ }}>
177
+ +{unit.config.tags.length - 3}
178
+ </span>
179
+ )}
180
+ </div>
181
+ )}
182
+ </a>
183
+ ))}
184
+ </div>
185
+ </div>
186
+ ))}
187
+ </div>
188
+ )
189
+ }