prev-cli 0.24.19 → 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 -1703
  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,26 +1,128 @@
1
- import React, { useState } from 'react'
2
- import type { PreviewUnit } from '../../vite/preview-types'
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import type { PreviewUnit } from '../../content/preview-types'
3
+ import type { PreviewConfig, PreviewMessage, BuildResult } from '../../preview-runtime/types'
4
+ import { useViewport, VIEWPORT_WIDTHS } from '../hooks/useViewport'
5
+ import { ViewportControls } from './ViewportControls'
6
+ import { useApprovalStatus } from '../hooks/useApprovalStatus'
7
+ import { StatusDropdown } from './StatusDropdown'
8
+ import { AnnotationLayer } from './AnnotationLayer'
9
+ import { SnapshotButton } from './SnapshotButton'
10
+ import { SnapshotPanel } from './SnapshotPanel'
11
+ import { useSnapshots } from '../hooks/useSnapshots'
12
+ import { useTokenOverrides } from '../hooks/useTokenOverrides'
13
+ import { TokenPlayground } from './TokenPlayground'
14
+ import { Icon } from '../icons'
15
+ import { tokens as designTokens } from 'virtual:prev-tokens'
3
16
 
4
17
  interface ScreenPreviewProps {
5
18
  unit: PreviewUnit
6
19
  initialState?: string
7
20
  }
8
21
 
9
- type Viewport = 'mobile' | 'tablet' | 'desktop'
10
-
11
- const viewports: Record<Viewport, { width: number; label: string }> = {
12
- mobile: { width: 375, label: 'Mobile' },
13
- tablet: { width: 768, label: 'Tablet' },
14
- desktop: { width: 1280, label: 'Desktop' },
15
- }
22
+ // Detect if running in static build (no dev server)
23
+ const isStaticBuild = typeof window !== 'undefined' &&
24
+ !window.location.hostname.includes('localhost') &&
25
+ !window.location.hostname.includes('127.0.0.1')
16
26
 
17
27
  export function ScreenPreview({ unit, initialState }: ScreenPreviewProps) {
18
28
  const states = ['index', ...(unit.files.states || []).map(s => s.replace(/\.(tsx|jsx)$/, ''))]
19
29
  const [activeState, setActiveState] = useState(initialState || 'index')
20
- const [viewport, setViewport] = useState<Viewport>('desktop')
30
+ const [viewport, setViewport] = useViewport()
31
+ const { status: approvalStatus, changeStatus, getAuditLog } = useApprovalStatus(`screens/${unit.name}`)
21
32
  const [isFullscreen, setIsFullscreen] = useState(false)
33
+ const [buildStatus, setBuildStatus] = useState<'loading' | 'building' | 'ready' | 'error'>('loading')
34
+ const [buildError, setBuildError] = useState<string | null>(null)
35
+ const [iframeLoaded, setIframeLoaded] = useState(false)
36
+ const [annotationsEnabled, setAnnotationsEnabled] = useState(false)
37
+ const [showSnapshots, setShowSnapshots] = useState(false)
38
+ const [showTokens, setShowTokens] = useState(false)
39
+ const { snapshots, captureSnapshot, deleteSnapshot } = useSnapshots(`screens/${unit.name}`)
40
+ const { overrides: tokenOverrides, setOverride, removeOverride, resetAll, toCssOverrides } = useTokenOverrides()
41
+ const iframeRef = useRef<HTMLIFrameElement>(null)
42
+ const fullscreenIframeRef = useRef<HTMLIFrameElement>(null)
43
+
44
+ // Build iframe URL - use static path for production, runtime for dev
45
+ const baseUrl = typeof window !== 'undefined'
46
+ ? (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '')
47
+ : ''
48
+ // For static builds: default state is in root, other states are in subdirs
49
+ const staticStatePath = activeState === 'index' ? '' : `${activeState}/`
50
+ const iframeUrl = isStaticBuild
51
+ ? `${baseUrl}/_preview/screens/${unit.name}/${staticStatePath}`
52
+ : `/_preview-runtime?preview=screens/${unit.name}&state=${activeState}`
53
+
54
+ // Skip spinner immediately for static builds (runs after hydration)
55
+ useEffect(() => {
56
+ if (isStaticBuild) {
57
+ setBuildStatus('ready')
58
+ }
59
+ }, [])
60
+
61
+ // Track iframe load state for opacity hint
62
+ useEffect(() => {
63
+ const iframe = isFullscreen ? fullscreenIframeRef.current : iframeRef.current
64
+ if (!iframe) return
65
+
66
+ const handleLoad = () => setIframeLoaded(true)
67
+ iframe.addEventListener('load', handleLoad)
68
+ return () => iframe.removeEventListener('load', handleLoad)
69
+ }, [isFullscreen])
70
+
71
+ // Initialize preview runtime - fetch config and send to iframe (dev mode only)
72
+ useEffect(() => {
73
+ if (isStaticBuild) return
74
+
75
+ const iframe = isFullscreen ? fullscreenIframeRef.current : iframeRef.current
76
+ if (!iframe) return
77
+
78
+ let configSent = false
79
+
80
+ const handleMessage = (event: MessageEvent) => {
81
+ const msg = event.data as PreviewMessage
82
+
83
+ if (msg.type === 'ready' && !configSent) {
84
+ configSent = true
85
+ setBuildStatus('building')
86
+
87
+ fetch(`/_preview-config/screens/${unit.name}`)
88
+ .then(res => res.json())
89
+ .then((config: PreviewConfig) => {
90
+ iframe.contentWindow?.postMessage({ type: 'init', config } as PreviewMessage, '*')
91
+ })
92
+ .catch(err => {
93
+ setBuildStatus('error')
94
+ setBuildError(`Failed to load preview config: ${err.message}`)
95
+ })
96
+ }
97
+
98
+ if (msg.type === 'built') {
99
+ const result = msg.result as BuildResult
100
+ if (result.success) {
101
+ setBuildStatus('ready')
102
+ setBuildError(null)
103
+ } else {
104
+ setBuildStatus('error')
105
+ setBuildError(result.error || 'Build failed')
106
+ }
107
+ }
108
+
109
+ if (msg.type === 'error') {
110
+ setBuildStatus('error')
111
+ setBuildError(msg.error)
112
+ }
113
+ }
114
+
115
+ window.addEventListener('message', handleMessage)
116
+ return () => window.removeEventListener('message', handleMessage)
117
+ }, [unit.name, activeState, isFullscreen])
22
118
 
23
- const iframeUrl = `/_preview-runtime?preview=screens/${unit.name}&state=${activeState}`
119
+ // Send token overrides to iframe
120
+ useEffect(() => {
121
+ const iframe = isFullscreen ? fullscreenIframeRef.current : iframeRef.current
122
+ if (!iframe?.contentWindow) return
123
+ const css = toCssOverrides()
124
+ iframe.contentWindow.postMessage({ type: 'token-overrides', css }, '*')
125
+ }, [tokenOverrides, isFullscreen, buildStatus])
24
126
 
25
127
  // Fullscreen mode
26
128
  if (isFullscreen) {
@@ -29,48 +131,68 @@ export function ScreenPreview({ unit, initialState }: ScreenPreviewProps) {
29
131
  position: 'fixed',
30
132
  inset: 0,
31
133
  zIndex: 50,
32
- backgroundColor: 'var(--fd-background)',
134
+ backgroundColor: 'oklch(0.12 0.01 260)',
33
135
  display: 'flex',
34
136
  flexDirection: 'column',
35
137
  }}>
138
+ {/* Fullscreen header */}
36
139
  <div style={{
37
140
  display: 'flex',
38
141
  alignItems: 'center',
39
142
  justifyContent: 'space-between',
40
- padding: '8px 16px',
41
- borderBottom: '1px solid var(--fd-border)',
42
- backgroundColor: 'var(--fd-muted)',
143
+ padding: '12px 20px',
144
+ backgroundColor: 'oklch(0.18 0.01 260)',
145
+ borderBottom: '1px solid oklch(0.25 0.01 260)',
43
146
  }}>
44
- <span style={{
45
- fontWeight: 500,
46
- fontSize: '14px',
47
- color: 'var(--fd-foreground)',
48
- }}>
49
- {unit.name} / {activeState === 'index' ? 'default' : activeState}
50
- </span>
147
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
148
+ <span style={{
149
+ fontSize: '14px',
150
+ fontWeight: 600,
151
+ color: 'oklch(0.95 0 0)',
152
+ }}>
153
+ {unit.config?.title || unit.name}
154
+ </span>
155
+ <span style={{
156
+ padding: '2px 8px',
157
+ fontSize: '11px',
158
+ backgroundColor: 'oklch(0.25 0.01 260)',
159
+ color: 'oklch(0.7 0 0)',
160
+ borderRadius: '4px',
161
+ }}>
162
+ {activeState === 'index' ? 'default' : activeState}
163
+ </span>
164
+ </div>
51
165
  <button
52
166
  onClick={() => setIsFullscreen(false)}
53
167
  style={{
54
- padding: '8px 12px',
55
- backgroundColor: 'transparent',
168
+ padding: '8px 16px',
169
+ backgroundColor: 'oklch(0.25 0.01 260)',
56
170
  border: 'none',
57
- borderRadius: '4px',
171
+ borderRadius: '8px',
58
172
  cursor: 'pointer',
59
- fontSize: '14px',
60
- color: 'var(--fd-foreground)',
173
+ fontSize: '13px',
174
+ fontWeight: 500,
175
+ color: 'oklch(0.9 0 0)',
176
+ transition: 'all 0.15s ease',
177
+ }}
178
+ onMouseEnter={(e) => {
179
+ e.currentTarget.style.backgroundColor = 'oklch(0.35 0.01 260)'
180
+ }}
181
+ onMouseLeave={(e) => {
182
+ e.currentTarget.style.backgroundColor = 'oklch(0.25 0.01 260)'
61
183
  }}
62
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'}
63
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
64
184
  >
65
- Close
185
+ Exit Fullscreen
66
186
  </button>
67
187
  </div>
68
188
  <iframe
189
+ ref={fullscreenIframeRef}
69
190
  src={iframeUrl}
70
191
  style={{
71
192
  width: '100%',
72
193
  flex: 1,
73
194
  border: 'none',
195
+ backgroundColor: 'white',
74
196
  }}
75
197
  title={`Screen: ${unit.name}`}
76
198
  />
@@ -82,209 +204,400 @@ export function ScreenPreview({ unit, initialState }: ScreenPreviewProps) {
82
204
  <div style={{
83
205
  display: 'flex',
84
206
  flexDirection: 'column',
85
- border: '1px solid var(--fd-border)',
86
- borderRadius: '8px',
207
+ borderRadius: '16px',
87
208
  overflow: 'hidden',
88
- backgroundColor: 'var(--fd-background)',
209
+ backgroundColor: 'var(--fd-card)',
210
+ boxShadow: '0 4px 24px -4px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.04)',
89
211
  }}>
90
212
  {/* Header with state tabs */}
91
213
  <div style={{
92
- display: 'flex',
93
- alignItems: 'center',
94
- justifyContent: 'space-between',
95
- padding: '12px 16px',
96
- backgroundColor: 'var(--fd-muted)',
214
+ padding: '16px 24px',
215
+ background: 'linear-gradient(to bottom, var(--fd-card), var(--fd-muted))',
97
216
  borderBottom: '1px solid var(--fd-border)',
98
217
  }}>
99
218
  <div style={{
100
219
  display: 'flex',
101
220
  alignItems: 'center',
221
+ justifyContent: 'space-between',
102
222
  gap: '16px',
103
223
  }}>
104
- <h2 style={{
105
- margin: 0,
106
- fontSize: '18px',
107
- fontWeight: 600,
108
- color: 'var(--fd-foreground)',
109
- }}>
110
- {unit.config?.title || unit.name}
111
- </h2>
224
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
225
+ {/* Screen icon */}
226
+ <div style={{
227
+ width: '36px',
228
+ height: '36px',
229
+ borderRadius: '10px',
230
+ background: 'linear-gradient(135deg, oklch(0.55 0.15 250) 0%, oklch(0.45 0.18 280) 100%)',
231
+ display: 'flex',
232
+ alignItems: 'center',
233
+ justifyContent: 'center',
234
+ color: 'white',
235
+ fontSize: '16px',
236
+ boxShadow: '0 2px 8px -2px rgba(0, 0, 0, 0.25)',
237
+ }}>
238
+
239
+ </div>
112
240
 
113
- {/* State tabs */}
114
- <div style={{
115
- display: 'flex',
116
- gap: '4px',
117
- }}>
118
- {states.map(state => (
119
- <button
120
- key={state}
121
- onClick={() => setActiveState(state)}
122
- style={{
123
- padding: '4px 12px',
241
+ <div>
242
+ <h2 style={{
243
+ margin: 0,
244
+ fontSize: '18px',
245
+ fontWeight: 600,
246
+ color: 'var(--fd-foreground)',
247
+ letterSpacing: '-0.02em',
248
+ display: 'flex',
249
+ alignItems: 'center',
250
+ gap: '10px',
251
+ }}>
252
+ {unit.config?.title || unit.name}
253
+ {buildStatus === 'building' && (
254
+ <span style={{
255
+ width: '8px',
256
+ height: '8px',
257
+ borderRadius: '50%',
258
+ backgroundColor: 'oklch(0.75 0.15 85)',
259
+ animation: 'pulse 1.5s ease-in-out infinite',
260
+ }} />
261
+ )}
262
+ </h2>
263
+ {unit.config?.description && (
264
+ <p style={{
265
+ margin: '4px 0 0 0',
124
266
  fontSize: '13px',
125
- border: 'none',
126
- borderRadius: '4px',
127
- cursor: 'pointer',
128
- backgroundColor: activeState === state ? 'var(--fd-primary)' : 'transparent',
129
- color: activeState === state ? 'var(--fd-primary-foreground)' : 'var(--fd-muted-foreground)',
130
- fontWeight: activeState === state ? 500 : 400,
131
- transition: 'background-color 0.15s, color 0.15s',
132
- }}
133
- onMouseEnter={(e) => {
134
- if (activeState !== state) {
135
- e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
136
- e.currentTarget.style.color = 'var(--fd-foreground)'
137
- }
138
- }}
139
- onMouseLeave={(e) => {
140
- if (activeState !== state) {
141
- e.currentTarget.style.backgroundColor = 'transparent'
142
- e.currentTarget.style.color = 'var(--fd-muted-foreground)'
143
- }
144
- }}
145
- >
146
- {state === 'index' ? 'default' : state}
147
- </button>
148
- ))}
267
+ color: 'var(--fd-muted-foreground)',
268
+ }}>
269
+ {unit.config.description}
270
+ </p>
271
+ )}
272
+ </div>
149
273
  </div>
150
- </div>
151
274
 
152
- <button
153
- onClick={() => setIsFullscreen(true)}
154
- style={{
155
- padding: '6px 12px',
156
- backgroundColor: 'transparent',
157
- border: '1px solid var(--fd-border)',
158
- borderRadius: '4px',
159
- cursor: 'pointer',
160
- fontSize: '13px',
161
- color: 'var(--fd-muted-foreground)',
162
- }}
163
- onMouseEnter={(e) => {
164
- e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
165
- e.currentTarget.style.color = 'var(--fd-foreground)'
166
- }}
167
- onMouseLeave={(e) => {
168
- e.currentTarget.style.backgroundColor = 'transparent'
169
- e.currentTarget.style.color = 'var(--fd-muted-foreground)'
170
- }}
171
- title="Fullscreen"
172
- >
173
- Fullscreen
174
- </button>
275
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
276
+ <button
277
+ onClick={() => setAnnotationsEnabled(prev => !prev)}
278
+ style={{
279
+ display: 'flex',
280
+ alignItems: 'center',
281
+ justifyContent: 'center',
282
+ padding: '6px',
283
+ border: 'none',
284
+ borderRadius: '8px',
285
+ cursor: 'pointer',
286
+ backgroundColor: annotationsEnabled ? 'oklch(0.92 0.08 250)' : 'var(--fd-muted)',
287
+ color: annotationsEnabled ? 'oklch(0.45 0.18 250)' : 'var(--fd-muted-foreground)',
288
+ transition: 'all 0.15s ease',
289
+ }}
290
+ title={annotationsEnabled ? 'Disable annotations' : 'Enable annotations'}
291
+ >
292
+ <Icon name="pin" size={14} />
293
+ </button>
294
+ <SnapshotButton onCapture={() => captureSnapshot(iframeRef, {
295
+ previewName: `screens/${unit.name}`,
296
+ stateOrStep: activeState,
297
+ viewport,
298
+ })} />
299
+ <button
300
+ onClick={() => setShowSnapshots(prev => !prev)}
301
+ style={{
302
+ display: 'flex',
303
+ alignItems: 'center',
304
+ justifyContent: 'center',
305
+ padding: '6px',
306
+ border: 'none',
307
+ borderRadius: '8px',
308
+ cursor: 'pointer',
309
+ backgroundColor: 'var(--fd-muted)',
310
+ color: 'var(--fd-muted-foreground)',
311
+ transition: 'all 0.15s ease',
312
+ position: 'relative',
313
+ }}
314
+ title="View snapshots"
315
+ >
316
+ <Icon name="camera" size={14} />
317
+ {snapshots.length > 0 && (
318
+ <span style={{
319
+ position: 'absolute',
320
+ top: '-4px',
321
+ right: '-4px',
322
+ width: '16px',
323
+ height: '16px',
324
+ borderRadius: '50%',
325
+ backgroundColor: 'var(--fd-primary)',
326
+ color: 'var(--fd-primary-foreground)',
327
+ fontSize: '9px',
328
+ fontWeight: 700,
329
+ display: 'flex',
330
+ alignItems: 'center',
331
+ justifyContent: 'center',
332
+ }}>{snapshots.length}</span>
333
+ )}
334
+ </button>
335
+ <button
336
+ onClick={() => setShowTokens(prev => !prev)}
337
+ style={{
338
+ display: 'flex',
339
+ alignItems: 'center',
340
+ justifyContent: 'center',
341
+ padding: '6px',
342
+ border: 'none',
343
+ borderRadius: '8px',
344
+ cursor: 'pointer',
345
+ backgroundColor: showTokens ? 'oklch(0.92 0.08 310)' : 'var(--fd-muted)',
346
+ color: showTokens ? 'oklch(0.45 0.18 310)' : 'var(--fd-muted-foreground)',
347
+ transition: 'all 0.15s ease',
348
+ }}
349
+ title="Token playground"
350
+ >
351
+ <Icon name="palette" size={14} />
352
+ </button>
353
+ <StatusDropdown
354
+ previewName={`screens/${unit.name}`}
355
+ status={approvalStatus}
356
+ onStatusChange={changeStatus}
357
+ getAuditLog={getAuditLog}
358
+ />
359
+ </div>
360
+
361
+ {/* State tabs */}
362
+ {states.length > 1 && (
363
+ <div style={{
364
+ display: 'flex',
365
+ gap: '2px',
366
+ backgroundColor: 'var(--fd-muted)',
367
+ padding: '3px',
368
+ borderRadius: '10px',
369
+ }}>
370
+ {states.map(state => (
371
+ <button
372
+ key={state}
373
+ onClick={() => setActiveState(state)}
374
+ style={{
375
+ padding: '6px 14px',
376
+ fontSize: '12px',
377
+ fontWeight: 500,
378
+ border: 'none',
379
+ borderRadius: '7px',
380
+ cursor: 'pointer',
381
+ backgroundColor: activeState === state ? 'var(--fd-background)' : 'transparent',
382
+ color: activeState === state ? 'var(--fd-foreground)' : 'var(--fd-muted-foreground)',
383
+ boxShadow: activeState === state ? '0 1px 3px rgba(0, 0, 0, 0.1)' : 'none',
384
+ transition: 'all 0.15s ease',
385
+ }}
386
+ >
387
+ {state === 'index' ? 'Default' : state}
388
+ </button>
389
+ ))}
390
+ </div>
391
+ )}
392
+ </div>
175
393
  </div>
176
394
 
177
- {/* Description if available */}
178
- {unit.config?.description && (
395
+ {/* Build error display */}
396
+ {buildError && (
179
397
  <div style={{
180
- padding: '12px 16px',
181
- borderBottom: '1px solid var(--fd-border)',
182
- backgroundColor: 'var(--fd-background)',
398
+ padding: '16px 24px',
399
+ backgroundColor: 'oklch(0.97 0.02 25)',
400
+ borderBottom: '1px solid oklch(0.90 0.05 25)',
401
+ display: 'flex',
402
+ alignItems: 'flex-start',
403
+ gap: '12px',
183
404
  }}>
184
- <p style={{
405
+ <div style={{
406
+ width: '20px',
407
+ height: '20px',
408
+ borderRadius: '50%',
409
+ backgroundColor: 'oklch(0.65 0.20 25)',
410
+ color: 'white',
411
+ display: 'flex',
412
+ alignItems: 'center',
413
+ justifyContent: 'center',
414
+ fontSize: '12px',
415
+ flexShrink: 0,
416
+ }}>!</div>
417
+ <pre style={{
185
418
  margin: 0,
186
- fontSize: '14px',
187
- color: 'var(--fd-muted-foreground)',
419
+ fontSize: '12px',
420
+ color: 'oklch(0.45 0.15 25)',
421
+ whiteSpace: 'pre-wrap',
422
+ fontFamily: 'var(--fd-font-mono)',
423
+ overflow: 'auto',
424
+ maxHeight: '100px',
425
+ flex: 1,
188
426
  }}>
189
- {unit.config.description}
190
- </p>
427
+ {buildError}
428
+ </pre>
191
429
  </div>
192
430
  )}
193
431
 
194
- {/* Preview with viewport */}
432
+ {/* Preview canvas with browser frame */}
195
433
  <div style={{
196
- padding: '24px',
434
+ padding: '32px',
197
435
  backgroundColor: 'var(--fd-muted)',
436
+ // Subtle cross-hatch pattern
437
+ backgroundImage: `
438
+ linear-gradient(45deg, transparent 48%, var(--fd-border) 49%, var(--fd-border) 51%, transparent 52%),
439
+ linear-gradient(-45deg, transparent 48%, var(--fd-border) 49%, var(--fd-border) 51%, transparent 52%)
440
+ `,
441
+ backgroundSize: '12px 12px',
198
442
  display: 'flex',
199
443
  justifyContent: 'center',
200
- overflow: 'auto',
201
- minHeight: '400px',
444
+ minHeight: '500px',
202
445
  }}>
446
+ {/* Browser frame */}
203
447
  <div
204
448
  style={{
205
- width: viewports[viewport].width,
449
+ width: viewport === 'desktop' ? '100%' : VIEWPORT_WIDTHS[viewport],
206
450
  maxWidth: '100%',
207
- backgroundColor: 'var(--fd-background)',
208
- boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
209
- borderRadius: '4px',
451
+ backgroundColor: 'var(--fd-card)',
452
+ borderRadius: '12px',
453
+ boxShadow: `
454
+ 0 0 0 1px var(--fd-border),
455
+ 0 8px 32px -8px rgba(0, 0, 0, 0.15),
456
+ 0 20px 60px -15px rgba(0, 0, 0, 0.1)
457
+ `,
210
458
  overflow: 'hidden',
211
- transition: 'width 0.3s ease',
459
+ transition: 'width 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
212
460
  }}
213
461
  >
214
- <iframe
215
- src={iframeUrl}
216
- style={{
217
- width: '100%',
218
- height: '600px',
219
- border: 'none',
220
- display: 'block',
221
- }}
222
- title={`Screen: ${unit.name} - ${activeState}`}
223
- />
462
+ {/* Browser chrome */}
463
+ <div style={{
464
+ display: 'flex',
465
+ alignItems: 'center',
466
+ gap: '8px',
467
+ padding: '10px 14px',
468
+ backgroundColor: 'var(--fd-muted)',
469
+ borderBottom: '1px solid var(--fd-border)',
470
+ }}>
471
+ {/* Traffic lights */}
472
+ <div style={{ display: 'flex', gap: '6px' }}>
473
+ <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: 'oklch(0.70 0.18 25)' }} />
474
+ <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: 'oklch(0.80 0.15 85)' }} />
475
+ <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: 'oklch(0.70 0.18 145)' }} />
476
+ </div>
477
+
478
+ {/* URL bar */}
479
+ <div style={{
480
+ flex: 1,
481
+ marginLeft: '8px',
482
+ padding: '6px 12px',
483
+ backgroundColor: 'var(--fd-background)',
484
+ borderRadius: '6px',
485
+ fontSize: '11px',
486
+ fontFamily: 'var(--fd-font-mono)',
487
+ color: 'var(--fd-muted-foreground)',
488
+ overflow: 'hidden',
489
+ textOverflow: 'ellipsis',
490
+ whiteSpace: 'nowrap',
491
+ }}>
492
+ screens/{unit.name}/{activeState === 'index' ? '' : activeState}
493
+ </div>
494
+
495
+ {/* Fullscreen button */}
496
+ <button
497
+ onClick={() => setIsFullscreen(true)}
498
+ style={{
499
+ padding: '6px 10px',
500
+ backgroundColor: 'transparent',
501
+ border: 'none',
502
+ borderRadius: '6px',
503
+ cursor: 'pointer',
504
+ fontSize: '12px',
505
+ color: 'var(--fd-muted-foreground)',
506
+ transition: 'all 0.15s ease',
507
+ }}
508
+ onMouseEnter={(e) => {
509
+ e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
510
+ e.currentTarget.style.color = 'var(--fd-foreground)'
511
+ }}
512
+ onMouseLeave={(e) => {
513
+ e.currentTarget.style.backgroundColor = 'transparent'
514
+ e.currentTarget.style.color = 'var(--fd-muted-foreground)'
515
+ }}
516
+ title="Fullscreen"
517
+ >
518
+
519
+ </button>
520
+ </div>
521
+
522
+ {/* Screen content */}
523
+ <AnnotationLayer
524
+ previewName={`screens/${unit.name}`}
525
+ stateOrStep={activeState}
526
+ enabled={annotationsEnabled}
527
+ >
528
+ <div style={{ position: 'relative' }}>
529
+ {buildStatus === 'loading' && (
530
+ <div style={{
531
+ position: 'absolute',
532
+ inset: 0,
533
+ backgroundColor: 'var(--fd-background)',
534
+ display: 'flex',
535
+ alignItems: 'center',
536
+ justifyContent: 'center',
537
+ zIndex: 5,
538
+ }}>
539
+ <div style={{
540
+ width: '32px',
541
+ height: '32px',
542
+ border: '2px solid var(--fd-border)',
543
+ borderTopColor: 'var(--fd-primary)',
544
+ borderRadius: '50%',
545
+ animation: 'spin 0.8s linear infinite',
546
+ }} />
547
+ </div>
548
+ )}
549
+ <iframe
550
+ ref={iframeRef}
551
+ src={iframeUrl}
552
+ style={{
553
+ width: '100%',
554
+ height: '500px',
555
+ border: 'none',
556
+ display: 'block',
557
+ backgroundColor: 'white',
558
+ opacity: (isStaticBuild ? iframeLoaded : buildStatus === 'ready') ? 1 : 0.5,
559
+ transition: 'opacity 0.3s ease',
560
+ }}
561
+ title={`Screen: ${unit.name} - ${activeState}`}
562
+ />
563
+ </div>
564
+ </AnnotationLayer>
224
565
  </div>
225
566
  </div>
226
567
 
227
- {/* Viewport toggle */}
568
+ {/* Viewport controls */}
228
569
  <div style={{
229
570
  display: 'flex',
571
+ alignItems: 'center',
230
572
  justifyContent: 'center',
231
- gap: '8px',
232
- padding: '12px',
573
+ padding: '16px',
233
574
  borderTop: '1px solid var(--fd-border)',
234
- backgroundColor: 'var(--fd-muted)',
575
+ backgroundColor: 'var(--fd-card)',
235
576
  }}>
236
- {(Object.entries(viewports) as [Viewport, typeof viewports[Viewport]][]).map(([key, { width, label }]) => (
237
- <button
238
- key={key}
239
- onClick={() => setViewport(key)}
240
- style={{
241
- padding: '6px 12px',
242
- fontSize: '13px',
243
- border: 'none',
244
- borderRadius: '4px',
245
- cursor: 'pointer',
246
- backgroundColor: viewport === key ? 'var(--fd-primary)' : 'transparent',
247
- color: viewport === key ? 'var(--fd-primary-foreground)' : 'var(--fd-muted-foreground)',
248
- fontWeight: viewport === key ? 500 : 400,
249
- transition: 'background-color 0.15s, color 0.15s',
250
- }}
251
- onMouseEnter={(e) => {
252
- if (viewport !== key) {
253
- e.currentTarget.style.backgroundColor = 'var(--fd-secondary)'
254
- e.currentTarget.style.color = 'var(--fd-foreground)'
255
- }
256
- }}
257
- onMouseLeave={(e) => {
258
- if (viewport !== key) {
259
- e.currentTarget.style.backgroundColor = 'transparent'
260
- e.currentTarget.style.color = 'var(--fd-muted-foreground)'
261
- }
262
- }}
263
- title={`${label} (${width}px)`}
264
- >
265
- {label}
266
- </button>
267
- ))}
577
+ <ViewportControls viewport={viewport} onViewportChange={setViewport} />
268
578
  </div>
269
579
 
270
580
  {/* Tags */}
271
581
  {unit.config?.tags && unit.config.tags.length > 0 && (
272
582
  <div style={{
273
- padding: '12px 16px',
583
+ padding: '16px 24px',
274
584
  borderTop: '1px solid var(--fd-border)',
275
585
  display: 'flex',
276
586
  gap: '8px',
277
587
  flexWrap: 'wrap',
588
+ backgroundColor: 'var(--fd-card)',
278
589
  }}>
279
590
  {unit.config.tags.map(tag => (
280
591
  <span
281
592
  key={tag}
282
593
  style={{
283
- padding: '2px 8px',
594
+ padding: '4px 12px',
284
595
  fontSize: '12px',
285
- backgroundColor: 'var(--fd-secondary)',
286
- color: 'var(--fd-secondary-foreground)',
287
- borderRadius: '4px',
596
+ fontWeight: 500,
597
+ backgroundColor: 'var(--fd-muted)',
598
+ color: 'var(--fd-muted-foreground)',
599
+ borderRadius: '100px',
600
+ border: '1px solid var(--fd-border)',
288
601
  }}
289
602
  >
290
603
  {tag}
@@ -292,6 +605,33 @@ export function ScreenPreview({ unit, initialState }: ScreenPreviewProps) {
292
605
  ))}
293
606
  </div>
294
607
  )}
608
+
609
+ {/* Panels */}
610
+ {showSnapshots && (
611
+ <SnapshotPanel
612
+ snapshots={snapshots}
613
+ onDelete={deleteSnapshot}
614
+ onClose={() => setShowSnapshots(false)}
615
+ />
616
+ )}
617
+ {showTokens && (
618
+ <TokenPlayground
619
+ tokens={designTokens}
620
+ overrides={tokenOverrides}
621
+ onSetOverride={setOverride}
622
+ onRemoveOverride={removeOverride}
623
+ onResetAll={resetAll}
624
+ onClose={() => setShowTokens(false)}
625
+ />
626
+ )}
627
+
628
+ {/* Inline styles for animations */}
629
+ <style>{`
630
+ @keyframes pulse {
631
+ 0%, 100% { opacity: 1; }
632
+ 50% { opacity: 0.4; }
633
+ }
634
+ `}</style>
295
635
  </div>
296
636
  )
297
637
  }