@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,222 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useCallback, useEffect } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { usePostMessage } from '@/hooks/usePostMessage'
6
+ import {
7
+ DEVICE_PRESETS,
8
+ PREVIEW_WIDTH_MIN,
9
+ PREVIEW_WIDTH_MAX,
10
+ BREAKPOINT_CATEGORY_MAP,
11
+ } from '@/lib/constants'
12
+ import type { DevicePreset } from '@/lib/constants'
13
+
14
+ export function ResponsiveToolbar() {
15
+ const previewWidth = useEditorStore((s) => s.previewWidth)
16
+ const setPreviewWidth = useEditorStore((s) => s.setPreviewWidth)
17
+ const activeBreakpoint = useEditorStore((s) => s.activeBreakpoint)
18
+ const { sendToInspector } = usePostMessage()
19
+
20
+ // Filter devices by active breakpoint category
21
+ const activeCategory = BREAKPOINT_CATEGORY_MAP[activeBreakpoint]
22
+ const filteredDevices = DEVICE_PRESETS.filter(
23
+ (d) => d.category === activeCategory,
24
+ )
25
+
26
+ const [dropdownOpen, setDropdownOpen] = useState(false)
27
+ const [inputValue, setInputValue] = useState(String(previewWidth))
28
+ const dropdownRef = useRef<HTMLDivElement>(null)
29
+
30
+ // Sync input when previewWidth changes externally (drag, slider, preset)
31
+ useEffect(() => {
32
+ setInputValue(String(previewWidth))
33
+ }, [previewWidth])
34
+
35
+ // Close dropdown on outside click
36
+ useEffect(() => {
37
+ if (!dropdownOpen) return
38
+ const handler = (e: MouseEvent) => {
39
+ if (
40
+ dropdownRef.current &&
41
+ !dropdownRef.current.contains(e.target as Node)
42
+ ) {
43
+ setDropdownOpen(false)
44
+ }
45
+ }
46
+ document.addEventListener('mousedown', handler)
47
+ return () => document.removeEventListener('mousedown', handler)
48
+ }, [dropdownOpen])
49
+
50
+ const applyWidth = useCallback(
51
+ (width: number) => {
52
+ setPreviewWidth(width)
53
+ sendToInspector({ type: 'SET_BREAKPOINT', payload: { width } })
54
+ },
55
+ [setPreviewWidth, sendToInspector],
56
+ )
57
+
58
+ const handleInputCommit = useCallback(() => {
59
+ const parsed = parseInt(inputValue, 10)
60
+ if (!isNaN(parsed)) {
61
+ applyWidth(parsed)
62
+ } else {
63
+ setInputValue(String(previewWidth))
64
+ }
65
+ }, [inputValue, applyWidth, previewWidth])
66
+
67
+ const handleInputKeyDown = useCallback(
68
+ (e: React.KeyboardEvent) => {
69
+ if (e.key === 'Enter') {
70
+ handleInputCommit()
71
+ ;(e.target as HTMLInputElement).blur()
72
+ } else if (e.key === 'Escape') {
73
+ setInputValue(String(previewWidth))
74
+ ;(e.target as HTMLInputElement).blur()
75
+ }
76
+ },
77
+ [handleInputCommit, previewWidth],
78
+ )
79
+
80
+ // Find matching device name
81
+ const matchedDevice = DEVICE_PRESETS.find((d) => d.width === previewWidth)
82
+
83
+ return (
84
+ <div
85
+ className="flex items-center gap-3 px-3 py-1.5 border-b select-none"
86
+ style={{
87
+ background: '#252526',
88
+ borderColor: 'var(--border)',
89
+ }}
90
+ >
91
+ {/* Device dropdown */}
92
+ <div ref={dropdownRef} className="relative">
93
+ <button
94
+ onClick={() => setDropdownOpen(!dropdownOpen)}
95
+ className="flex items-center gap-1.5 px-2 py-1 rounded text-[11px] transition-colors"
96
+ style={{
97
+ background: dropdownOpen ? 'var(--bg-hover)' : 'transparent',
98
+ color: matchedDevice
99
+ ? 'var(--text-primary)'
100
+ : 'var(--text-secondary)',
101
+ border: '1px solid var(--border)',
102
+ }}
103
+ >
104
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
105
+ <path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3zm1 0v7h10V3H3zm-1 9.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z" />
106
+ </svg>
107
+ <span className="max-w-[100px] truncate">
108
+ {matchedDevice ? matchedDevice.name : 'Custom'}
109
+ </span>
110
+ <svg
111
+ width="8"
112
+ height="8"
113
+ viewBox="0 0 8 8"
114
+ fill="currentColor"
115
+ className="opacity-60"
116
+ >
117
+ <path d="M1 3l3 3 3-3z" />
118
+ </svg>
119
+ </button>
120
+
121
+ {dropdownOpen && (
122
+ <div
123
+ className="absolute top-full left-0 mt-1 w-52 rounded shadow-lg z-50 py-1 overflow-auto max-h-64"
124
+ style={{
125
+ background: '#2a2a2a',
126
+ border: '1px solid var(--border)',
127
+ }}
128
+ >
129
+ <div
130
+ className="px-3 py-1 text-[10px] uppercase tracking-wider"
131
+ style={{ color: 'var(--text-muted)' }}
132
+ >
133
+ {activeCategory === 'phone'
134
+ ? 'Phones'
135
+ : activeCategory === 'tablet'
136
+ ? 'Tablets'
137
+ : 'Desktops'}
138
+ </div>
139
+ {filteredDevices.map((device) => (
140
+ <button
141
+ key={device.name}
142
+ onClick={() => {
143
+ applyWidth(device.width)
144
+ setDropdownOpen(false)
145
+ }}
146
+ className="w-full text-left px-3 py-1.5 text-[11px] flex justify-between items-center transition-colors"
147
+ style={{
148
+ color:
149
+ previewWidth === device.width
150
+ ? 'var(--accent)'
151
+ : 'var(--text-primary)',
152
+ background:
153
+ previewWidth === device.width
154
+ ? 'rgba(74, 158, 255, 0.1)'
155
+ : 'transparent',
156
+ }}
157
+ onMouseEnter={(e) => {
158
+ if (previewWidth !== device.width) {
159
+ e.currentTarget.style.background = 'var(--bg-hover)'
160
+ }
161
+ }}
162
+ onMouseLeave={(e) => {
163
+ e.currentTarget.style.background =
164
+ previewWidth === device.width
165
+ ? 'rgba(74, 158, 255, 0.1)'
166
+ : 'transparent'
167
+ }}
168
+ >
169
+ <span>{device.name}</span>
170
+ <span style={{ color: 'var(--text-muted)' }}>
171
+ {device.width}px
172
+ </span>
173
+ </button>
174
+ ))}
175
+ </div>
176
+ )}
177
+ </div>
178
+
179
+ {/* Width input */}
180
+ <div className="flex items-center gap-1">
181
+ <input
182
+ type="text"
183
+ value={inputValue}
184
+ onChange={(e) => setInputValue(e.target.value)}
185
+ onBlur={handleInputCommit}
186
+ onKeyDown={handleInputKeyDown}
187
+ className="w-14 text-center text-[11px] px-1.5 py-0.5 rounded outline-none"
188
+ style={{
189
+ background: '#1e1e1e',
190
+ color: 'var(--text-primary)',
191
+ border: '1px solid var(--border)',
192
+ }}
193
+ onFocus={(e) => {
194
+ e.currentTarget.style.borderColor = 'var(--accent)'
195
+ }}
196
+ onBlurCapture={(e) => {
197
+ e.currentTarget.style.borderColor = 'var(--border)'
198
+ }}
199
+ />
200
+ <span className="text-[10px]" style={{ color: 'var(--text-muted)' }}>
201
+ px
202
+ </span>
203
+ </div>
204
+
205
+ {/* Width slider */}
206
+ <input
207
+ type="range"
208
+ min={PREVIEW_WIDTH_MIN}
209
+ max={PREVIEW_WIDTH_MAX}
210
+ value={previewWidth}
211
+ onChange={(e) => applyWidth(parseInt(e.target.value, 10))}
212
+ className="flex-1 h-1 appearance-none rounded cursor-pointer"
213
+ style={{
214
+ accentColor: '#4a9eff',
215
+ background: '#3a3a3a',
216
+ minWidth: 80,
217
+ maxWidth: 200,
218
+ }}
219
+ />
220
+ </div>
221
+ )
222
+ }
@@ -0,0 +1,243 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { normalizeTargetUrl } from '@/lib/utils'
6
+ import { LOCAL_STORAGE_KEYS } from '@/lib/constants'
7
+ import { isEditorOnLocalhost } from '@/hooks/usePostMessage'
8
+
9
+ export function TargetSelector() {
10
+ const targetUrl = useEditorStore((s) => s.targetUrl)
11
+ const connectionStatus = useEditorStore((s) => s.connectionStatus)
12
+ const setTargetUrl = useEditorStore((s) => s.setTargetUrl)
13
+ const setConnectionStatus = useEditorStore((s) => s.setConnectionStatus)
14
+ const addRecentUrl = useEditorStore((s) => s.addRecentUrl)
15
+ const bridgeStatus = useEditorStore((s) => s.bridgeStatus)
16
+ const isRemote = typeof window !== 'undefined' && !isEditorOnLocalhost()
17
+
18
+ const portOptions = Array.from({ length: 8 }, (_, i) => 3000 + i)
19
+ const getPortFromUrl = (url: string | null) => {
20
+ if (!url) return 3000
21
+ const match = url.match(/:(\d+)/)
22
+ return match ? parseInt(match[1], 10) : 3000
23
+ }
24
+ const [selectedPort, setSelectedPort] = useState(getPortFromUrl(targetUrl))
25
+ const [urlMode, setUrlMode] = useState(false)
26
+ const [customUrl, setCustomUrl] = useState('http://localhost:3000')
27
+ const [error, setError] = useState<string | null>(null)
28
+
29
+ const isConnected = connectionStatus === 'connected'
30
+
31
+ const handleConnect = () => {
32
+ setError(null)
33
+ const raw = urlMode ? customUrl.trim() : `http://localhost:${selectedPort}`
34
+ if (urlMode && !raw) {
35
+ setError('Enter a URL')
36
+ return
37
+ }
38
+ const normalized = normalizeTargetUrl(raw)
39
+ setTargetUrl(normalized)
40
+ setConnectionStatus('connecting')
41
+ addRecentUrl(normalized)
42
+ }
43
+
44
+ const handleDisconnect = () => {
45
+ // Clear persisted changes for this URL so reconnect loads fresh content
46
+ if (targetUrl) {
47
+ try {
48
+ localStorage.removeItem(LOCAL_STORAGE_KEYS.CHANGES_PREFIX + targetUrl)
49
+ } catch {}
50
+ }
51
+ const store = useEditorStore.getState()
52
+ store.clearAllChanges()
53
+ store.clearSelection()
54
+ setTargetUrl(null)
55
+ setConnectionStatus('disconnected')
56
+ setError(null)
57
+ }
58
+
59
+ const handlePortChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
60
+ setSelectedPort(parseInt(e.target.value, 10))
61
+ setError(null)
62
+ }
63
+
64
+ const handleKeyDown = (e: React.KeyboardEvent) => {
65
+ if (e.key === 'Enter' && !isConnected) {
66
+ handleConnect()
67
+ }
68
+ }
69
+
70
+ const statusColor =
71
+ connectionStatus === 'connected'
72
+ ? 'var(--success)'
73
+ : connectionStatus === 'connecting' ||
74
+ connectionStatus === 'confirming' ||
75
+ connectionStatus === 'scanning'
76
+ ? 'var(--warning)'
77
+ : 'var(--error)'
78
+
79
+ return (
80
+ <div className="flex items-center gap-2 relative">
81
+ {/* Connection status dot */}
82
+ <div
83
+ className="w-2 h-2 rounded-full flex-shrink-0"
84
+ style={{ background: statusColor }}
85
+ title={connectionStatus}
86
+ />
87
+
88
+ {/* Toggle between dropdown and URL input */}
89
+ <button
90
+ onClick={() => {
91
+ if (!isConnected) {
92
+ setUrlMode(!urlMode)
93
+ setError(null)
94
+ }
95
+ }}
96
+ className="p-1 rounded hover:bg-[var(--bg-hover)] transition-colors flex-shrink-0"
97
+ style={{
98
+ color: urlMode ? 'var(--accent)' : 'var(--text-muted)',
99
+ cursor: isConnected ? 'default' : 'pointer',
100
+ opacity: isConnected ? 0.5 : 1,
101
+ }}
102
+ title={urlMode ? 'Switch to port selector' : 'Switch to URL input'}
103
+ >
104
+ {urlMode ? (
105
+ <svg
106
+ width="14"
107
+ height="14"
108
+ viewBox="0 0 24 24"
109
+ fill="none"
110
+ stroke="currentColor"
111
+ strokeWidth="2"
112
+ strokeLinecap="round"
113
+ strokeLinejoin="round"
114
+ >
115
+ <polyline points="6 9 6 2 18 2 18 9" />
116
+ <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" />
117
+ <rect x="6" y="14" width="12" height="8" />
118
+ </svg>
119
+ ) : (
120
+ <svg
121
+ width="14"
122
+ height="14"
123
+ viewBox="0 0 24 24"
124
+ fill="none"
125
+ stroke="currentColor"
126
+ strokeWidth="2"
127
+ strokeLinecap="round"
128
+ strokeLinejoin="round"
129
+ >
130
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
131
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
132
+ </svg>
133
+ )}
134
+ </button>
135
+
136
+ {isConnected ? (
137
+ <div
138
+ className="w-56 text-sm rounded px-2 py-1 truncate"
139
+ style={{
140
+ background: 'var(--bg-secondary)',
141
+ color: 'var(--text-primary)',
142
+ border: '1px solid var(--border)',
143
+ opacity: 0.7,
144
+ }}
145
+ title={targetUrl || ''}
146
+ >
147
+ {targetUrl || 'http://localhost:3000'}
148
+ </div>
149
+ ) : urlMode ? (
150
+ <input
151
+ type="text"
152
+ value={customUrl}
153
+ onChange={(e) => {
154
+ setCustomUrl(e.target.value)
155
+ setError(null)
156
+ }}
157
+ onKeyDown={handleKeyDown}
158
+ placeholder="http://localhost:3000/path"
159
+ className="w-56 text-xs bg-[var(--bg-secondary)] text-[var(--text-primary)] border border-[var(--border)] rounded px-2 py-1 outline-none focus:border-[var(--accent)]"
160
+ />
161
+ ) : (
162
+ <select
163
+ value={selectedPort}
164
+ onChange={handlePortChange}
165
+ className="w-56 text-xs bg-[var(--bg-secondary)] text-[var(--text-primary)] border border-[var(--border)] rounded px-2 py-1 outline-none focus:border-[var(--accent)]"
166
+ >
167
+ {portOptions.map((port) => (
168
+ <option key={port} value={port}>
169
+ http://localhost:{port}
170
+ </option>
171
+ ))}
172
+ </select>
173
+ )}
174
+
175
+ {/* Connect / Disconnect button */}
176
+ <button
177
+ onClick={isConnected ? handleDisconnect : handleConnect}
178
+ className="px-3 py-1 text-xs rounded transition-colors font-medium"
179
+ style={{
180
+ background: isConnected ? 'var(--bg-hover)' : 'var(--accent)',
181
+ color: isConnected ? 'var(--text-secondary)' : '#fff',
182
+ }}
183
+ >
184
+ {isConnected
185
+ ? 'Disconnect'
186
+ : connectionStatus === 'connecting'
187
+ ? 'Connecting...'
188
+ : 'Connect'}
189
+ </button>
190
+
191
+ {/* Bridge status indicator (shown when running on Vercel) */}
192
+ {isRemote && (
193
+ <div
194
+ className="flex items-center gap-1 flex-shrink-0"
195
+ title={
196
+ bridgeStatus === 'connected'
197
+ ? 'Bridge connected'
198
+ : bridgeStatus === 'checking'
199
+ ? 'Detecting bridge...'
200
+ : 'Bridge not detected — run: bun run bridge'
201
+ }
202
+ >
203
+ <div
204
+ className="w-1.5 h-1.5 rounded-full"
205
+ style={{
206
+ background:
207
+ bridgeStatus === 'connected'
208
+ ? 'var(--accent)'
209
+ : bridgeStatus === 'checking'
210
+ ? 'var(--warning)'
211
+ : 'var(--text-muted)',
212
+ }}
213
+ />
214
+ <span
215
+ className="text-[10px]"
216
+ style={{
217
+ color:
218
+ bridgeStatus === 'connected'
219
+ ? 'var(--accent)'
220
+ : 'var(--text-muted)',
221
+ }}
222
+ >
223
+ {bridgeStatus === 'connected'
224
+ ? 'Bridge'
225
+ : bridgeStatus === 'checking'
226
+ ? '...'
227
+ : 'No bridge'}
228
+ </span>
229
+ </div>
230
+ )}
231
+
232
+ {/* Error message */}
233
+ {error && (
234
+ <span
235
+ className="text-xs absolute -bottom-5 left-6"
236
+ style={{ color: 'var(--error)' }}
237
+ >
238
+ {error}
239
+ </span>
240
+ )}
241
+ </div>
242
+ )
243
+ }