@antigenic-oss/paint 0.1.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 (158) hide show
  1. package/LICENSE +178 -0
  2. package/NOTICE +4 -0
  3. package/README.md +180 -0
  4. package/bin/paint.js +266 -0
  5. package/next-env.d.ts +6 -0
  6. package/next.config.ts +19 -0
  7. package/package.json +81 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/dev-editor-inspector.js +1872 -0
  10. package/src/app/api/claude/analyze/route.ts +319 -0
  11. package/src/app/api/claude/apply/route.ts +185 -0
  12. package/src/app/api/claude/pick-folder/route.ts +64 -0
  13. package/src/app/api/claude/scan/route.ts +221 -0
  14. package/src/app/api/claude/status/route.ts +55 -0
  15. package/src/app/api/project/scan/route.ts +634 -0
  16. package/src/app/api/project-scan/css-variables/route.ts +238 -0
  17. package/src/app/api/project-scan/route.ts +40 -0
  18. package/src/app/api/project-scan/tailwind-config/route.ts +172 -0
  19. package/src/app/api/proxy/[[...path]]/route.ts +2400 -0
  20. package/src/app/docs/DocsClient.tsx +322 -0
  21. package/src/app/docs/layout.tsx +7 -0
  22. package/src/app/docs/page.tsx +855 -0
  23. package/src/app/globals.css +176 -0
  24. package/src/app/layout.tsx +19 -0
  25. package/src/app/page.tsx +46 -0
  26. package/src/bridge/api-handlers.ts +885 -0
  27. package/src/bridge/proxy-handler.ts +329 -0
  28. package/src/bridge/server.ts +113 -0
  29. package/src/components/BreakpointTabs.tsx +72 -0
  30. package/src/components/ChangeSummaryModal.tsx +267 -0
  31. package/src/components/ConnectModal.tsx +994 -0
  32. package/src/components/Editor.tsx +90 -0
  33. package/src/components/PageSelector.tsx +208 -0
  34. package/src/components/PreviewFrame.tsx +299 -0
  35. package/src/components/ProjectFolderBanner.tsx +91 -0
  36. package/src/components/ResponsiveToolbar.tsx +222 -0
  37. package/src/components/TargetSelector.tsx +243 -0
  38. package/src/components/TopBar.tsx +315 -0
  39. package/src/components/common/CollapsibleSection.tsx +36 -0
  40. package/src/components/common/ColorPicker.tsx +920 -0
  41. package/src/components/common/EditablePre.tsx +136 -0
  42. package/src/components/common/ErrorBoundary.tsx +65 -0
  43. package/src/components/common/ResizablePanel.tsx +83 -0
  44. package/src/components/common/ScanAnimation.tsx +76 -0
  45. package/src/components/common/ToastContainer.tsx +97 -0
  46. package/src/components/common/UnitInput.tsx +77 -0
  47. package/src/components/common/VariableColorPicker.tsx +622 -0
  48. package/src/components/left-panel/AddElementPanel.tsx +237 -0
  49. package/src/components/left-panel/ComponentsPanel.tsx +609 -0
  50. package/src/components/left-panel/IconSidebar.tsx +99 -0
  51. package/src/components/left-panel/LayerNode.tsx +874 -0
  52. package/src/components/left-panel/LayerSearch.tsx +23 -0
  53. package/src/components/left-panel/LayersPanel.tsx +52 -0
  54. package/src/components/left-panel/LeftPanel.tsx +122 -0
  55. package/src/components/left-panel/PagesPanel.tsx +114 -0
  56. package/src/components/left-panel/icons.tsx +162 -0
  57. package/src/components/left-panel/terminal/ScanOverlay.tsx +66 -0
  58. package/src/components/left-panel/terminal/TerminalPanel.tsx +217 -0
  59. package/src/components/right-panel/ElementLogBox.tsx +248 -0
  60. package/src/components/right-panel/PanelTabs.tsx +83 -0
  61. package/src/components/right-panel/RightPanel.tsx +41 -0
  62. package/src/components/right-panel/changes/AiScanResultPanel.tsx +285 -0
  63. package/src/components/right-panel/changes/ChangeEntry.tsx +59 -0
  64. package/src/components/right-panel/changes/ChangelogActions.tsx +105 -0
  65. package/src/components/right-panel/changes/ChangesPanel.tsx +1474 -0
  66. package/src/components/right-panel/claude/ApplyConfirmModal.tsx +376 -0
  67. package/src/components/right-panel/claude/ClaudeErrorState.tsx +125 -0
  68. package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +482 -0
  69. package/src/components/right-panel/claude/ClaudeProgressIndicator.tsx +76 -0
  70. package/src/components/right-panel/claude/DiffCard.tsx +130 -0
  71. package/src/components/right-panel/claude/DiffViewer.tsx +54 -0
  72. package/src/components/right-panel/claude/ProjectRootSelector.tsx +275 -0
  73. package/src/components/right-panel/claude/ResultsSummary.tsx +119 -0
  74. package/src/components/right-panel/claude/SetupFlow.tsx +315 -0
  75. package/src/components/right-panel/console/ConsolePanel.tsx +209 -0
  76. package/src/components/right-panel/design/AppearanceSection.tsx +703 -0
  77. package/src/components/right-panel/design/BackgroundSection.tsx +516 -0
  78. package/src/components/right-panel/design/BorderSection.tsx +161 -0
  79. package/src/components/right-panel/design/CSSRawView.tsx +412 -0
  80. package/src/components/right-panel/design/DesignCSSTabToggle.tsx +51 -0
  81. package/src/components/right-panel/design/DesignPanel.tsx +275 -0
  82. package/src/components/right-panel/design/ElementBreadcrumb.tsx +51 -0
  83. package/src/components/right-panel/design/GradientEditor.tsx +726 -0
  84. package/src/components/right-panel/design/LayoutSection.tsx +1948 -0
  85. package/src/components/right-panel/design/PositionSection.tsx +865 -0
  86. package/src/components/right-panel/design/PropertiesSection.tsx +86 -0
  87. package/src/components/right-panel/design/SVGSection.tsx +361 -0
  88. package/src/components/right-panel/design/ShadowBlurSection.tsx +227 -0
  89. package/src/components/right-panel/design/SizeSection.tsx +183 -0
  90. package/src/components/right-panel/design/TextSection.tsx +719 -0
  91. package/src/components/right-panel/design/icons.tsx +948 -0
  92. package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +467 -0
  93. package/src/components/right-panel/design/inputs/ColorInput.tsx +43 -0
  94. package/src/components/right-panel/design/inputs/CompactInput.tsx +333 -0
  95. package/src/components/right-panel/design/inputs/DraggableLabel.tsx +118 -0
  96. package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +54 -0
  97. package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +174 -0
  98. package/src/components/right-panel/design/inputs/SectionHeader.tsx +79 -0
  99. package/src/components/right-panel/variables/VariablesPanel.tsx +388 -0
  100. package/src/hooks/useBridge.ts +95 -0
  101. package/src/hooks/useChangeTracker.ts +563 -0
  102. package/src/hooks/useClaudeAPI.ts +118 -0
  103. package/src/hooks/useDOMTree.ts +25 -0
  104. package/src/hooks/useKeyboardShortcuts.ts +76 -0
  105. package/src/hooks/usePostMessage.ts +589 -0
  106. package/src/hooks/useProjectScan.ts +204 -0
  107. package/src/hooks/useResizable.ts +20 -0
  108. package/src/hooks/useSelectedElement.ts +51 -0
  109. package/src/hooks/useTargetUrl.ts +81 -0
  110. package/src/inspector/DOMTraverser.ts +71 -0
  111. package/src/inspector/ElementSelector.ts +23 -0
  112. package/src/inspector/HoverHighlighter.ts +54 -0
  113. package/src/inspector/SelectionHighlighter.ts +27 -0
  114. package/src/inspector/StyleExtractor.ts +19 -0
  115. package/src/inspector/inspector.ts +17 -0
  116. package/src/inspector/messaging.ts +30 -0
  117. package/src/lib/apiBase.ts +15 -0
  118. package/src/lib/classifyElement.ts +430 -0
  119. package/src/lib/claude-bin.ts +197 -0
  120. package/src/lib/claude-stream.ts +158 -0
  121. package/src/lib/clientProjectScanner.ts +344 -0
  122. package/src/lib/componentMatcher.ts +156 -0
  123. package/src/lib/constants.ts +573 -0
  124. package/src/lib/cssVariableUtils.ts +409 -0
  125. package/src/lib/diffParser.ts +206 -0
  126. package/src/lib/folderPicker.ts +84 -0
  127. package/src/lib/gradientParser.ts +160 -0
  128. package/src/lib/projectScanner.ts +355 -0
  129. package/src/lib/promptBuilder.ts +402 -0
  130. package/src/lib/shadowParser.ts +124 -0
  131. package/src/lib/tailwindClassParser.ts +248 -0
  132. package/src/lib/textShadowUtils.ts +106 -0
  133. package/src/lib/utils.ts +299 -0
  134. package/src/lib/validatePath.ts +40 -0
  135. package/src/proxy.ts +92 -0
  136. package/src/server/terminal-server.ts +104 -0
  137. package/src/store/changeSlice.ts +288 -0
  138. package/src/store/claudeSlice.ts +222 -0
  139. package/src/store/componentSlice.ts +90 -0
  140. package/src/store/consoleSlice.ts +51 -0
  141. package/src/store/cssVariableSlice.ts +94 -0
  142. package/src/store/elementSlice.ts +78 -0
  143. package/src/store/index.ts +35 -0
  144. package/src/store/terminalSlice.ts +30 -0
  145. package/src/store/treeSlice.ts +69 -0
  146. package/src/store/uiSlice.ts +327 -0
  147. package/src/types/changelog.ts +49 -0
  148. package/src/types/claude.ts +131 -0
  149. package/src/types/component.ts +49 -0
  150. package/src/types/cssVariables.ts +18 -0
  151. package/src/types/element.ts +21 -0
  152. package/src/types/file-system-access.d.ts +27 -0
  153. package/src/types/gradient.ts +12 -0
  154. package/src/types/messages.ts +392 -0
  155. package/src/types/shadow.ts +8 -0
  156. package/src/types/tree.ts +9 -0
  157. package/tsconfig.json +42 -0
  158. package/tsconfig.server.json +12 -0
@@ -0,0 +1,994 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useRef } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { normalizeTargetUrl } from '@/lib/utils'
6
+ import { BREAKPOINTS } from '@/lib/constants'
7
+ import { useProjectScan } from '@/hooks/useProjectScan'
8
+ import { pickFolder } from '@/lib/folderPicker'
9
+ import { isEditorOnLocalhost } from '@/hooks/usePostMessage'
10
+ import { ScanAnimation } from './common/ScanAnimation'
11
+ import type { Breakpoint } from '@/types/changelog'
12
+ import type { ScanResult } from '@/hooks/useProjectScan'
13
+
14
+ export function ConnectModal() {
15
+ const setTargetUrl = useEditorStore((s) => s.setTargetUrl)
16
+ const setConnectionStatus = useEditorStore((s) => s.setConnectionStatus)
17
+ const addRecentUrl = useEditorStore((s) => s.addRecentUrl)
18
+ const connectionStatus = useEditorStore((s) => s.connectionStatus)
19
+ const recentUrls = useEditorStore((s) => s.recentUrls)
20
+ const activeBreakpoint = useEditorStore((s) => s.activeBreakpoint)
21
+ const setActiveBreakpoint = useEditorStore((s) => s.setActiveBreakpoint)
22
+ const setPreviewWidth = useEditorStore((s) => s.setPreviewWidth)
23
+ const portRoots = useEditorStore((s) => s.portRoots)
24
+ const setProjectRoot = useEditorStore((s) => s.setProjectRoot)
25
+ const setPendingConnection = useEditorStore((s) => s.setPendingConnection)
26
+ const finalizeConnection = useEditorStore((s) => s.finalizeConnection)
27
+ const cancelPendingConnection = useEditorStore(
28
+ (s) => s.cancelPendingConnection,
29
+ )
30
+ const pendingTargetUrl = useEditorStore((s) => s.pendingTargetUrl)
31
+ const pendingFolderPath = useEditorStore((s) => s.pendingFolderPath)
32
+ const targetUrl = useEditorStore((s) => s.targetUrl)
33
+
34
+ const setDirectoryHandle = useEditorStore((s) => s.setDirectoryHandle)
35
+ const directoryHandle = useEditorStore((s) => s.directoryHandle)
36
+ const { triggerScan, triggerClientScan } = useProjectScan()
37
+ const [isLocal, setIsLocal] = useState(false)
38
+
39
+ useEffect(() => {
40
+ setIsLocal(isEditorOnLocalhost())
41
+ }, [])
42
+
43
+ const portOptions = Array.from({ length: 8 }, (_, i) => 3000 + i)
44
+ const [selectedPort, setSelectedPort] = useState(3000)
45
+ const [urlMode, setUrlMode] = useState(false)
46
+ const [customUrl, setCustomUrl] = useState('http://localhost:3000')
47
+ const [folderPath, setFolderPath] = useState('')
48
+ const [folderError, setFolderError] = useState<string | null>(null)
49
+ const [isBrowsing, setIsBrowsing] = useState(false)
50
+ const [error, setError] = useState<string | null>(null)
51
+ const [howToOpen, setHowToOpen] = useState(false)
52
+ const [showScriptFallback, setShowScriptFallback] = useState(false)
53
+ const [scriptCopied, setScriptCopied] = useState(false)
54
+ const [scanResult, setScanResult] = useState<ScanResult | null>(null)
55
+ const [scanDone, setScanDone] = useState(false)
56
+ const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
57
+ const autoAdvanceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
58
+
59
+ const isConnecting = connectionStatus === 'connecting'
60
+ const isConfirming = connectionStatus === 'confirming'
61
+ const isScanning = connectionStatus === 'scanning'
62
+
63
+ // Show script tag fallback after 5s of connecting (immediately when deployed)
64
+ useEffect(() => {
65
+ if (isConnecting && targetUrl) {
66
+ if (!isLocal) {
67
+ // On Vercel, show immediately — proxy won't inject the script
68
+ setShowScriptFallback(true)
69
+ } else {
70
+ fallbackTimerRef.current = setTimeout(() => {
71
+ setShowScriptFallback(true)
72
+ }, 5000)
73
+ }
74
+ } else {
75
+ setShowScriptFallback(false)
76
+ if (fallbackTimerRef.current) {
77
+ clearTimeout(fallbackTimerRef.current)
78
+ fallbackTimerRef.current = null
79
+ }
80
+ }
81
+ return () => {
82
+ if (fallbackTimerRef.current) {
83
+ clearTimeout(fallbackTimerRef.current)
84
+ fallbackTimerRef.current = null
85
+ }
86
+ }
87
+ }, [isConnecting, targetUrl, isLocal])
88
+
89
+ // Cleanup auto-advance timer
90
+ useEffect(() => {
91
+ return () => {
92
+ if (autoAdvanceRef.current) {
93
+ clearTimeout(autoAdvanceRef.current)
94
+ }
95
+ }
96
+ }, [])
97
+
98
+ // Cancel current connection and reset to editable state
99
+ const cancelConnection = () => {
100
+ if (isConnecting) {
101
+ setConnectionStatus('disconnected')
102
+ setTargetUrl(null)
103
+ }
104
+ if (isConfirming || isScanning) {
105
+ cancelPendingConnection()
106
+ }
107
+ setShowScriptFallback(false)
108
+ setScriptCopied(false)
109
+ setScanResult(null)
110
+ setScanDone(false)
111
+ if (fallbackTimerRef.current) {
112
+ clearTimeout(fallbackTimerRef.current)
113
+ fallbackTimerRef.current = null
114
+ }
115
+ if (autoAdvanceRef.current) {
116
+ clearTimeout(autoAdvanceRef.current)
117
+ autoAdvanceRef.current = null
118
+ }
119
+ }
120
+
121
+ const handleCopyScript = async () => {
122
+ const scriptTag = `<script src="${window.location.origin}/dev-editor-inspector.js"></script>`
123
+ try {
124
+ await navigator.clipboard.writeText(scriptTag)
125
+ setScriptCopied(true)
126
+ setTimeout(() => setScriptCopied(false), 2000)
127
+ } catch {
128
+ /* fallback: user can manually copy */
129
+ }
130
+ }
131
+
132
+ // Pre-fill folder path from portRoots when selected URL changes
133
+ const currentUrl = urlMode
134
+ ? customUrl.trim()
135
+ : `http://localhost:${selectedPort}`
136
+ useEffect(() => {
137
+ if (connectionStatus !== 'disconnected') return
138
+ const normalized = normalizeTargetUrl(currentUrl)
139
+ const saved = portRoots[normalized]
140
+ if (saved) {
141
+ setFolderPath(saved)
142
+ setFolderError(null)
143
+ }
144
+ }, [selectedPort, urlMode, currentUrl, portRoots, connectionStatus])
145
+
146
+ const handleBrowse = async () => {
147
+ setIsBrowsing(true)
148
+ setFolderError(null)
149
+ try {
150
+ const result = await pickFolder()
151
+ if (result.type === 'path') {
152
+ setFolderPath(result.path)
153
+ setDirectoryHandle(null)
154
+ } else if (result.type === 'handle') {
155
+ setFolderPath(result.name)
156
+ setDirectoryHandle(result.handle)
157
+ } else if (result.type === 'error') {
158
+ setFolderError(result.message)
159
+ }
160
+ // type === 'cancelled' — do nothing
161
+ } catch {
162
+ setFolderError('Failed to open folder picker')
163
+ } finally {
164
+ setIsBrowsing(false)
165
+ }
166
+ }
167
+
168
+ const handleConnect = () => {
169
+ setError(null)
170
+ setFolderError(null)
171
+ const raw = urlMode ? customUrl.trim() : `http://localhost:${selectedPort}`
172
+ if (urlMode && !raw) {
173
+ setError('Enter a URL')
174
+ return
175
+ }
176
+ const normalized = normalizeTargetUrl(raw)
177
+ const trimmedFolder = folderPath.trim()
178
+
179
+ if (trimmedFolder) {
180
+ // Save folder and go to confirmation step
181
+ // For client-side handles, store the folder name (not a server path)
182
+ if (isLocal || !directoryHandle) {
183
+ setProjectRoot(normalized, trimmedFolder)
184
+ }
185
+ setPendingConnection(normalized, trimmedFolder)
186
+ addRecentUrl(normalized)
187
+ } else {
188
+ // No folder — skip confirmation and scan, connect directly
189
+ setPendingConnection(normalized, '')
190
+ addRecentUrl(normalized)
191
+ }
192
+ }
193
+
194
+ const handleConfirm = async () => {
195
+ if (!pendingTargetUrl || !pendingFolderPath) return
196
+ setConnectionStatus('scanning')
197
+ setScanResult(null)
198
+ setScanDone(false)
199
+
200
+ // Use client-side scan when we have a directory handle (Vercel / FSAA mode)
201
+ const result = directoryHandle
202
+ ? await triggerClientScan(directoryHandle)
203
+ : await triggerScan(pendingFolderPath)
204
+ setScanResult(result)
205
+ setScanDone(true)
206
+
207
+ // Auto-advance to connecting after brief display
208
+ autoAdvanceRef.current = setTimeout(() => {
209
+ finalizeConnection()
210
+ }, 1200)
211
+ }
212
+
213
+ const handleContinueAnyway = () => {
214
+ if (autoAdvanceRef.current) {
215
+ clearTimeout(autoAdvanceRef.current)
216
+ }
217
+ finalizeConnection()
218
+ }
219
+
220
+ const handleBack = () => {
221
+ cancelPendingConnection()
222
+ setScanResult(null)
223
+ setScanDone(false)
224
+ }
225
+
226
+ const handleRecentClick = (url: string) => {
227
+ cancelConnection()
228
+ setError(null)
229
+ // Pre-fill the URL input so the user can review before clicking Connect
230
+ setUrlMode(true)
231
+ setCustomUrl(url)
232
+ }
233
+
234
+ const handleKeyDown = (e: React.KeyboardEvent) => {
235
+ if (e.key === 'Enter') {
236
+ handleConnect()
237
+ }
238
+ }
239
+
240
+ // Header subtitle changes per step
241
+ const headerSubtitle = isConfirming
242
+ ? 'Confirm connection details'
243
+ : isScanning
244
+ ? 'Scanning project folder'
245
+ : isConnecting
246
+ ? 'Connecting to your project'
247
+ : isLocal
248
+ ? 'Connect to your localhost project'
249
+ : 'Connect to your project'
250
+
251
+ return (
252
+ <div
253
+ className="fixed inset-0 z-[9999] flex items-center justify-center"
254
+ style={{ background: 'rgba(0, 0, 0, 0.6)' }}
255
+ >
256
+ <div
257
+ className="w-[520px] max-h-[85vh] flex flex-col rounded-lg shadow-2xl overflow-hidden"
258
+ style={{
259
+ background: 'var(--bg-primary)',
260
+ border: '1px solid var(--border)',
261
+ }}
262
+ >
263
+ {/* Header */}
264
+ <div
265
+ className="px-6 pt-6 pb-4 flex-shrink-0"
266
+ style={{ borderBottom: '1px solid var(--border)' }}
267
+ >
268
+ <div className="flex items-center gap-2.5 mb-1.5">
269
+ {/* Plug icon */}
270
+ <svg
271
+ width="18"
272
+ height="18"
273
+ viewBox="0 0 24 24"
274
+ fill="none"
275
+ stroke="var(--accent)"
276
+ strokeWidth="2"
277
+ strokeLinecap="round"
278
+ strokeLinejoin="round"
279
+ >
280
+ <path d="M12 22v-5" />
281
+ <path d="M9 8V2" />
282
+ <path d="M15 8V2" />
283
+ <path d="M18 8v5a6 6 0 0 1-6 6 6 6 0 0 1-6-6V8z" />
284
+ </svg>
285
+ <span
286
+ className="text-sm font-semibold"
287
+ style={{ color: 'var(--text-primary)' }}
288
+ >
289
+ pAInt
290
+ </span>
291
+ </div>
292
+ <p
293
+ className="text-xs ml-[30px]"
294
+ style={{ color: 'var(--text-secondary)' }}
295
+ >
296
+ {headerSubtitle}
297
+ </p>
298
+ </div>
299
+
300
+ {/* Body — scrollable */}
301
+ <div className="flex-1 overflow-y-auto px-6 py-5">
302
+ {/* ─── STEP: SETUP (disconnected) ─── */}
303
+ {connectionStatus === 'disconnected' && (
304
+ <>
305
+ {/* Connection controls */}
306
+ <div className="flex items-center gap-2">
307
+ {/* URL mode toggle */}
308
+ <button
309
+ onClick={() => {
310
+ setUrlMode(!urlMode)
311
+ setError(null)
312
+ }}
313
+ className="p-1.5 rounded transition-colors flex-shrink-0"
314
+ style={{
315
+ color: urlMode ? 'var(--accent)' : 'var(--text-muted)',
316
+ background: urlMode ? 'var(--accent-bg)' : 'transparent',
317
+ }}
318
+ title={
319
+ urlMode ? 'Switch to port selector' : 'Switch to URL input'
320
+ }
321
+ >
322
+ {urlMode ? (
323
+ <svg
324
+ width="14"
325
+ height="14"
326
+ viewBox="0 0 24 24"
327
+ fill="none"
328
+ stroke="currentColor"
329
+ strokeWidth="2"
330
+ strokeLinecap="round"
331
+ strokeLinejoin="round"
332
+ >
333
+ <polyline points="6 9 6 2 18 2 18 9" />
334
+ <path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
335
+ <rect x="6" y="14" width="12" height="8" />
336
+ </svg>
337
+ ) : (
338
+ <svg
339
+ width="14"
340
+ height="14"
341
+ viewBox="0 0 24 24"
342
+ fill="none"
343
+ stroke="currentColor"
344
+ strokeWidth="2"
345
+ strokeLinecap="round"
346
+ strokeLinejoin="round"
347
+ >
348
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
349
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
350
+ </svg>
351
+ )}
352
+ </button>
353
+
354
+ {/* Port selector or URL input */}
355
+ {urlMode ? (
356
+ <input
357
+ type="text"
358
+ value={customUrl}
359
+ onChange={(e) => {
360
+ setCustomUrl(e.target.value)
361
+ setError(null)
362
+ }}
363
+ onKeyDown={handleKeyDown}
364
+ placeholder="http://localhost:3000/path"
365
+ className="flex-1 text-xs rounded px-2.5 py-1.5 outline-none"
366
+ style={{
367
+ background: 'var(--bg-secondary)',
368
+ color: 'var(--text-primary)',
369
+ border: '1px solid var(--border)',
370
+ }}
371
+ autoFocus
372
+ />
373
+ ) : (
374
+ <select
375
+ value={selectedPort}
376
+ onChange={(e) => {
377
+ setSelectedPort(parseInt(e.target.value, 10))
378
+ setError(null)
379
+ }}
380
+ onKeyDown={handleKeyDown}
381
+ className="flex-1 text-xs rounded px-2.5 py-1.5 outline-none"
382
+ style={{
383
+ background: 'var(--bg-secondary)',
384
+ color: 'var(--text-primary)',
385
+ border: '1px solid var(--border)',
386
+ }}
387
+ >
388
+ {portOptions.map((port) => (
389
+ <option key={port} value={port}>
390
+ http://localhost:{port}
391
+ </option>
392
+ ))}
393
+ </select>
394
+ )}
395
+ </div>
396
+
397
+ {/* Breakpoint selector */}
398
+ <div className="flex items-center gap-1.5 mt-3">
399
+ <span
400
+ className="text-[11px] mr-1"
401
+ style={{ color: 'var(--text-muted)' }}
402
+ >
403
+ Viewport
404
+ </span>
405
+ {(
406
+ Object.entries(BREAKPOINTS) as [
407
+ Breakpoint,
408
+ { label: string; width: number },
409
+ ][]
410
+ )
411
+ .reverse()
412
+ .map(([bp, { label, width }]) => (
413
+ <button
414
+ key={bp}
415
+ onClick={() => {
416
+ setActiveBreakpoint(bp)
417
+ setPreviewWidth(width)
418
+ }}
419
+ className="text-[11px] px-2.5 py-1 rounded transition-colors"
420
+ style={{
421
+ background:
422
+ activeBreakpoint === bp
423
+ ? 'var(--accent-bg)'
424
+ : 'var(--bg-tertiary)',
425
+ color:
426
+ activeBreakpoint === bp
427
+ ? 'var(--accent)'
428
+ : 'var(--text-secondary)',
429
+ border: `1px solid ${activeBreakpoint === bp ? 'var(--accent)' : 'var(--border)'}`,
430
+ }}
431
+ >
432
+ {label}
433
+ <span
434
+ className="ml-1"
435
+ style={{ color: 'var(--text-muted)', fontSize: '10px' }}
436
+ >
437
+ {width}px
438
+ </span>
439
+ </button>
440
+ ))}
441
+ </div>
442
+
443
+ {/* Project folder path (optional) */}
444
+ <div className="mt-3">
445
+ <span
446
+ className="text-[11px] mr-1"
447
+ style={{ color: 'var(--text-muted)' }}
448
+ >
449
+ Project folder
450
+ <span
451
+ className="ml-1"
452
+ style={{ color: 'var(--text-muted)', opacity: 0.6 }}
453
+ >
454
+ (optional)
455
+ </span>
456
+ </span>
457
+ <div className="flex items-center gap-1.5 mt-1">
458
+ <div
459
+ className="flex-1 text-xs rounded px-2.5 py-1.5 font-mono truncate cursor-default select-none"
460
+ style={{
461
+ background: 'var(--bg-secondary)',
462
+ color: folderPath
463
+ ? 'var(--text-primary)'
464
+ : 'var(--text-muted)',
465
+ border: `1px solid ${folderError ? 'var(--error)' : 'var(--border)'}`,
466
+ minHeight: '28px',
467
+ lineHeight: '16px',
468
+ }}
469
+ title={folderPath || undefined}
470
+ onClick={handleBrowse}
471
+ >
472
+ {folderPath || 'Click Browse to select a folder'}
473
+ </div>
474
+ <button
475
+ onClick={handleBrowse}
476
+ disabled={isBrowsing}
477
+ className="px-2.5 py-1.5 text-[11px] rounded transition-colors flex-shrink-0"
478
+ style={{
479
+ background: 'var(--bg-tertiary)',
480
+ color: 'var(--text-secondary)',
481
+ border: '1px solid var(--border)',
482
+ opacity: isBrowsing ? 0.6 : 1,
483
+ }}
484
+ title="Browse for folder"
485
+ >
486
+ {isBrowsing ? '...' : 'Browse'}
487
+ </button>
488
+ </div>
489
+ {folderError && (
490
+ <p
491
+ className="text-[11px] mt-1"
492
+ style={{ color: 'var(--error)' }}
493
+ >
494
+ {folderError}
495
+ </p>
496
+ )}
497
+ </div>
498
+
499
+ {/* Recent URLs */}
500
+ {recentUrls.length > 0 && (
501
+ <div className="mt-4">
502
+ <span
503
+ className="text-[11px] font-medium"
504
+ style={{ color: 'var(--text-muted)' }}
505
+ >
506
+ Recent
507
+ </span>
508
+ <div className="flex flex-wrap gap-1.5 mt-1.5">
509
+ {recentUrls.map((url) => (
510
+ <button
511
+ key={url}
512
+ onClick={() => handleRecentClick(url)}
513
+ className="text-[11px] px-2.5 py-1 rounded transition-colors"
514
+ style={{
515
+ background: 'var(--bg-tertiary)',
516
+ color: 'var(--text-secondary)',
517
+ border: '1px solid var(--border)',
518
+ }}
519
+ onMouseEnter={(e) => {
520
+ e.currentTarget.style.borderColor = 'var(--accent)'
521
+ e.currentTarget.style.color = 'var(--text-primary)'
522
+ }}
523
+ onMouseLeave={(e) => {
524
+ e.currentTarget.style.borderColor = 'var(--border)'
525
+ e.currentTarget.style.color = 'var(--text-secondary)'
526
+ }}
527
+ >
528
+ {url.replace(/^https?:\/\//, '')}
529
+ </button>
530
+ ))}
531
+ </div>
532
+ </div>
533
+ )}
534
+
535
+ {/* Divider */}
536
+ <div
537
+ className="h-px my-5"
538
+ style={{ background: 'var(--border)' }}
539
+ />
540
+
541
+ {/* How to Use — collapsible */}
542
+ <button
543
+ onClick={() => setHowToOpen(!howToOpen)}
544
+ className="flex items-center gap-2 w-full text-left"
545
+ >
546
+ <svg
547
+ width="12"
548
+ height="12"
549
+ viewBox="0 0 24 24"
550
+ fill="none"
551
+ stroke="var(--text-muted)"
552
+ strokeWidth="2"
553
+ strokeLinecap="round"
554
+ strokeLinejoin="round"
555
+ className="transition-transform flex-shrink-0"
556
+ style={{
557
+ transform: howToOpen ? 'rotate(0deg)' : 'rotate(-90deg)',
558
+ }}
559
+ >
560
+ <polyline points="6 9 12 15 18 9" />
561
+ </svg>
562
+ <span
563
+ className="text-xs font-medium"
564
+ style={{ color: 'var(--text-secondary)' }}
565
+ >
566
+ How to Use
567
+ </span>
568
+ </button>
569
+
570
+ {howToOpen && (
571
+ <div
572
+ className="mt-3 rounded-lg px-4 py-4 text-xs leading-relaxed flex flex-col gap-4"
573
+ style={{
574
+ background: 'var(--bg-secondary)',
575
+ color: 'var(--text-secondary)',
576
+ }}
577
+ >
578
+ {/* Connection Methods */}
579
+ <div>
580
+ <h4
581
+ className="text-[11px] font-semibold uppercase tracking-wide mb-2"
582
+ style={{ color: 'var(--text-primary)' }}
583
+ >
584
+ Connection Methods
585
+ </h4>
586
+ <div className="flex flex-col gap-2">
587
+ <div>
588
+ <span style={{ color: 'var(--success)' }}>
589
+ Automatic (Reverse Proxy)
590
+ </span>{' '}
591
+ — Default. The editor loads your page through a built-in
592
+ proxy and injects the inspector script automatically.
593
+ </div>
594
+ <div>
595
+ <span style={{ color: 'var(--warning)' }}>
596
+ Manual (Script Tag)
597
+ </span>{' '}
598
+ — If auto-connect takes longer than 5s, add the provided
599
+ script tag to your project&apos;s HTML layout.
600
+ </div>
601
+ <div>
602
+ <span style={{ color: 'var(--accent)' }}>
603
+ React Native / Expo Web
604
+ </span>{' '}
605
+ — Add the inspector script dynamically in your root
606
+ layout:
607
+ <pre
608
+ className="mt-1.5 px-3 py-2.5 rounded text-[11px] leading-relaxed overflow-x-auto whitespace-pre"
609
+ style={{
610
+ background: 'var(--bg-tertiary)',
611
+ color: 'var(--text-primary)',
612
+ border: '1px solid var(--border)',
613
+ }}
614
+ >{`useEffect(() => {
615
+ if (Platform.OS === 'web') {
616
+ const script1 = document.createElement('script');
617
+ script1.src = 'http://localhost:4000/dev-editor-inspector.js';
618
+ document.body.appendChild(script1);
619
+
620
+ const script2 = document.createElement('script');
621
+ script2.src = 'https://dev-editor-flow.vercel.app/dev-editor-inspector.js';
622
+ document.body.appendChild(script2);
623
+
624
+ return () => {
625
+ document.body.removeChild(script1);
626
+ document.body.removeChild(script2);
627
+ };
628
+ }
629
+ }`}</pre>
630
+ </div>
631
+ </div>
632
+ </div>
633
+
634
+ {/* What You Can Do */}
635
+ <div>
636
+ <h4
637
+ className="text-[11px] font-semibold uppercase tracking-wide mb-2"
638
+ style={{ color: 'var(--text-primary)' }}
639
+ >
640
+ What You Can Do
641
+ </h4>
642
+ <ul className="flex flex-col gap-1">
643
+ <li>
644
+ <span style={{ color: 'var(--accent)' }}>
645
+ Style Editing
646
+ </span>{' '}
647
+ — Adjust colors, spacing, typography, borders, and
648
+ layout live
649
+ </li>
650
+ <li>
651
+ <span style={{ color: 'var(--accent)' }}>
652
+ Responsive Testing
653
+ </span>{' '}
654
+ — Switch between Mobile, Tablet, and Desktop breakpoints
655
+ </li>
656
+ <li>
657
+ <span style={{ color: 'var(--accent)' }}>
658
+ Change Tracking
659
+ </span>{' '}
660
+ — Every edit recorded with original and new values
661
+ </li>
662
+ <li>
663
+ <span style={{ color: 'var(--accent)' }}>
664
+ Changelog Export
665
+ </span>{' '}
666
+ — Copy or send changes to Claude Code for source file
667
+ updates
668
+ </li>
669
+ </ul>
670
+ </div>
671
+ </div>
672
+ )}
673
+ </>
674
+ )}
675
+
676
+ {/* ─── STEP: CONFIRM ─── */}
677
+ {isConfirming && (
678
+ <div className="flex flex-col gap-4">
679
+ {/* URL summary */}
680
+ <div>
681
+ <span
682
+ className="text-[11px] font-medium"
683
+ style={{ color: 'var(--text-muted)' }}
684
+ >
685
+ URL
686
+ </span>
687
+ <div
688
+ className="mt-1 text-xs rounded px-3 py-2 font-mono"
689
+ style={{
690
+ background: 'var(--bg-secondary)',
691
+ color: 'var(--text-primary)',
692
+ border: '1px solid var(--border)',
693
+ }}
694
+ >
695
+ {pendingTargetUrl}
696
+ </div>
697
+ </div>
698
+
699
+ {/* Folder summary */}
700
+ <div>
701
+ <span
702
+ className="text-[11px] font-medium"
703
+ style={{ color: 'var(--text-muted)' }}
704
+ >
705
+ Project Folder
706
+ </span>
707
+ <div
708
+ className="mt-1 text-xs rounded px-3 py-2 font-mono truncate"
709
+ style={{
710
+ background: 'var(--bg-secondary)',
711
+ color: 'var(--text-primary)',
712
+ border: '1px solid var(--border)',
713
+ }}
714
+ title={pendingFolderPath || undefined}
715
+ >
716
+ {pendingFolderPath}
717
+ </div>
718
+ </div>
719
+
720
+ <p
721
+ className="text-[11px]"
722
+ style={{ color: 'var(--text-secondary)' }}
723
+ >
724
+ Clicking Confirm will scan this folder for components and CSS
725
+ files before loading the page.
726
+ </p>
727
+ </div>
728
+ )}
729
+
730
+ {/* ─── STEP: SCANNING ─── */}
731
+ {isScanning && (
732
+ <div className="flex flex-col items-center py-6 gap-4">
733
+ <ScanAnimation active={!scanDone} label="SCANNING" />
734
+
735
+ {/* Folder being scanned */}
736
+ <p
737
+ className="text-[11px] font-mono text-center truncate max-w-full px-4"
738
+ style={{ color: 'var(--text-secondary)' }}
739
+ title={pendingFolderPath || undefined}
740
+ >
741
+ {pendingFolderPath}
742
+ </p>
743
+
744
+ {/* Scan result feedback */}
745
+ {scanDone && scanResult && scanResult.success && (
746
+ <div className="flex flex-col items-center gap-1.5">
747
+ <div
748
+ className="text-xs font-medium text-center"
749
+ style={{ color: 'var(--success)' }}
750
+ >
751
+ {[
752
+ scanResult.count > 0
753
+ ? `${scanResult.count} component${scanResult.count !== 1 ? 's' : ''}`
754
+ : null,
755
+ scanResult.pageCount > 0
756
+ ? `${scanResult.pageCount} page${scanResult.pageCount !== 1 ? 's' : ''}`
757
+ : null,
758
+ scanResult.cssFileCount > 0
759
+ ? `${scanResult.cssFileCount} CSS file${scanResult.cssFileCount !== 1 ? 's' : ''}`
760
+ : null,
761
+ ]
762
+ .filter(Boolean)
763
+ .join(', ') || 'No files found'}
764
+ </div>
765
+ {(scanResult.framework ||
766
+ scanResult.cssStrategy.length > 0) && (
767
+ <div
768
+ className="text-[11px] text-center"
769
+ style={{ color: 'var(--text-secondary)' }}
770
+ >
771
+ {[
772
+ scanResult.framework,
773
+ scanResult.cssStrategy.length > 0
774
+ ? scanResult.cssStrategy.join(', ')
775
+ : null,
776
+ ]
777
+ .filter(Boolean)
778
+ .join(' \u00b7 ')}
779
+ </div>
780
+ )}
781
+ </div>
782
+ )}
783
+ {scanDone && scanResult && !scanResult.success && (
784
+ <div
785
+ className="text-xs font-medium text-center"
786
+ style={{ color: 'var(--error)' }}
787
+ >
788
+ {scanResult.error || 'Scan failed'}
789
+ </div>
790
+ )}
791
+
792
+ {/* Error: continue anyway / back */}
793
+ {scanDone && scanResult && !scanResult.success && (
794
+ <div className="flex items-center gap-3 mt-2">
795
+ <button
796
+ onClick={handleBack}
797
+ className="px-4 py-1.5 text-[11px] rounded transition-colors"
798
+ style={{
799
+ background: 'var(--bg-tertiary)',
800
+ color: 'var(--text-secondary)',
801
+ border: '1px solid var(--border)',
802
+ }}
803
+ >
804
+ Back
805
+ </button>
806
+ <button
807
+ onClick={handleContinueAnyway}
808
+ className="px-4 py-1.5 text-[11px] rounded font-medium transition-colors"
809
+ style={{
810
+ background: 'var(--accent)',
811
+ color: '#fff',
812
+ }}
813
+ >
814
+ Continue anyway
815
+ </button>
816
+ </div>
817
+ )}
818
+ </div>
819
+ )}
820
+
821
+ {/* ─── STEP: CONNECTING ─── */}
822
+ {isConnecting && (
823
+ <div className="flex flex-col items-center py-8 gap-3">
824
+ {/* Spinner */}
825
+ <div
826
+ className="w-8 h-8 rounded-full"
827
+ style={{
828
+ border: '2px solid var(--border)',
829
+ borderTopColor: 'var(--accent)',
830
+ animation: 'spin 0.8s linear infinite',
831
+ }}
832
+ />
833
+ <p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
834
+ Connecting to {targetUrl?.replace(/^https?:\/\//, '')}...
835
+ </p>
836
+ </div>
837
+ )}
838
+ </div>
839
+
840
+ {/* Script fallback banner — shown after 5s of connecting */}
841
+ {showScriptFallback && (
842
+ <div
843
+ className="px-6 py-3 flex-shrink-0"
844
+ style={{
845
+ borderTop: '1px solid var(--border)',
846
+ background: 'var(--bg-secondary)',
847
+ }}
848
+ >
849
+ <div
850
+ className="text-xs font-medium mb-1"
851
+ style={{ color: 'var(--warning)' }}
852
+ >
853
+ {isLocal
854
+ ? 'Inspector script not detected'
855
+ : 'Script tag required'}
856
+ </div>
857
+ <div
858
+ className="text-[11px] mb-2"
859
+ style={{ color: 'var(--text-secondary)' }}
860
+ >
861
+ {isLocal
862
+ ? "Add this script tag to your project's HTML layout:"
863
+ : "Since the editor is running remotely, add this script tag to your project's HTML layout to enable inspection:"}
864
+ </div>
865
+ <div className="flex items-center gap-2">
866
+ <code
867
+ className="flex-1 text-[11px] px-2 py-1.5 rounded overflow-x-auto whitespace-nowrap"
868
+ style={{
869
+ background: 'var(--bg-primary)',
870
+ color: 'var(--text-primary)',
871
+ border: '1px solid var(--border)',
872
+ }}
873
+ >
874
+ {`<script src="${typeof window !== 'undefined' ? window.location.origin : ''}/dev-editor-inspector.js"></script>`}
875
+ </code>
876
+ <button
877
+ onClick={handleCopyScript}
878
+ className="px-3 py-1.5 text-[11px] font-medium rounded whitespace-nowrap transition-colors flex-shrink-0"
879
+ style={{
880
+ background: scriptCopied ? 'var(--success)' : 'var(--accent)',
881
+ color: '#fff',
882
+ }}
883
+ >
884
+ {scriptCopied ? 'Copied!' : 'Copy'}
885
+ </button>
886
+ </div>
887
+ </div>
888
+ )}
889
+
890
+ {/* Footer */}
891
+ <div
892
+ className="px-6 py-4 flex-shrink-0"
893
+ style={{ borderTop: '1px solid var(--border)' }}
894
+ >
895
+ {error && (
896
+ <p className="text-xs mb-2" style={{ color: 'var(--error)' }}>
897
+ {error}
898
+ </p>
899
+ )}
900
+
901
+ {/* SETUP footer: Connect button */}
902
+ {connectionStatus === 'disconnected' && (
903
+ <>
904
+ <button
905
+ onClick={handleConnect}
906
+ className="w-full py-2 text-xs rounded font-medium transition-colors"
907
+ style={{
908
+ background: 'var(--accent)',
909
+ color: '#fff',
910
+ }}
911
+ >
912
+ Connect
913
+ </button>
914
+ <div className="mt-3 text-center">
915
+ <a
916
+ href="/docs"
917
+ target="_blank"
918
+ rel="noopener noreferrer"
919
+ className="text-xs no-underline transition-colors"
920
+ style={{ color: 'var(--text-muted)' }}
921
+ onMouseEnter={(e) =>
922
+ (e.currentTarget.style.color = 'var(--accent)')
923
+ }
924
+ onMouseLeave={(e) =>
925
+ (e.currentTarget.style.color = 'var(--text-muted)')
926
+ }
927
+ >
928
+ Setup Guide & Docs
929
+ </a>
930
+ </div>
931
+ </>
932
+ )}
933
+
934
+ {/* CONFIRM footer: Back + Confirm buttons */}
935
+ {isConfirming && (
936
+ <div className="flex items-center gap-3">
937
+ <button
938
+ onClick={handleBack}
939
+ className="flex-1 py-2 text-xs rounded font-medium transition-colors"
940
+ style={{
941
+ background: 'var(--bg-tertiary)',
942
+ color: 'var(--text-secondary)',
943
+ border: '1px solid var(--border)',
944
+ }}
945
+ >
946
+ Back
947
+ </button>
948
+ <button
949
+ onClick={handleConfirm}
950
+ className="flex-1 py-2 text-xs rounded font-medium transition-colors"
951
+ style={{
952
+ background: 'var(--accent)',
953
+ color: '#fff',
954
+ }}
955
+ >
956
+ Confirm
957
+ </button>
958
+ </div>
959
+ )}
960
+
961
+ {/* SCANNING footer: Cancel */}
962
+ {isScanning && !scanDone && (
963
+ <button
964
+ onClick={cancelConnection}
965
+ className="w-full py-2 text-xs rounded font-medium transition-colors"
966
+ style={{
967
+ background: 'var(--bg-tertiary)',
968
+ color: 'var(--text-secondary)',
969
+ border: '1px solid var(--border)',
970
+ }}
971
+ >
972
+ Cancel
973
+ </button>
974
+ )}
975
+
976
+ {/* CONNECTING footer: Cancel */}
977
+ {isConnecting && (
978
+ <button
979
+ onClick={cancelConnection}
980
+ className="w-full py-2 text-xs rounded font-medium transition-colors"
981
+ style={{
982
+ background: 'var(--bg-tertiary)',
983
+ color: 'var(--text-secondary)',
984
+ border: '1px solid var(--border)',
985
+ }}
986
+ >
987
+ Cancel
988
+ </button>
989
+ )}
990
+ </div>
991
+ </div>
992
+ </div>
993
+ )
994
+ }