prev-cli 0.24.20 → 0.25.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.
Files changed (150) hide show
  1. package/dist/cli.js +2006 -1714
  2. package/dist/previews/components/cart-item/index.d.ts +5 -0
  3. package/dist/previews/components/price-tag/index.d.ts +6 -0
  4. package/dist/previews/screens/cart/empty.d.ts +1 -0
  5. package/dist/previews/screens/cart/index.d.ts +1 -0
  6. package/dist/previews/screens/payment/error.d.ts +1 -0
  7. package/dist/previews/screens/payment/index.d.ts +1 -0
  8. package/dist/previews/screens/payment/processing.d.ts +1 -0
  9. package/dist/previews/screens/receipt/index.d.ts +1 -0
  10. package/dist/previews/shared/data.d.ts +30 -0
  11. package/dist/src/content/config-parser.d.ts +30 -0
  12. package/dist/src/content/flow-verifier.d.ts +21 -0
  13. package/dist/src/content/preview-types.d.ts +288 -0
  14. package/dist/{vite → src/content}/previews.d.ts +3 -11
  15. package/dist/{preview-runtime → src/preview-runtime}/build-optimized.d.ts +2 -0
  16. package/dist/{preview-runtime → src/preview-runtime}/build.d.ts +1 -1
  17. package/dist/src/preview-runtime/region-bridge.d.ts +1 -0
  18. package/dist/{preview-runtime → src/preview-runtime}/types.d.ts +18 -0
  19. package/dist/src/preview-runtime/vendors.d.ts +11 -0
  20. package/dist/{renderers → src/renderers}/index.d.ts +1 -1
  21. package/dist/{renderers → src/renderers}/types.d.ts +3 -31
  22. package/dist/src/server/build.d.ts +6 -0
  23. package/dist/src/server/dev.d.ts +13 -0
  24. package/dist/src/server/plugins/aliases.d.ts +5 -0
  25. package/dist/src/server/plugins/mdx.d.ts +5 -0
  26. package/dist/src/server/plugins/virtual-modules.d.ts +8 -0
  27. package/dist/src/server/preview.d.ts +10 -0
  28. package/dist/src/server/routes/component-bundle.d.ts +1 -0
  29. package/dist/src/server/routes/jsx-bundle.d.ts +3 -0
  30. package/dist/src/server/routes/og-image.d.ts +15 -0
  31. package/dist/src/server/routes/preview-bundle.d.ts +1 -0
  32. package/dist/src/server/routes/preview-config.d.ts +1 -0
  33. package/dist/src/server/routes/tokens.d.ts +1 -0
  34. package/dist/{vite → src/server}/start.d.ts +5 -2
  35. package/dist/{ui → src/ui}/button.d.ts +1 -1
  36. package/dist/{validators → src/validators}/index.d.ts +0 -5
  37. package/dist/{validators → src/validators}/semantic-validator.d.ts +2 -3
  38. package/package.json +8 -11
  39. package/src/jsx/CLAUDE.md +18 -0
  40. package/src/jsx/jsx-runtime.ts +1 -1
  41. package/src/preview-runtime/CLAUDE.md +21 -0
  42. package/src/preview-runtime/build-optimized.ts +189 -73
  43. package/src/preview-runtime/build.ts +75 -79
  44. package/src/preview-runtime/fast-template.html +5 -1
  45. package/src/preview-runtime/region-bridge.test.ts +41 -0
  46. package/src/preview-runtime/region-bridge.ts +101 -0
  47. package/src/preview-runtime/types.ts +6 -0
  48. package/src/preview-runtime/vendors.ts +215 -22
  49. package/src/primitives/CLAUDE.md +17 -0
  50. package/src/theme/CLAUDE.md +20 -0
  51. package/src/theme/Preview.tsx +10 -4
  52. package/src/theme/Toolbar.tsx +2 -2
  53. package/src/theme/entry.tsx +247 -121
  54. package/src/theme/hooks/useAnnotations.ts +77 -0
  55. package/src/theme/hooks/useApprovalStatus.ts +50 -0
  56. package/src/theme/hooks/useSnapshots.ts +147 -0
  57. package/src/theme/hooks/useStorage.ts +26 -0
  58. package/src/theme/hooks/useTokenOverrides.ts +56 -0
  59. package/src/theme/hooks/useViewport.ts +23 -0
  60. package/src/theme/icons.tsx +39 -1
  61. package/src/theme/index.html +18 -0
  62. package/src/theme/mdx-components.tsx +1 -1
  63. package/src/theme/previews/AnnotationLayer.tsx +285 -0
  64. package/src/theme/previews/AnnotationPin.tsx +61 -0
  65. package/src/theme/previews/AnnotationThread.tsx +257 -0
  66. package/src/theme/previews/CLAUDE.md +18 -0
  67. package/src/theme/previews/ComponentPreview.tsx +487 -107
  68. package/src/theme/previews/FlowDiagram.tsx +111 -0
  69. package/src/theme/previews/FlowPreview.tsx +938 -174
  70. package/src/theme/previews/PreviewRouter.tsx +1 -4
  71. package/src/theme/previews/ScreenPreview.tsx +515 -175
  72. package/src/theme/previews/SnapshotButton.tsx +68 -0
  73. package/src/theme/previews/SnapshotCompare.tsx +216 -0
  74. package/src/theme/previews/SnapshotPanel.tsx +274 -0
  75. package/src/theme/previews/StatusBadge.tsx +66 -0
  76. package/src/theme/previews/StatusDropdown.tsx +158 -0
  77. package/src/theme/previews/TokenPlayground.tsx +438 -0
  78. package/src/theme/previews/ViewportControls.tsx +67 -0
  79. package/src/theme/previews/flow-diagram.test.ts +141 -0
  80. package/src/theme/previews/flow-diagram.ts +109 -0
  81. package/src/theme/previews/flow-navigation.test.ts +90 -0
  82. package/src/theme/previews/flow-navigation.ts +47 -0
  83. package/src/theme/previews/machines/derived.test.ts +225 -0
  84. package/src/theme/previews/machines/derived.ts +73 -0
  85. package/src/theme/previews/machines/flow-machine.test.ts +379 -0
  86. package/src/theme/previews/machines/flow-machine.ts +207 -0
  87. package/src/theme/previews/machines/screen-machine.test.ts +149 -0
  88. package/src/theme/previews/machines/screen-machine.ts +76 -0
  89. package/src/theme/previews/stores/flow-store.test.ts +157 -0
  90. package/src/theme/previews/stores/flow-store.ts +49 -0
  91. package/src/theme/previews/stores/screen-store.test.ts +68 -0
  92. package/src/theme/previews/stores/screen-store.ts +33 -0
  93. package/src/theme/storage.test.ts +97 -0
  94. package/src/theme/storage.ts +71 -0
  95. package/src/theme/styles.css +296 -25
  96. package/src/theme/types.ts +64 -0
  97. package/src/tokens/CLAUDE.md +16 -0
  98. package/src/tokens/resolver.ts +1 -1
  99. package/dist/preview-runtime/vendors.d.ts +0 -6
  100. package/dist/vite/config-parser.d.ts +0 -13
  101. package/dist/vite/config.d.ts +0 -12
  102. package/dist/vite/plugins/config-plugin.d.ts +0 -3
  103. package/dist/vite/plugins/debug-plugin.d.ts +0 -3
  104. package/dist/vite/plugins/entry-plugin.d.ts +0 -2
  105. package/dist/vite/plugins/fumadocs-plugin.d.ts +0 -9
  106. package/dist/vite/plugins/pages-plugin.d.ts +0 -5
  107. package/dist/vite/plugins/previews-plugin.d.ts +0 -2
  108. package/dist/vite/plugins/tokens-plugin.d.ts +0 -2
  109. package/dist/vite/preview-types.d.ts +0 -70
  110. package/src/theme/previews/AtlasPreview.tsx +0 -528
  111. package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
  112. package/dist/{config → src/config}/index.d.ts +0 -0
  113. package/dist/{config → src/config}/loader.d.ts +0 -0
  114. package/dist/{config → src/config}/schema.d.ts +0 -0
  115. package/dist/{vite → src/content}/pages.d.ts +0 -0
  116. package/dist/{jsx → src/jsx}/adapters/html.d.ts +0 -0
  117. package/dist/{jsx → src/jsx}/adapters/react.d.ts +0 -0
  118. package/dist/{jsx → src/jsx}/define-component.d.ts +0 -0
  119. package/dist/{jsx → src/jsx}/index.d.ts +0 -0
  120. package/dist/{jsx → src/jsx}/jsx-runtime.d.ts +0 -0
  121. package/dist/{jsx → src/jsx}/migrate.d.ts +0 -0
  122. package/dist/{jsx → src/jsx}/schemas/index.d.ts +0 -0
  123. package/dist/{jsx → src/jsx}/schemas/primitives.d.ts +10 -10
  124. package/dist/{jsx → src/jsx}/schemas/tokens.d.ts +3 -3
  125. /package/dist/{jsx → src/jsx}/validation.d.ts +0 -0
  126. /package/dist/{jsx → src/jsx}/vnode.d.ts +0 -0
  127. /package/dist/{migrate.d.ts → src/migrate.d.ts} +0 -0
  128. /package/dist/{preview-runtime → src/preview-runtime}/tailwind.d.ts +0 -0
  129. /package/dist/{primitives → src/primitives}/index.d.ts +0 -0
  130. /package/dist/{primitives → src/primitives}/migrate.d.ts +0 -0
  131. /package/dist/{primitives → src/primitives}/parser.d.ts +0 -0
  132. /package/dist/{primitives → src/primitives}/template-parser.d.ts +0 -0
  133. /package/dist/{primitives → src/primitives}/template-renderer.d.ts +0 -0
  134. /package/dist/{primitives → src/primitives}/types.d.ts +0 -0
  135. /package/dist/{renderers → src/renderers}/html/index.d.ts +0 -0
  136. /package/dist/{renderers → src/renderers}/react/index.d.ts +0 -0
  137. /package/dist/{renderers → src/renderers}/registry.d.ts +0 -0
  138. /package/dist/{renderers → src/renderers}/render.d.ts +0 -0
  139. /package/dist/{tokens → src/tokens}/defaults.d.ts +0 -0
  140. /package/dist/{tokens → src/tokens}/resolver.d.ts +0 -0
  141. /package/dist/{tokens → src/tokens}/utils.d.ts +0 -0
  142. /package/dist/{tokens → src/tokens}/validation.d.ts +0 -0
  143. /package/dist/{typecheck → src/typecheck}/index.d.ts +0 -0
  144. /package/dist/{ui → src/ui}/card.d.ts +0 -0
  145. /package/dist/{ui → src/ui}/index.d.ts +0 -0
  146. /package/dist/{ui → src/ui}/utils.d.ts +0 -0
  147. /package/dist/{utils → src/utils}/cache.d.ts +0 -0
  148. /package/dist/{utils → src/utils}/debug.d.ts +0 -0
  149. /package/dist/{utils → src/utils}/port.d.ts +0 -0
  150. /package/dist/{validators → src/validators}/schema-validator.d.ts +0 -0
@@ -1,270 +1,1034 @@
1
- import React, { useState, useEffect } from 'react'
2
- import type { PreviewUnit, FlowDefinition } from '../../vite/preview-types'
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react'
2
+ import type { PreviewUnit, FlowDefinition, FlowConfig, FlowStep } from '../../content/preview-types'
3
+ import { resolveRegionClick, navigateBack, canLinearNext } from './flow-navigation'
4
+ import { REGION_BRIDGE_SCRIPT } from '../../preview-runtime/region-bridge'
5
+ import { useViewport, VIEWPORT_WIDTHS } from '../hooks/useViewport'
6
+ import { ViewportControls } from './ViewportControls'
7
+ import { useApprovalStatus } from '../hooks/useApprovalStatus'
8
+ import { StatusDropdown } from './StatusDropdown'
9
+ import { AnnotationLayer } from './AnnotationLayer'
10
+ import { SnapshotButton } from './SnapshotButton'
11
+ import { SnapshotPanel } from './SnapshotPanel'
12
+ import { useSnapshots } from '../hooks/useSnapshots'
13
+ import { useTokenOverrides } from '../hooks/useTokenOverrides'
14
+ import { TokenPlayground } from './TokenPlayground'
15
+ import { FlowDiagram } from './FlowDiagram'
16
+ import { Icon } from '../icons'
17
+ import { tokens as designTokens } from 'virtual:prev-tokens'
3
18
 
4
19
  interface FlowPreviewProps {
5
20
  unit: PreviewUnit
21
+ initialStep?: string
6
22
  }
7
23
 
8
- export function FlowPreview({ unit }: FlowPreviewProps) {
9
- const [flow, setFlow] = useState<FlowDefinition | null>(null)
10
- const [currentStep, setCurrentStep] = useState(0)
24
+ interface FlowData {
25
+ name: string
26
+ description?: string
27
+ steps: FlowStep[]
28
+ _verification?: { errors: string[]; warnings: string[] }
29
+ }
30
+
31
+ // Detect if running in static build (no dev server)
32
+ const isStaticBuild = typeof window !== 'undefined' &&
33
+ !window.location.hostname.includes('localhost') &&
34
+ !window.location.hostname.includes('127.0.0.1')
35
+
36
+ export function FlowPreview({ unit, initialStep }: FlowPreviewProps) {
37
+ const [flow, setFlow] = useState<FlowData | null>(null)
38
+ const [currentStepId, setCurrentStepId] = useState<string | null>(null)
39
+ const [history, setHistory] = useState<string[]>([])
11
40
  const [loading, setLoading] = useState(true)
41
+ const [isFullscreen, setIsFullscreen] = useState(false)
42
+ const [outcomePicker, setOutcomePicker] = useState<{
43
+ outcomes: Record<string, { goto: string; label?: string }>
44
+ } | null>(null)
45
+ const [regionRects, setRegionRects] = useState<Array<{ name: string; x: number; y: number; width: number; height: number }>>([])
46
+ const [showOverlay, setShowOverlay] = useState(true)
47
+ const [viewport, setViewport] = useViewport()
48
+ const { status: approvalStatus, changeStatus, getAuditLog } = useApprovalStatus(`flows/${unit.name}`)
49
+ const [annotationsEnabled, setAnnotationsEnabled] = useState(false)
50
+ const [showSnapshots, setShowSnapshots] = useState(false)
51
+ const [showTokens, setShowTokens] = useState(false)
52
+ const [showFlowMap, setShowFlowMap] = useState(false)
53
+ const { snapshots, captureSnapshot, deleteSnapshot } = useSnapshots(`flows/${unit.name}`)
54
+ const { overrides: tokenOverrides, setOverride, removeOverride, resetAll, toCssOverrides } = useTokenOverrides()
55
+ const iframeRef = useRef<HTMLIFrameElement>(null)
56
+ const fullscreenIframeRef = useRef<HTMLIFrameElement>(null)
12
57
 
13
58
  // Load flow definition
14
59
  useEffect(() => {
60
+ const config = unit.config as FlowConfig | undefined
61
+ if (config?.steps && config.steps.length > 0) {
62
+ const data: FlowData = {
63
+ name: config.title || unit.name,
64
+ description: config.description,
65
+ steps: config.steps,
66
+ }
67
+ setFlow(data)
68
+ const targetId = initialStep && config.steps.some(s => s.id === initialStep)
69
+ ? initialStep
70
+ : config.steps[0].id || 'step-0'
71
+ setCurrentStepId(targetId)
72
+ setHistory([targetId])
73
+ setLoading(false)
74
+ return
75
+ }
76
+
15
77
  fetch(`/_preview-config/flows/${unit.name}`)
16
78
  .then(res => res.json())
17
- .then(data => {
18
- setFlow(data)
79
+ .then((data: FlowData & { _verification?: FlowData['_verification'] }) => {
80
+ // Handle both legacy FlowDefinition and new FlowConfig format
81
+ const steps = data.steps || (data as unknown as FlowDefinition).steps || []
82
+ const flowData: FlowData = {
83
+ name: data.name || (data as unknown as FlowDefinition).name || unit.name,
84
+ description: data.description,
85
+ steps,
86
+ _verification: data._verification,
87
+ }
88
+ setFlow(flowData)
89
+ if (steps.length > 0) {
90
+ const targetId = initialStep && steps.some(s => s.id === initialStep)
91
+ ? initialStep
92
+ : steps[0].id || 'step-0'
93
+ setCurrentStepId(targetId)
94
+ setHistory([targetId])
95
+ }
19
96
  setLoading(false)
20
97
  })
21
98
  .catch(() => setLoading(false))
22
- }, [unit.name])
99
+ }, [unit.name, unit.config])
100
+
101
+ // Inject bridge script into iframe after load
102
+ const injectBridge = useCallback((iframe: HTMLIFrameElement | null) => {
103
+ if (!iframe) return
104
+ const tryInject = () => {
105
+ try {
106
+ const doc = iframe.contentDocument
107
+ if (!doc) return
108
+ // Inject bridge if not already present
109
+ if (!doc.querySelector('[data-region-bridge]')) {
110
+ const script = doc.createElement('script')
111
+ script.setAttribute('data-region-bridge', 'true')
112
+ script.textContent = REGION_BRIDGE_SCRIPT
113
+ doc.body.appendChild(script)
114
+ }
115
+
116
+ // Highlight available regions for current step
117
+ const step = currentStep
118
+ if (step?.regions) {
119
+ iframe.contentWindow?.postMessage({
120
+ type: 'highlight-regions',
121
+ regions: Object.keys(step.regions),
122
+ }, '*')
123
+ }
124
+ } catch {
125
+ // cross-origin — can't inject, bridge must be loaded separately
126
+ }
127
+ }
128
+ // Try immediately and also on load
129
+ tryInject()
130
+ iframe.addEventListener('load', tryInject)
131
+ return () => iframe.removeEventListener('load', tryInject)
132
+ }, [flow, currentStepId])
133
+
134
+ // Handle messages from iframe (region-click and region-rects)
135
+ useEffect(() => {
136
+ const handleMessage = (e: MessageEvent) => {
137
+ if (!e.data) return
138
+
139
+ if (e.data.type === 'region-rects') {
140
+ setRegionRects(e.data.rects || [])
141
+ return
142
+ }
143
+
144
+ if (e.data.type !== 'region-click') return
145
+ if (!flow || !currentStep) return
146
+
147
+ const result = resolveRegionClick(currentStep, e.data.region)
148
+ if (!result) return
149
+
150
+ if (result.type === 'goto') {
151
+ goToStep(result.stepId)
152
+ } else if (result.type === 'pick') {
153
+ setOutcomePicker({ outcomes: result.outcomes })
154
+ }
155
+ }
156
+
157
+ window.addEventListener('message', handleMessage)
158
+ return () => window.removeEventListener('message', handleMessage)
159
+ }, [flow, currentStepId])
160
+
161
+ // Sync overlay toggle with iframe highlights
162
+ useEffect(() => {
163
+ const iframe = isFullscreen ? fullscreenIframeRef.current : iframeRef.current
164
+ if (!iframe?.contentWindow || !currentStep) return
165
+ if (showOverlay && currentStep.regions) {
166
+ iframe.contentWindow.postMessage({
167
+ type: 'highlight-regions',
168
+ regions: Object.keys(currentStep.regions),
169
+ }, '*')
170
+ } else {
171
+ iframe.contentWindow.postMessage({
172
+ type: 'highlight-regions',
173
+ regions: [],
174
+ }, '*')
175
+ }
176
+ }, [showOverlay])
177
+
178
+ // Derived state
179
+ const steps = flow?.steps || []
180
+ const currentStepIndex = steps.findIndex(s => s.id === currentStepId)
181
+ const currentStep = currentStepIndex >= 0 ? steps[currentStepIndex] : null
182
+ const hasRegions = currentStep?.regions && Object.keys(currentStep.regions).length > 0
183
+
184
+ const goToStep = (stepId: string) => {
185
+ setCurrentStepId(stepId)
186
+ setHistory(prev => [...prev, stepId])
187
+ setOutcomePicker(null)
188
+ setRegionRects([]) // Clear overlay rects — will be re-reported after iframe loads
189
+ // Highlight regions for new step
190
+ setTimeout(() => {
191
+ const iframe = isFullscreen ? fullscreenIframeRef.current : iframeRef.current
192
+ const newStep = steps.find(s => s.id === stepId)
193
+ if (iframe && newStep?.regions) {
194
+ iframe.contentWindow?.postMessage({
195
+ type: 'highlight-regions',
196
+ regions: Object.keys(newStep.regions),
197
+ }, '*')
198
+ }
199
+ }, 500) // Wait for iframe to load new content
200
+ }
201
+
202
+ const handleBack = () => {
203
+ const result = navigateBack(history)
204
+ if (result) {
205
+ setCurrentStepId(result.stepId)
206
+ setHistory(result.history)
207
+ setOutcomePicker(null)
208
+ }
209
+ }
210
+
211
+ const handleLinearNext = () => {
212
+ if (currentStepIndex < steps.length - 1) {
213
+ const nextStep = steps[currentStepIndex + 1]
214
+ const nextId = nextStep.id || `step-${currentStepIndex + 1}`
215
+ goToStep(nextId)
216
+ }
217
+ }
218
+
219
+ const handleLinearPrev = () => {
220
+ if (currentStepIndex > 0) {
221
+ handleBack()
222
+ }
223
+ }
224
+
225
+ // Send token overrides to iframe
226
+ useEffect(() => {
227
+ const iframe = isFullscreen ? fullscreenIframeRef.current : iframeRef.current
228
+ if (!iframe?.contentWindow) return
229
+ const css = toCssOverrides()
230
+ iframe.contentWindow.postMessage({ type: 'token-overrides', css }, '*')
231
+ }, [tokenOverrides, isFullscreen, currentStepId])
232
+
233
+ // Build iframe URL
234
+ const screenName = currentStep
235
+ ? (typeof currentStep.screen === 'string' ? currentStep.screen : currentStep.screen.ref)
236
+ : ''
237
+ const basePath = typeof window !== 'undefined'
238
+ ? (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '')
239
+ : ''
240
+ const staticStatePath = currentStep?.state ? `${currentStep.state}/` : ''
241
+ const iframeUrl = currentStep
242
+ ? (isStaticBuild
243
+ ? `${basePath}/_preview/screens/${screenName}/${staticStatePath}`
244
+ : `/_preview-runtime?src=screens/${screenName}${currentStep.state ? `&state=${currentStep.state}` : ''}`)
245
+ : ''
23
246
 
24
247
  if (loading) {
25
248
  return (
26
249
  <div style={{
27
- padding: '32px',
28
- textAlign: 'center',
29
- color: 'var(--fd-muted-foreground)',
250
+ padding: '48px',
251
+ display: 'flex',
252
+ alignItems: 'center',
253
+ justifyContent: 'center',
254
+ backgroundColor: 'var(--fd-card)',
255
+ borderRadius: '16px',
30
256
  }}>
31
- Loading flow...
257
+ <div style={{
258
+ width: '32px',
259
+ height: '32px',
260
+ border: '2px solid var(--fd-border)',
261
+ borderTopColor: 'var(--fd-primary)',
262
+ borderRadius: '50%',
263
+ animation: 'spin 0.8s linear infinite',
264
+ }} />
32
265
  </div>
33
266
  )
34
267
  }
35
268
 
36
- // Fix 3: Zero-step flow handling
37
- if (!flow || flow.steps.length === 0) {
269
+ if (!flow || steps.length === 0) {
38
270
  return (
39
271
  <div style={{
40
- padding: '32px',
272
+ padding: '48px',
41
273
  textAlign: 'center',
42
- color: 'oklch(0.65 0.15 85)', // yellow-ish warning color
274
+ backgroundColor: 'var(--fd-card)',
275
+ borderRadius: '16px',
276
+ border: '1px solid var(--fd-border)',
43
277
  }}>
278
+ <div style={{
279
+ width: '56px',
280
+ height: '56px',
281
+ borderRadius: '50%',
282
+ backgroundColor: 'oklch(0.94 0.06 85)',
283
+ display: 'flex',
284
+ alignItems: 'center',
285
+ justifyContent: 'center',
286
+ margin: '0 auto 16px',
287
+ fontSize: '24px',
288
+ }}>
289
+ !
290
+ </div>
44
291
  <h2 style={{
45
292
  margin: '0 0 8px 0',
46
293
  fontSize: '18px',
47
294
  fontWeight: 600,
295
+ color: 'var(--fd-foreground)',
48
296
  }}>
49
297
  {flow?.name || 'Flow'}
50
298
  </h2>
51
- <p style={{ margin: 0 }}>
299
+ <p style={{
300
+ margin: 0,
301
+ fontSize: '14px',
302
+ color: 'var(--fd-muted-foreground)',
303
+ }}>
52
304
  {flow ? 'This flow has no steps defined.' : 'Failed to load flow definition.'}
53
305
  </p>
54
306
  </div>
55
307
  )
56
308
  }
57
309
 
58
- const step = flow.steps[currentStep]
59
- const totalSteps = flow.steps.length
310
+ const verificationWarnings = flow._verification?.warnings || []
311
+ const verificationErrors = flow._verification?.errors || []
60
312
 
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
- : ''
313
+ // Region overlay click handler same logic as region-click from iframe
314
+ const handleOverlayRegionClick = (regionName: string) => {
315
+ if (!currentStep) return
316
+ const result = resolveRegionClick(currentStep, regionName)
317
+ if (!result) return
318
+ if (result.type === 'goto') {
319
+ goToStep(result.stepId)
320
+ } else if (result.type === 'pick') {
321
+ setOutcomePicker({ outcomes: result.outcomes })
322
+ }
323
+ }
65
324
 
66
- return (
325
+ // Figma-style region overlay rendered over the iframe
326
+ const RegionOverlay = showOverlay && regionRects.length > 0 && (
67
327
  <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)',
328
+ position: 'absolute',
329
+ inset: 0,
330
+ pointerEvents: 'none',
331
+ zIndex: 5,
74
332
  }}>
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',
333
+ {regionRects.map((rect, i) => (
334
+ <div key={`${rect.name}-${i}`} style={{ position: 'absolute' }}>
335
+ {/* Label badge */}
336
+ <div style={{
337
+ position: 'absolute',
338
+ left: `${rect.x}px`,
339
+ top: `${rect.y - 24}px`,
340
+ fontSize: '11px',
88
341
  fontWeight: 600,
89
- color: 'var(--fd-foreground)',
342
+ fontFamily: 'system-ui, sans-serif',
343
+ color: 'white',
344
+ backgroundColor: 'oklch(0.55 0.25 250)',
345
+ padding: '2px 8px',
346
+ borderRadius: '6px 6px 0 0',
347
+ whiteSpace: 'nowrap',
348
+ pointerEvents: 'none',
349
+ lineHeight: '18px',
90
350
  }}>
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
- )}
351
+ {rect.name}
352
+ </div>
353
+ {/* Clickable region rectangle */}
354
+ <div
355
+ onClick={() => handleOverlayRegionClick(rect.name)}
356
+ style={{
357
+ position: 'absolute',
358
+ left: `${rect.x}px`,
359
+ top: `${rect.y}px`,
360
+ width: `${rect.width}px`,
361
+ height: `${rect.height}px`,
362
+ backgroundColor: 'oklch(0.65 0.25 250 / 0.12)',
363
+ border: '2px solid oklch(0.65 0.25 250)',
364
+ borderRadius: '4px',
365
+ cursor: 'pointer',
366
+ pointerEvents: 'auto',
367
+ animation: 'region-pulse 1.5s ease-out',
368
+ transition: 'background-color 0.15s ease',
369
+ }}
370
+ onMouseEnter={e => {
371
+ e.currentTarget.style.backgroundColor = 'oklch(0.65 0.25 250 / 0.22)'
372
+ }}
373
+ onMouseLeave={e => {
374
+ e.currentTarget.style.backgroundColor = 'oklch(0.65 0.25 250 / 0.12)'
375
+ }}
376
+ title={`Navigate: ${rect.name}`}
377
+ />
102
378
  </div>
103
- <span style={{
104
- fontSize: '14px',
105
- color: 'var(--fd-muted-foreground)',
106
- }}>
107
- Step {currentStep + 1} of {totalSteps}
108
- </span>
379
+ ))}
380
+ <style>{`
381
+ @keyframes region-pulse {
382
+ 0% { opacity: 0; transform: scale(1.04); }
383
+ 50% { opacity: 1; transform: scale(1); }
384
+ 100% { opacity: 1; }
385
+ }
386
+ `}</style>
387
+ </div>
388
+ )
389
+
390
+ // Outcome picker overlay
391
+ const OutcomePicker = outcomePicker && (
392
+ <div style={{
393
+ position: 'absolute',
394
+ bottom: '24px',
395
+ left: '50%',
396
+ transform: 'translateX(-50%)',
397
+ backgroundColor: 'var(--fd-card)',
398
+ borderRadius: '12px',
399
+ boxShadow: '0 8px 32px -8px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(0, 0, 0, 0.08)',
400
+ padding: '16px',
401
+ zIndex: 10,
402
+ minWidth: '200px',
403
+ }}>
404
+ <p style={{
405
+ margin: '0 0 12px',
406
+ fontSize: '12px',
407
+ fontWeight: 600,
408
+ color: 'var(--fd-muted-foreground)',
409
+ textTransform: 'uppercase',
410
+ letterSpacing: '0.05em',
411
+ }}>
412
+ Choose outcome
413
+ </p>
414
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
415
+ {Object.entries(outcomePicker.outcomes).map(([key, outcome]) => (
416
+ <button
417
+ key={key}
418
+ onClick={() => goToStep(outcome.goto)}
419
+ style={{
420
+ padding: '8px 16px',
421
+ fontSize: '13px',
422
+ fontWeight: 500,
423
+ border: '1px solid var(--fd-border)',
424
+ borderRadius: '8px',
425
+ cursor: 'pointer',
426
+ backgroundColor: 'var(--fd-background)',
427
+ color: 'var(--fd-foreground)',
428
+ textAlign: 'left',
429
+ transition: 'all 0.15s ease',
430
+ }}
431
+ onMouseEnter={e => {
432
+ e.currentTarget.style.backgroundColor = 'var(--fd-primary)'
433
+ e.currentTarget.style.color = 'var(--fd-primary-foreground)'
434
+ e.currentTarget.style.borderColor = 'var(--fd-primary)'
435
+ }}
436
+ onMouseLeave={e => {
437
+ e.currentTarget.style.backgroundColor = 'var(--fd-background)'
438
+ e.currentTarget.style.color = 'var(--fd-foreground)'
439
+ e.currentTarget.style.borderColor = 'var(--fd-border)'
440
+ }}
441
+ >
442
+ {outcome.label || key}
443
+ </button>
444
+ ))}
109
445
  </div>
446
+ <button
447
+ onClick={() => setOutcomePicker(null)}
448
+ style={{
449
+ marginTop: '8px',
450
+ padding: '4px 8px',
451
+ fontSize: '11px',
452
+ border: 'none',
453
+ borderRadius: '4px',
454
+ cursor: 'pointer',
455
+ backgroundColor: 'transparent',
456
+ color: 'var(--fd-muted-foreground)',
457
+ width: '100%',
458
+ }}
459
+ >
460
+ Cancel
461
+ </button>
462
+ </div>
463
+ )
110
464
 
111
- {/* Preview area */}
465
+ // Fullscreen mode
466
+ if (isFullscreen) {
467
+ return (
112
468
  <div style={{
113
- padding: '24px',
114
- backgroundColor: 'var(--fd-muted)',
469
+ position: 'fixed',
470
+ inset: 0,
471
+ zIndex: 50,
472
+ backgroundColor: 'oklch(0.12 0.01 260)',
115
473
  display: 'flex',
116
- justifyContent: 'center',
117
- overflow: 'auto',
474
+ flexDirection: 'column',
118
475
  }}>
476
+ {/* Fullscreen header */}
119
477
  <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',
478
+ display: 'flex',
479
+ alignItems: 'center',
480
+ justifyContent: 'space-between',
481
+ padding: '12px 20px',
482
+ backgroundColor: 'oklch(0.18 0.01 260)',
483
+ borderBottom: '1px solid oklch(0.25 0.01 260)',
126
484
  }}>
485
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
486
+ {history.length > 1 && (
487
+ <button
488
+ onClick={handleBack}
489
+ style={{
490
+ padding: '6px 10px',
491
+ fontSize: '12px',
492
+ border: '1px solid oklch(0.35 0.01 260)',
493
+ borderRadius: '6px',
494
+ cursor: 'pointer',
495
+ backgroundColor: 'oklch(0.25 0.01 260)',
496
+ color: 'oklch(0.9 0 0)',
497
+ }}
498
+ >
499
+ Back
500
+ </button>
501
+ )}
502
+ <span style={{ fontSize: '14px', fontWeight: 600, color: 'oklch(0.95 0 0)' }}>
503
+ {flow.name}
504
+ </span>
505
+ {currentStep?.title && (
506
+ <span style={{
507
+ padding: '2px 8px',
508
+ fontSize: '11px',
509
+ backgroundColor: 'oklch(0.25 0.01 260)',
510
+ color: 'oklch(0.7 0 0)',
511
+ borderRadius: '4px',
512
+ }}>
513
+ {currentStep.title}
514
+ </span>
515
+ )}
516
+ </div>
517
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
518
+ {/* Step indicator */}
519
+ <span style={{ fontSize: '12px', color: 'oklch(0.6 0 0)' }}>
520
+ Step {currentStepIndex + 1} of {steps.length}
521
+ </span>
522
+ {hasRegions && (
523
+ <button
524
+ onClick={() => setShowOverlay(prev => !prev)}
525
+ style={{
526
+ padding: '6px 12px',
527
+ backgroundColor: showOverlay ? 'oklch(0.35 0.12 250)' : 'oklch(0.25 0.01 260)',
528
+ border: '1px solid oklch(0.35 0.01 260)',
529
+ borderRadius: '6px',
530
+ cursor: 'pointer',
531
+ fontSize: '12px',
532
+ color: 'oklch(0.9 0 0)',
533
+ }}
534
+ title={showOverlay ? 'Hide region overlay' : 'Show region overlay'}
535
+ >
536
+ {showOverlay ? '\u{1F441} Overlay' : '\u{1F441}\u{FE0F}\u{200D}\u{1F5E8}\u{FE0F} Overlay'}
537
+ </button>
538
+ )}
539
+ <button
540
+ onClick={() => setIsFullscreen(false)}
541
+ style={{
542
+ padding: '8px 16px',
543
+ backgroundColor: 'oklch(0.25 0.01 260)',
544
+ border: 'none',
545
+ borderRadius: '8px',
546
+ cursor: 'pointer',
547
+ fontSize: '13px',
548
+ fontWeight: 500,
549
+ color: 'oklch(0.9 0 0)',
550
+ }}
551
+ >
552
+ Exit Fullscreen
553
+ </button>
554
+ </div>
555
+ </div>
556
+ <div style={{ flex: 1, position: 'relative' }}>
127
557
  <iframe
558
+ ref={el => {
559
+ (fullscreenIframeRef as React.MutableRefObject<HTMLIFrameElement | null>).current = el
560
+ injectBridge(el)
561
+ }}
128
562
  src={iframeUrl}
129
563
  style={{
130
564
  width: '100%',
131
- height: '500px',
565
+ height: '100%',
132
566
  border: 'none',
133
- display: 'block',
567
+ backgroundColor: 'white',
568
+ position: 'relative',
569
+ zIndex: 0,
134
570
  }}
135
- title={`Flow: ${flow.name} - Step ${currentStep + 1}`}
571
+ title={`Flow: ${flow.name} - ${currentStep?.title || 'Step'}`}
136
572
  />
573
+ {RegionOverlay}
574
+ {OutcomePicker}
137
575
  </div>
138
576
  </div>
577
+ )
578
+ }
139
579
 
140
- {/* Navigation */}
580
+ return (
581
+ <div style={{
582
+ display: 'flex',
583
+ flexDirection: 'column',
584
+ borderRadius: '16px',
585
+ backgroundColor: 'var(--fd-card)',
586
+ boxShadow: '0 4px 24px -4px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.04)',
587
+ flex: 1,
588
+ minHeight: 0,
589
+ }}>
590
+ {/* Verification warnings banner */}
591
+ {(verificationErrors.length > 0 || verificationWarnings.length > 0) && (
592
+ <div style={{
593
+ padding: '8px 16px',
594
+ backgroundColor: verificationErrors.length > 0 ? 'oklch(0.94 0.08 25)' : 'oklch(0.94 0.06 85)',
595
+ borderBottom: '1px solid var(--fd-border)',
596
+ fontSize: '12px',
597
+ color: verificationErrors.length > 0 ? 'oklch(0.45 0.15 25)' : 'oklch(0.45 0.12 85)',
598
+ }}>
599
+ {verificationErrors.map((e, i) => <div key={`e${i}`}>Error: {e}</div>)}
600
+ {verificationWarnings.map((w, i) => <div key={`w${i}`}>Warning: {w}</div>)}
601
+ </div>
602
+ )}
603
+
604
+ {/* Compact header with inline controls */}
141
605
  <div style={{
142
- padding: '16px',
143
- borderTop: '1px solid var(--fd-border)',
606
+ padding: '12px 16px',
144
607
  backgroundColor: 'var(--fd-muted)',
608
+ borderBottom: '1px solid var(--fd-border)',
609
+ display: 'flex',
610
+ alignItems: 'center',
611
+ justifyContent: 'space-between',
612
+ gap: '12px',
145
613
  }}>
614
+ {/* Left: Back + Flow name and step info */}
615
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0 }}>
616
+ {history.length > 1 && (
617
+ <button
618
+ onClick={handleBack}
619
+ style={{
620
+ padding: '4px 8px',
621
+ fontSize: '12px',
622
+ fontWeight: 500,
623
+ border: '1px solid var(--fd-border)',
624
+ borderRadius: '6px',
625
+ cursor: 'pointer',
626
+ backgroundColor: 'var(--fd-background)',
627
+ color: 'var(--fd-foreground)',
628
+ flexShrink: 0,
629
+ }}
630
+ >
631
+ Back
632
+ </button>
633
+ )}
634
+ <div style={{
635
+ width: '28px',
636
+ height: '28px',
637
+ borderRadius: '6px',
638
+ background: 'linear-gradient(135deg, oklch(0.60 0.18 170) 0%, oklch(0.50 0.20 200) 100%)',
639
+ display: 'flex',
640
+ alignItems: 'center',
641
+ justifyContent: 'center',
642
+ color: 'white',
643
+ fontSize: '14px',
644
+ flexShrink: 0,
645
+ }}>
646
+ {hasRegions ? '\u2B95' : '\u21E2'}
647
+ </div>
648
+ <div style={{ minWidth: 0 }}>
649
+ <h2 style={{
650
+ margin: 0,
651
+ fontSize: '14px',
652
+ fontWeight: 600,
653
+ color: 'var(--fd-foreground)',
654
+ whiteSpace: 'nowrap',
655
+ overflow: 'hidden',
656
+ textOverflow: 'ellipsis',
657
+ }}>
658
+ {flow.name}
659
+ </h2>
660
+ {currentStep?.title && (
661
+ <p style={{
662
+ margin: 0,
663
+ fontSize: '12px',
664
+ color: 'var(--fd-muted-foreground)',
665
+ whiteSpace: 'nowrap',
666
+ overflow: 'hidden',
667
+ textOverflow: 'ellipsis',
668
+ }}>
669
+ {currentStep.title}
670
+ {hasRegions && (
671
+ <span style={{
672
+ marginLeft: '8px',
673
+ padding: '1px 6px',
674
+ fontSize: '10px',
675
+ backgroundColor: 'oklch(0.92 0.08 250)',
676
+ color: 'oklch(0.45 0.18 250)',
677
+ borderRadius: '4px',
678
+ }}>
679
+ interactive
680
+ </span>
681
+ )}
682
+ </p>
683
+ )}
684
+ </div>
685
+ </div>
686
+
687
+ <StatusDropdown
688
+ previewName={`flows/${unit.name}`}
689
+ status={approvalStatus}
690
+ onStatusChange={changeStatus}
691
+ getAuditLog={getAuditLog}
692
+ />
693
+
694
+ {/* Center: Progress dots */}
146
695
  <div style={{
147
696
  display: 'flex',
148
697
  alignItems: 'center',
149
- justifyContent: 'space-between',
150
- maxWidth: '896px',
151
- margin: '0 auto',
698
+ gap: '3px',
152
699
  }}>
153
- {/* Previous button */}
700
+ {steps.map((s, i) => {
701
+ const isVisited = history.includes(s.id || `step-${i}`)
702
+ const isCurrent = i === currentStepIndex
703
+ return (
704
+ <React.Fragment key={s.id || i}>
705
+ <button
706
+ onClick={() => goToStep(s.id || `step-${i}`)}
707
+ style={{
708
+ display: 'flex',
709
+ alignItems: 'center',
710
+ justifyContent: 'center',
711
+ width: '24px',
712
+ height: '24px',
713
+ borderRadius: '50%',
714
+ border: 'none',
715
+ cursor: 'pointer',
716
+ fontSize: '10px',
717
+ fontWeight: 600,
718
+ backgroundColor: isCurrent
719
+ ? 'var(--fd-primary)'
720
+ : isVisited
721
+ ? 'oklch(0.65 0.18 155)'
722
+ : 'var(--fd-background)',
723
+ color: isCurrent || isVisited
724
+ ? 'white'
725
+ : 'var(--fd-muted-foreground)',
726
+ transition: 'all 0.15s ease',
727
+ }}
728
+ title={s.title || `Step ${i + 1}`}
729
+ >
730
+ {isVisited && !isCurrent ? '\u2713' : i + 1}
731
+ </button>
732
+ {i < steps.length - 1 && (
733
+ <div style={{
734
+ width: '12px',
735
+ height: '2px',
736
+ backgroundColor: isVisited && history.includes(steps[i + 1]?.id || `step-${i + 1}`)
737
+ ? 'oklch(0.65 0.18 155)'
738
+ : 'var(--fd-border)',
739
+ }} />
740
+ )}
741
+ </React.Fragment>
742
+ )
743
+ })}
744
+ </div>
745
+
746
+ {/* Right: Annotations + Snapshots + Tokens + FlowMap + Viewport + Navigation + fullscreen */}
747
+ <div style={{ display: 'flex', gap: '4px', flexShrink: 0, alignItems: 'center' }}>
154
748
  <button
155
- onClick={() => setCurrentStep(s => Math.max(0, s - 1))}
156
- disabled={currentStep === 0}
749
+ onClick={() => setAnnotationsEnabled(prev => !prev)}
157
750
  style={{
158
- padding: '8px 16px',
159
- fontSize: '14px',
751
+ display: 'flex',
752
+ alignItems: 'center',
753
+ justifyContent: 'center',
754
+ padding: '5px',
160
755
  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',
756
+ borderRadius: '6px',
757
+ cursor: 'pointer',
758
+ backgroundColor: annotationsEnabled ? 'oklch(0.92 0.08 250)' : 'var(--fd-background)',
759
+ color: annotationsEnabled ? 'oklch(0.45 0.18 250)' : 'var(--fd-muted-foreground)',
167
760
  }}
168
- onMouseEnter={(e) => {
169
- if (currentStep !== 0) {
170
- e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
171
- }
761
+ title={annotationsEnabled ? 'Disable annotations' : 'Enable annotations'}
762
+ >
763
+ <Icon name="pin" size={13} />
764
+ </button>
765
+ <SnapshotButton onCapture={() => captureSnapshot(iframeRef, {
766
+ previewName: `flows/${unit.name}`,
767
+ stateOrStep: currentStepId || 'step-0',
768
+ viewport,
769
+ })} />
770
+ <button
771
+ onClick={() => setShowSnapshots(prev => !prev)}
772
+ style={{
773
+ display: 'flex',
774
+ alignItems: 'center',
775
+ justifyContent: 'center',
776
+ padding: '5px',
777
+ border: 'none',
778
+ borderRadius: '6px',
779
+ cursor: 'pointer',
780
+ backgroundColor: 'var(--fd-background)',
781
+ color: 'var(--fd-muted-foreground)',
782
+ position: 'relative',
172
783
  }}
173
- onMouseLeave={(e) => {
174
- e.currentTarget.style.backgroundColor = 'transparent'
784
+ title="View snapshots"
785
+ >
786
+ <Icon name="camera" size={13} />
787
+ {snapshots.length > 0 && (
788
+ <span style={{
789
+ position: 'absolute',
790
+ top: '-3px',
791
+ right: '-3px',
792
+ width: '14px',
793
+ height: '14px',
794
+ borderRadius: '50%',
795
+ backgroundColor: 'var(--fd-primary)',
796
+ color: 'var(--fd-primary-foreground)',
797
+ fontSize: '8px',
798
+ fontWeight: 700,
799
+ display: 'flex',
800
+ alignItems: 'center',
801
+ justifyContent: 'center',
802
+ }}>{snapshots.length}</span>
803
+ )}
804
+ </button>
805
+ <button
806
+ onClick={() => setShowTokens(prev => !prev)}
807
+ style={{
808
+ display: 'flex',
809
+ alignItems: 'center',
810
+ justifyContent: 'center',
811
+ padding: '5px',
812
+ border: 'none',
813
+ borderRadius: '6px',
814
+ cursor: 'pointer',
815
+ backgroundColor: showTokens ? 'oklch(0.92 0.08 310)' : 'var(--fd-background)',
816
+ color: showTokens ? 'oklch(0.45 0.18 310)' : 'var(--fd-muted-foreground)',
175
817
  }}
818
+ title="Token playground"
176
819
  >
177
- Previous
820
+ <Icon name="palette" size={13} />
178
821
  </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
822
  <button
208
- onClick={() => setCurrentStep(s => Math.min(totalSteps - 1, s + 1))}
209
- disabled={currentStep === totalSteps - 1}
823
+ onClick={() => setShowFlowMap(prev => !prev)}
210
824
  style={{
211
- padding: '8px 16px',
212
- fontSize: '14px',
825
+ display: 'flex',
826
+ alignItems: 'center',
827
+ justifyContent: 'center',
828
+ padding: '5px',
213
829
  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',
830
+ borderRadius: '6px',
831
+ cursor: 'pointer',
832
+ backgroundColor: showFlowMap ? 'oklch(0.92 0.08 170)' : 'var(--fd-background)',
833
+ color: showFlowMap ? 'oklch(0.40 0.15 170)' : 'var(--fd-muted-foreground)',
220
834
  }}
221
- onMouseEnter={(e) => {
222
- if (currentStep !== totalSteps - 1) {
223
- e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
224
- }
835
+ title="Flow map"
836
+ >
837
+ <Icon name="map" size={13} />
838
+ </button>
839
+ <ViewportControls viewport={viewport} onViewportChange={setViewport} />
840
+ <button
841
+ onClick={handleLinearPrev}
842
+ disabled={currentStepIndex <= 0}
843
+ style={{
844
+ padding: '6px 10px',
845
+ fontSize: '12px',
846
+ fontWeight: 500,
847
+ border: '1px solid var(--fd-border)',
848
+ borderRadius: '6px',
849
+ cursor: currentStepIndex <= 0 ? 'not-allowed' : 'pointer',
850
+ backgroundColor: 'var(--fd-background)',
851
+ color: currentStepIndex <= 0 ? 'var(--fd-muted-foreground)' : 'var(--fd-foreground)',
852
+ opacity: currentStepIndex <= 0 ? 0.5 : 1,
225
853
  }}
226
- onMouseLeave={(e) => {
227
- e.currentTarget.style.backgroundColor = 'transparent'
854
+ >
855
+ {'\u2190'}
856
+ </button>
857
+ {currentStep && canLinearNext(currentStep) && (
858
+ <button
859
+ onClick={handleLinearNext}
860
+ disabled={currentStepIndex >= steps.length - 1}
861
+ style={{
862
+ padding: '6px 10px',
863
+ fontSize: '12px',
864
+ fontWeight: 500,
865
+ border: 'none',
866
+ borderRadius: '6px',
867
+ cursor: currentStepIndex >= steps.length - 1 ? 'not-allowed' : 'pointer',
868
+ backgroundColor: currentStepIndex >= steps.length - 1 ? 'var(--fd-border)' : 'var(--fd-primary)',
869
+ color: currentStepIndex >= steps.length - 1 ? 'var(--fd-muted-foreground)' : 'var(--fd-primary-foreground)',
870
+ opacity: currentStepIndex >= steps.length - 1 ? 0.5 : 1,
871
+ }}
872
+ >
873
+ {'\u2192'}
874
+ </button>
875
+ )}
876
+ {hasRegions && (
877
+ <button
878
+ onClick={() => setShowOverlay(prev => !prev)}
879
+ style={{
880
+ padding: '6px 10px',
881
+ fontSize: '12px',
882
+ fontWeight: 500,
883
+ border: '1px solid var(--fd-border)',
884
+ borderRadius: '6px',
885
+ cursor: 'pointer',
886
+ backgroundColor: showOverlay ? 'oklch(0.92 0.08 250)' : 'var(--fd-background)',
887
+ color: showOverlay ? 'oklch(0.45 0.18 250)' : 'var(--fd-foreground)',
888
+ }}
889
+ title={showOverlay ? 'Hide region overlay' : 'Show region overlay'}
890
+ >
891
+ {showOverlay ? '\u25C9' : '\u25CE'}
892
+ </button>
893
+ )}
894
+ <button
895
+ onClick={() => setIsFullscreen(true)}
896
+ style={{
897
+ padding: '6px 10px',
898
+ fontSize: '12px',
899
+ fontWeight: 500,
900
+ border: '1px solid var(--fd-border)',
901
+ borderRadius: '6px',
902
+ cursor: 'pointer',
903
+ backgroundColor: 'var(--fd-background)',
904
+ color: 'var(--fd-foreground)',
228
905
  }}
906
+ title="Fullscreen"
229
907
  >
230
- Next
908
+ {'\u26F6'}
231
909
  </button>
232
910
  </div>
911
+ </div>
233
912
 
234
- {/* Step info */}
235
- {step && (step.note || step.trigger) && (
913
+ {/* Preview canvas */}
914
+ <div style={{
915
+ padding: '16px',
916
+ backgroundColor: 'oklch(0.15 0.01 260)',
917
+ backgroundImage: `
918
+ linear-gradient(oklch(0.20 0.01 260) 1px, transparent 1px),
919
+ linear-gradient(90deg, oklch(0.20 0.01 260) 1px, transparent 1px)
920
+ `,
921
+ backgroundSize: '16px 16px',
922
+ display: 'flex',
923
+ justifyContent: 'center',
924
+ position: 'relative',
925
+ flex: 1,
926
+ minHeight: 0,
927
+ }}>
928
+ <div style={{
929
+ width: viewport === 'desktop' ? '100%' : VIEWPORT_WIDTHS[viewport],
930
+ maxWidth: '100%',
931
+ backgroundColor: 'var(--fd-card)',
932
+ borderRadius: '8px',
933
+ boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.1), 0 4px 20px -4px rgba(0, 0, 0, 0.3)',
934
+ display: 'flex',
935
+ flexDirection: 'column',
936
+ flex: 1,
937
+ minHeight: 0,
938
+ transition: 'width 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
939
+ }}>
940
+ {/* Minimal browser chrome */}
236
941
  <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',
942
+ display: 'flex',
943
+ alignItems: 'center',
944
+ gap: '6px',
945
+ padding: '6px 10px',
946
+ backgroundColor: 'var(--fd-muted)',
947
+ borderBottom: '1px solid var(--fd-border)',
244
948
  }}>
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
- )}
949
+ <div style={{ display: 'flex', gap: '4px' }}>
950
+ <div style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: 'oklch(0.70 0.18 25)' }} />
951
+ <div style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: 'oklch(0.80 0.15 85)' }} />
952
+ <div style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: 'oklch(0.70 0.18 145)' }} />
953
+ </div>
954
+ <div style={{
955
+ flex: 1,
956
+ marginLeft: '6px',
957
+ padding: '3px 8px',
958
+ backgroundColor: 'var(--fd-background)',
959
+ borderRadius: '4px',
960
+ fontSize: '9px',
961
+ fontFamily: 'var(--fd-font-mono)',
962
+ color: 'var(--fd-muted-foreground)',
963
+ }}>
964
+ {screenName}{currentStep?.state ? `/${currentStep.state}` : ''}
965
+ </div>
265
966
  </div>
266
- )}
967
+
968
+ <AnnotationLayer
969
+ previewName={`flows/${unit.name}`}
970
+ stateOrStep={currentStepId || 'step-0'}
971
+ enabled={annotationsEnabled}
972
+ >
973
+ <div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
974
+ <iframe
975
+ ref={el => {
976
+ (iframeRef as React.MutableRefObject<HTMLIFrameElement | null>).current = el
977
+ injectBridge(el)
978
+ }}
979
+ src={iframeUrl}
980
+ style={{
981
+ width: '100%',
982
+ height: '100%',
983
+ border: 'none',
984
+ display: 'block',
985
+ backgroundColor: 'white',
986
+ }}
987
+ title={`Flow: ${flow.name} - ${currentStep?.title || `Step ${currentStepIndex + 1}`}`}
988
+ />
989
+ {RegionOverlay}
990
+ </div>
991
+ </AnnotationLayer>
992
+ </div>
993
+
994
+ {/* Outcome picker floating panel */}
995
+ {OutcomePicker}
267
996
  </div>
997
+
998
+ {/* Flow diagram panel */}
999
+ {showFlowMap && (
1000
+ <div style={{
1001
+ padding: '16px',
1002
+ borderTop: '1px solid var(--fd-border)',
1003
+ backgroundColor: 'var(--fd-card)',
1004
+ }}>
1005
+ <FlowDiagram
1006
+ steps={steps}
1007
+ currentStepId={currentStepId}
1008
+ visitedStepIds={history}
1009
+ onStepClick={goToStep}
1010
+ />
1011
+ </div>
1012
+ )}
1013
+
1014
+ {/* Panels */}
1015
+ {showSnapshots && (
1016
+ <SnapshotPanel
1017
+ snapshots={snapshots}
1018
+ onDelete={deleteSnapshot}
1019
+ onClose={() => setShowSnapshots(false)}
1020
+ />
1021
+ )}
1022
+ {showTokens && (
1023
+ <TokenPlayground
1024
+ tokens={designTokens}
1025
+ overrides={tokenOverrides}
1026
+ onSetOverride={setOverride}
1027
+ onRemoveOverride={removeOverride}
1028
+ onResetAll={resetAll}
1029
+ onClose={() => setShowTokens(false)}
1030
+ />
1031
+ )}
268
1032
  </div>
269
1033
  )
270
1034
  }