@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.
- package/LICENSE +178 -0
- package/NOTICE +4 -0
- package/README.md +180 -0
- package/bin/paint.js +266 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +19 -0
- package/package.json +81 -0
- package/postcss.config.mjs +8 -0
- package/public/dev-editor-inspector.js +1872 -0
- package/src/app/api/claude/analyze/route.ts +319 -0
- package/src/app/api/claude/apply/route.ts +185 -0
- package/src/app/api/claude/pick-folder/route.ts +64 -0
- package/src/app/api/claude/scan/route.ts +221 -0
- package/src/app/api/claude/status/route.ts +55 -0
- package/src/app/api/project/scan/route.ts +634 -0
- package/src/app/api/project-scan/css-variables/route.ts +238 -0
- package/src/app/api/project-scan/route.ts +40 -0
- package/src/app/api/project-scan/tailwind-config/route.ts +172 -0
- package/src/app/api/proxy/[[...path]]/route.ts +2400 -0
- package/src/app/docs/DocsClient.tsx +322 -0
- package/src/app/docs/layout.tsx +7 -0
- package/src/app/docs/page.tsx +855 -0
- package/src/app/globals.css +176 -0
- package/src/app/layout.tsx +19 -0
- package/src/app/page.tsx +46 -0
- package/src/bridge/api-handlers.ts +885 -0
- package/src/bridge/proxy-handler.ts +329 -0
- package/src/bridge/server.ts +113 -0
- package/src/components/BreakpointTabs.tsx +72 -0
- package/src/components/ChangeSummaryModal.tsx +267 -0
- package/src/components/ConnectModal.tsx +994 -0
- package/src/components/Editor.tsx +90 -0
- package/src/components/PageSelector.tsx +208 -0
- package/src/components/PreviewFrame.tsx +299 -0
- package/src/components/ProjectFolderBanner.tsx +91 -0
- package/src/components/ResponsiveToolbar.tsx +222 -0
- package/src/components/TargetSelector.tsx +243 -0
- package/src/components/TopBar.tsx +315 -0
- package/src/components/common/CollapsibleSection.tsx +36 -0
- package/src/components/common/ColorPicker.tsx +920 -0
- package/src/components/common/EditablePre.tsx +136 -0
- package/src/components/common/ErrorBoundary.tsx +65 -0
- package/src/components/common/ResizablePanel.tsx +83 -0
- package/src/components/common/ScanAnimation.tsx +76 -0
- package/src/components/common/ToastContainer.tsx +97 -0
- package/src/components/common/UnitInput.tsx +77 -0
- package/src/components/common/VariableColorPicker.tsx +622 -0
- package/src/components/left-panel/AddElementPanel.tsx +237 -0
- package/src/components/left-panel/ComponentsPanel.tsx +609 -0
- package/src/components/left-panel/IconSidebar.tsx +99 -0
- package/src/components/left-panel/LayerNode.tsx +874 -0
- package/src/components/left-panel/LayerSearch.tsx +23 -0
- package/src/components/left-panel/LayersPanel.tsx +52 -0
- package/src/components/left-panel/LeftPanel.tsx +122 -0
- package/src/components/left-panel/PagesPanel.tsx +114 -0
- package/src/components/left-panel/icons.tsx +162 -0
- package/src/components/left-panel/terminal/ScanOverlay.tsx +66 -0
- package/src/components/left-panel/terminal/TerminalPanel.tsx +217 -0
- package/src/components/right-panel/ElementLogBox.tsx +248 -0
- package/src/components/right-panel/PanelTabs.tsx +83 -0
- package/src/components/right-panel/RightPanel.tsx +41 -0
- package/src/components/right-panel/changes/AiScanResultPanel.tsx +285 -0
- package/src/components/right-panel/changes/ChangeEntry.tsx +59 -0
- package/src/components/right-panel/changes/ChangelogActions.tsx +105 -0
- package/src/components/right-panel/changes/ChangesPanel.tsx +1474 -0
- package/src/components/right-panel/claude/ApplyConfirmModal.tsx +376 -0
- package/src/components/right-panel/claude/ClaudeErrorState.tsx +125 -0
- package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +482 -0
- package/src/components/right-panel/claude/ClaudeProgressIndicator.tsx +76 -0
- package/src/components/right-panel/claude/DiffCard.tsx +130 -0
- package/src/components/right-panel/claude/DiffViewer.tsx +54 -0
- package/src/components/right-panel/claude/ProjectRootSelector.tsx +275 -0
- package/src/components/right-panel/claude/ResultsSummary.tsx +119 -0
- package/src/components/right-panel/claude/SetupFlow.tsx +315 -0
- package/src/components/right-panel/console/ConsolePanel.tsx +209 -0
- package/src/components/right-panel/design/AppearanceSection.tsx +703 -0
- package/src/components/right-panel/design/BackgroundSection.tsx +516 -0
- package/src/components/right-panel/design/BorderSection.tsx +161 -0
- package/src/components/right-panel/design/CSSRawView.tsx +412 -0
- package/src/components/right-panel/design/DesignCSSTabToggle.tsx +51 -0
- package/src/components/right-panel/design/DesignPanel.tsx +275 -0
- package/src/components/right-panel/design/ElementBreadcrumb.tsx +51 -0
- package/src/components/right-panel/design/GradientEditor.tsx +726 -0
- package/src/components/right-panel/design/LayoutSection.tsx +1948 -0
- package/src/components/right-panel/design/PositionSection.tsx +865 -0
- package/src/components/right-panel/design/PropertiesSection.tsx +86 -0
- package/src/components/right-panel/design/SVGSection.tsx +361 -0
- package/src/components/right-panel/design/ShadowBlurSection.tsx +227 -0
- package/src/components/right-panel/design/SizeSection.tsx +183 -0
- package/src/components/right-panel/design/TextSection.tsx +719 -0
- package/src/components/right-panel/design/icons.tsx +948 -0
- package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +467 -0
- package/src/components/right-panel/design/inputs/ColorInput.tsx +43 -0
- package/src/components/right-panel/design/inputs/CompactInput.tsx +333 -0
- package/src/components/right-panel/design/inputs/DraggableLabel.tsx +118 -0
- package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +54 -0
- package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +174 -0
- package/src/components/right-panel/design/inputs/SectionHeader.tsx +79 -0
- package/src/components/right-panel/variables/VariablesPanel.tsx +388 -0
- package/src/hooks/useBridge.ts +95 -0
- package/src/hooks/useChangeTracker.ts +563 -0
- package/src/hooks/useClaudeAPI.ts +118 -0
- package/src/hooks/useDOMTree.ts +25 -0
- package/src/hooks/useKeyboardShortcuts.ts +76 -0
- package/src/hooks/usePostMessage.ts +589 -0
- package/src/hooks/useProjectScan.ts +204 -0
- package/src/hooks/useResizable.ts +20 -0
- package/src/hooks/useSelectedElement.ts +51 -0
- package/src/hooks/useTargetUrl.ts +81 -0
- package/src/inspector/DOMTraverser.ts +71 -0
- package/src/inspector/ElementSelector.ts +23 -0
- package/src/inspector/HoverHighlighter.ts +54 -0
- package/src/inspector/SelectionHighlighter.ts +27 -0
- package/src/inspector/StyleExtractor.ts +19 -0
- package/src/inspector/inspector.ts +17 -0
- package/src/inspector/messaging.ts +30 -0
- package/src/lib/apiBase.ts +15 -0
- package/src/lib/classifyElement.ts +430 -0
- package/src/lib/claude-bin.ts +197 -0
- package/src/lib/claude-stream.ts +158 -0
- package/src/lib/clientProjectScanner.ts +344 -0
- package/src/lib/componentMatcher.ts +156 -0
- package/src/lib/constants.ts +573 -0
- package/src/lib/cssVariableUtils.ts +409 -0
- package/src/lib/diffParser.ts +206 -0
- package/src/lib/folderPicker.ts +84 -0
- package/src/lib/gradientParser.ts +160 -0
- package/src/lib/projectScanner.ts +355 -0
- package/src/lib/promptBuilder.ts +402 -0
- package/src/lib/shadowParser.ts +124 -0
- package/src/lib/tailwindClassParser.ts +248 -0
- package/src/lib/textShadowUtils.ts +106 -0
- package/src/lib/utils.ts +299 -0
- package/src/lib/validatePath.ts +40 -0
- package/src/proxy.ts +92 -0
- package/src/server/terminal-server.ts +104 -0
- package/src/store/changeSlice.ts +288 -0
- package/src/store/claudeSlice.ts +222 -0
- package/src/store/componentSlice.ts +90 -0
- package/src/store/consoleSlice.ts +51 -0
- package/src/store/cssVariableSlice.ts +94 -0
- package/src/store/elementSlice.ts +78 -0
- package/src/store/index.ts +35 -0
- package/src/store/terminalSlice.ts +30 -0
- package/src/store/treeSlice.ts +69 -0
- package/src/store/uiSlice.ts +327 -0
- package/src/types/changelog.ts +49 -0
- package/src/types/claude.ts +131 -0
- package/src/types/component.ts +49 -0
- package/src/types/cssVariables.ts +18 -0
- package/src/types/element.ts +21 -0
- package/src/types/file-system-access.d.ts +27 -0
- package/src/types/gradient.ts +12 -0
- package/src/types/messages.ts +392 -0
- package/src/types/shadow.ts +8 -0
- package/src/types/tree.ts +9 -0
- package/tsconfig.json +42 -0
- 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'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
|
+
}
|