@antigenic-oss/paint 0.2.9 → 0.3.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.
@@ -49,43 +49,14 @@ export function ConnectModal() {
49
49
  const [isBrowsing, setIsBrowsing] = useState(false)
50
50
  const [error, setError] = useState<string | null>(null)
51
51
  const [howToOpen, setHowToOpen] = useState(false)
52
- const [showScriptFallback, setShowScriptFallback] = useState(false)
53
- const [scriptCopied, setScriptCopied] = useState(false)
54
52
  const [scanResult, setScanResult] = useState<ScanResult | null>(null)
55
53
  const [scanDone, setScanDone] = useState(false)
56
- const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
57
54
  const autoAdvanceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
58
55
 
59
56
  const isConnecting = connectionStatus === 'connecting'
60
57
  const isConfirming = connectionStatus === 'confirming'
61
58
  const isScanning = connectionStatus === 'scanning'
62
59
 
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
60
  // Cleanup auto-advance timer
90
61
  useEffect(() => {
91
62
  return () => {
@@ -104,31 +75,14 @@ export function ConnectModal() {
104
75
  if (isConfirming || isScanning) {
105
76
  cancelPendingConnection()
106
77
  }
107
- setShowScriptFallback(false)
108
- setScriptCopied(false)
109
78
  setScanResult(null)
110
79
  setScanDone(false)
111
- if (fallbackTimerRef.current) {
112
- clearTimeout(fallbackTimerRef.current)
113
- fallbackTimerRef.current = null
114
- }
115
80
  if (autoAdvanceRef.current) {
116
81
  clearTimeout(autoAdvanceRef.current)
117
82
  autoAdvanceRef.current = null
118
83
  }
119
84
  }
120
85
 
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
86
  // Pre-fill folder path from portRoots when selected URL changes
133
87
  const currentUrl = urlMode
134
88
  ? customUrl.trim()
@@ -191,6 +145,19 @@ export function ConnectModal() {
191
145
  }
192
146
  }
193
147
 
148
+ const handleLoginFirst = () => {
149
+ setError(null)
150
+ const raw = urlMode ? customUrl.trim() : `http://localhost:${selectedPort}`
151
+ if (urlMode && !raw) {
152
+ setError('Enter a URL')
153
+ return
154
+ }
155
+ const normalized = normalizeTargetUrl(raw)
156
+ setTargetUrl(normalized)
157
+ setConnectionStatus('authenticating')
158
+ addRecentUrl(normalized)
159
+ }
160
+
194
161
  const handleConfirm = async () => {
195
162
  if (!pendingTargetUrl || !pendingFolderPath) return
196
163
  setConnectionStatus('scanning')
@@ -237,16 +204,17 @@ export function ConnectModal() {
237
204
  }
238
205
  }
239
206
 
207
+ // Auto-dismiss modal once connecting/authenticating starts — top bar shows status
208
+ if (isConnecting || connectionStatus === 'connected' || connectionStatus === 'authenticating') return null
209
+
240
210
  // Header subtitle changes per step
241
211
  const headerSubtitle = isConfirming
242
212
  ? 'Confirm connection details'
243
213
  : isScanning
244
214
  ? 'Scanning project folder'
245
- : isConnecting
246
- ? 'Connecting to your project'
247
- : isLocal
248
- ? 'Connect to your localhost project'
249
- : 'Connect to your project'
215
+ : isLocal
216
+ ? 'Connect to your localhost project'
217
+ : 'Connect to your project'
250
218
 
251
219
  return (
252
220
  <div
@@ -574,58 +542,28 @@ export function ConnectModal() {
574
542
  color: 'var(--text-secondary)',
575
543
  }}
576
544
  >
577
- {/* Connection Methods */}
545
+ {/* How It Works */}
578
546
  <div>
579
547
  <h4
580
548
  className="text-[11px] font-semibold uppercase tracking-wide mb-2"
581
549
  style={{ color: 'var(--text-primary)' }}
582
550
  >
583
- Connection Methods
551
+ How It Works
584
552
  </h4>
585
553
  <div className="flex flex-col gap-2">
586
554
  <div>
555
+ pAInt uses a{' '}
587
556
  <span style={{ color: 'var(--success)' }}>
588
- Automatic (Reverse Proxy)
589
- </span>{' '}
590
- — Default. The editor loads your page through a built-in
591
- proxy and injects the inspector script automatically.
592
- </div>
593
- <div>
594
- <span style={{ color: 'var(--warning)' }}>
595
- Manual (Script Tag)
557
+ Service Worker proxy
596
558
  </span>{' '}
597
- If auto-connect takes longer than 5s, add the provided
598
- script tag to your project&apos;s HTML layout.
559
+ to load your page, inject the inspector script, and
560
+ strip security headers all automatically. No script
561
+ tags or project modifications needed.
599
562
  </div>
600
563
  <div>
601
- <span style={{ color: 'var(--accent)' }}>
602
- React Native / Expo Web
603
- </span>{' '}
604
- — Add the inspector script dynamically in your root
605
- layout:
606
- <pre
607
- className="mt-1.5 px-3 py-2.5 rounded text-[11px] leading-relaxed overflow-x-auto whitespace-pre"
608
- style={{
609
- background: 'var(--bg-tertiary)',
610
- color: 'var(--text-primary)',
611
- border: '1px solid var(--border)',
612
- }}
613
- >{`useEffect(() => {
614
- if (Platform.OS === 'web') {
615
- const script1 = document.createElement('script');
616
- script1.src = 'http://localhost:4000/dev-editor-inspector.js';
617
- document.body.appendChild(script1);
618
-
619
- const script2 = document.createElement('script');
620
- script2.src = 'https://dev-editor-flow.vercel.app/dev-editor-inspector.js';
621
- document.body.appendChild(script2);
622
-
623
- return () => {
624
- document.body.removeChild(script1);
625
- document.body.removeChild(script2);
626
- };
627
- }
628
- }`}</pre>
564
+ Your page&apos;s scripts and client-side rendering
565
+ work normally, so interactive features (3D, animations,
566
+ routing) are fully preserved.
629
567
  </div>
630
568
  </div>
631
569
  </div>
@@ -817,75 +755,8 @@ export function ConnectModal() {
817
755
  </div>
818
756
  )}
819
757
 
820
- {/* ─── STEP: CONNECTING ─── */}
821
- {isConnecting && (
822
- <div className="flex flex-col items-center py-8 gap-3">
823
- {/* Spinner */}
824
- <div
825
- className="w-8 h-8 rounded-full"
826
- style={{
827
- border: '2px solid var(--border)',
828
- borderTopColor: 'var(--accent)',
829
- animation: 'spin 0.8s linear infinite',
830
- }}
831
- />
832
- <p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
833
- Connecting to {targetUrl?.replace(/^https?:\/\//, '')}...
834
- </p>
835
- </div>
836
- )}
837
758
  </div>
838
759
 
839
- {/* Script fallback banner — shown after 5s of connecting */}
840
- {showScriptFallback && (
841
- <div
842
- className="px-6 py-3 flex-shrink-0"
843
- style={{
844
- borderTop: '1px solid var(--border)',
845
- background: 'var(--bg-secondary)',
846
- }}
847
- >
848
- <div
849
- className="text-xs font-medium mb-1"
850
- style={{ color: 'var(--warning)' }}
851
- >
852
- {isLocal
853
- ? 'Inspector script not detected'
854
- : 'Script tag required'}
855
- </div>
856
- <div
857
- className="text-[11px] mb-2"
858
- style={{ color: 'var(--text-secondary)' }}
859
- >
860
- {isLocal
861
- ? "Add this script tag to your project's HTML layout:"
862
- : "Since the editor is running remotely, add this script tag to your project's HTML layout to enable inspection:"}
863
- </div>
864
- <div className="flex items-center gap-2">
865
- <code
866
- className="flex-1 text-[11px] px-2 py-1.5 rounded overflow-x-auto whitespace-nowrap"
867
- style={{
868
- background: 'var(--bg-primary)',
869
- color: 'var(--text-primary)',
870
- border: '1px solid var(--border)',
871
- }}
872
- >
873
- {`<script src="${typeof window !== 'undefined' ? window.location.origin : ''}/dev-editor-inspector.js"></script>`}
874
- </code>
875
- <button
876
- onClick={handleCopyScript}
877
- className="px-3 py-1.5 text-[11px] font-medium rounded whitespace-nowrap transition-colors flex-shrink-0"
878
- style={{
879
- background: scriptCopied ? 'var(--success)' : 'var(--accent)',
880
- color: '#fff',
881
- }}
882
- >
883
- {scriptCopied ? 'Copied!' : 'Copy'}
884
- </button>
885
- </div>
886
- </div>
887
- )}
888
-
889
760
  {/* Footer */}
890
761
  <div
891
762
  className="px-6 py-4 flex-shrink-0"
@@ -897,20 +768,80 @@ export function ConnectModal() {
897
768
  </p>
898
769
  )}
899
770
 
900
- {/* SETUP footer: Connect button */}
771
+ {/* SETUP footer: Connect + Login first buttons */}
901
772
  {connectionStatus === 'disconnected' && (
902
773
  <>
903
- <button
904
- onClick={handleConnect}
905
- className="w-full py-2 text-xs rounded font-medium transition-colors"
774
+ <div className="flex items-center gap-2">
775
+ <button
776
+ onClick={handleConnect}
777
+ className="flex-1 py-2 text-xs rounded font-medium transition-colors"
778
+ style={{
779
+ background: 'var(--accent)',
780
+ color: '#fff',
781
+ }}
782
+ >
783
+ Connect
784
+ </button>
785
+ <button
786
+ onClick={handleLoginFirst}
787
+ className="py-2 px-4 text-xs rounded font-medium transition-colors flex-shrink-0"
788
+ style={{
789
+ background: 'var(--bg-tertiary)',
790
+ color: 'var(--text-secondary)',
791
+ border: '1px solid var(--border)',
792
+ }}
793
+ title="Log in to your project first, then connect the editor"
794
+ >
795
+ Login first
796
+ </button>
797
+ </div>
798
+ {/* Auth hint */}
799
+ <p
800
+ className="text-[11px] mt-2 text-center"
801
+ style={{ color: 'var(--text-muted)' }}
802
+ >
803
+ Project requires authentication? Use{' '}
804
+ <span style={{ color: 'var(--text-secondary)' }}>Login first</span>{' '}
805
+ to sign in before the editor connects.
806
+ </p>
807
+ {/* Incognito tip banner */}
808
+ <div
809
+ className="mt-3 rounded-lg px-4 py-3 flex items-start gap-3"
906
810
  style={{
907
- background: 'var(--accent)',
908
- color: '#fff',
811
+ background: 'linear-gradient(135deg, rgba(251, 191, 36, 0.15) 0%, rgba(251, 146, 0, 0.08) 100%)',
812
+ border: '1px solid rgba(251, 191, 36, 0.4)',
909
813
  }}
910
814
  >
911
- Connect
912
- </button>
913
- <div className="mt-3 text-center">
815
+ {/* Incognito icon */}
816
+ <div
817
+ className="flex-shrink-0 mt-0.5"
818
+ style={{ color: 'var(--warning)', opacity: 0.9 }}
819
+ >
820
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
821
+ <path d="M12 12c2.5 0 4.5-1 6-3H6c1.5 2 3.5 3 6 3z" />
822
+ <path d="M3 9h18" />
823
+ <circle cx="7.5" cy="15.5" r="2.5" />
824
+ <circle cx="16.5" cy="15.5" r="2.5" />
825
+ <path d="M10 15.5h4" />
826
+ </svg>
827
+ </div>
828
+ <div className="flex flex-col gap-1">
829
+ <span
830
+ className="text-xs font-medium leading-tight"
831
+ style={{ color: 'var(--warning)' }}
832
+ >
833
+ Use Incognito for the best experience
834
+ </span>
835
+ <span
836
+ className="text-[11px] leading-relaxed"
837
+ style={{ color: 'var(--text-primary)' }}
838
+ >
839
+ Browser extensions and cached data can interfere with the
840
+ connection. If it gets stuck, try an Incognito window.
841
+ </span>
842
+ </div>
843
+ </div>
844
+ <div className="mt-2.5 text-center">
914
845
  <a
915
846
  href="/docs"
916
847
  target="_blank"
@@ -972,20 +903,6 @@ export function ConnectModal() {
972
903
  </button>
973
904
  )}
974
905
 
975
- {/* CONNECTING footer: Cancel */}
976
- {isConnecting && (
977
- <button
978
- onClick={cancelConnection}
979
- className="w-full py-2 text-xs rounded font-medium transition-colors"
980
- style={{
981
- background: 'var(--bg-tertiary)',
982
- color: 'var(--text-secondary)',
983
- border: '1px solid var(--border)',
984
- }}
985
- >
986
- Cancel
987
- </button>
988
- )}
989
906
  </div>
990
907
  </div>
991
908
  </div>
@@ -8,6 +8,7 @@ import {
8
8
  PREVIEW_WIDTH_MAX,
9
9
  PROXY_HEADER,
10
10
  } from '@/lib/constants'
11
+ import { isSwProxyReady } from '@/lib/serviceWorkerRegistration'
11
12
  import { ResponsiveToolbar } from './ResponsiveToolbar'
12
13
 
13
14
  /**
@@ -49,12 +50,38 @@ function buildDirectUrl(targetUrl: string, pagePath: string): string {
49
50
  return `${targetUrl}${path}`
50
51
  }
51
52
 
53
+ /**
54
+ * Build the SW proxy URL for the iframe. Routes through the Service Worker
55
+ * at /sw-proxy/ which intercepts all requests and proxies them to the target,
56
+ * preserving all scripts for full client-rendered content.
57
+ */
58
+ function buildSwProxyUrl(targetUrl: string, pagePath: string): string {
59
+ const path = pagePath === '/' ? '/' : pagePath
60
+ const encoded = encodeURIComponent(targetUrl)
61
+ return `/sw-proxy${path}?__sw_target=${encoded}`
62
+ }
63
+
64
+ /**
65
+ * Build a SW proxy URL for auth mode. Same as regular SW proxy but adds
66
+ * __sw_auth=1 so the SW skips inspector/nav-blocker injection, letting
67
+ * the user interact with login forms normally.
68
+ */
69
+ function buildSwProxyAuthUrl(targetUrl: string, pagePath: string): string {
70
+ const path = pagePath === '/' ? '/' : pagePath
71
+ const encoded = encodeURIComponent(targetUrl)
72
+ return `/sw-proxy${path}?__sw_target=${encoded}&__sw_auth=1`
73
+ }
74
+
52
75
  /**
53
76
  * Build the appropriate iframe URL based on whether the editor is local,
54
77
  * has a bridge connection, or is remote without bridge.
78
+ * Prefers SW proxy when available for full client-rendered content.
55
79
  */
56
80
  function buildIframeUrl(targetUrl: string, pagePath: string): string {
57
81
  if (isEditorOnLocalhost()) {
82
+ if (isSwProxyReady()) {
83
+ return buildSwProxyUrl(targetUrl, pagePath)
84
+ }
58
85
  return buildProxyUrl(targetUrl, pagePath)
59
86
  }
60
87
  // Check for bridge connection
@@ -78,6 +105,24 @@ export function PreviewFrame() {
78
105
  const lastSrcRef = useRef<string | null>(null)
79
106
  const [isDragging, setIsDragging] = useState(false)
80
107
 
108
+ // Handle auth mode — load target without inspector so user can log in
109
+ useEffect(() => {
110
+ if (!targetUrl || connectionStatus !== 'authenticating') return
111
+
112
+ const iframe = iframeRef.current
113
+ if (!iframe) return
114
+
115
+ const newSrc = isSwProxyReady()
116
+ ? buildSwProxyAuthUrl(targetUrl, currentPagePath)
117
+ : buildProxyUrl(targetUrl, currentPagePath)
118
+ console.debug('[PreviewFrame] auth mode iframe src:', newSrc)
119
+
120
+ if (lastSrcRef.current !== newSrc) {
121
+ lastSrcRef.current = newSrc
122
+ iframe.src = newSrc
123
+ }
124
+ }, [targetUrl, connectionStatus, currentPagePath, iframeRef])
125
+
81
126
  // Handle initial connection — load through proxy
82
127
  useEffect(() => {
83
128
  if (!targetUrl || connectionStatus !== 'connecting') return
@@ -86,6 +131,8 @@ export function PreviewFrame() {
86
131
  if (!iframe) return
87
132
 
88
133
  const newSrc = buildIframeUrl(targetUrl, currentPagePath)
134
+ const usingSw = newSrc.startsWith('/sw-proxy/')
135
+ console.debug('[PreviewFrame] iframe src:', newSrc, '| SW ready:', isSwProxyReady())
89
136
 
90
137
  if (lastSrcRef.current !== newSrc) {
91
138
  lastSrcRef.current = newSrc
@@ -98,8 +145,23 @@ export function PreviewFrame() {
98
145
 
99
146
  iframe.addEventListener('error', handleError)
100
147
 
148
+ // Fallback: if SW proxy doesn't connect within 8s, retry with old proxy.
149
+ // This handles stale SWs, extension interference, or other SW issues.
150
+ let fallbackTimer: ReturnType<typeof setTimeout> | null = null
151
+ if (usingSw) {
152
+ fallbackTimer = setTimeout(() => {
153
+ // Only fall back if still connecting (not yet connected)
154
+ if (useEditorStore.getState().connectionStatus !== 'connecting') return
155
+ console.debug('[PreviewFrame] SW proxy timeout — falling back to reverse proxy')
156
+ const fallbackSrc = buildProxyUrl(targetUrl, currentPagePath)
157
+ lastSrcRef.current = fallbackSrc
158
+ iframe.src = fallbackSrc
159
+ }, 8000)
160
+ }
161
+
101
162
  return () => {
102
163
  iframe.removeEventListener('error', handleError)
164
+ if (fallbackTimer) clearTimeout(fallbackTimer)
103
165
  }
104
166
  }, [
105
167
  targetUrl,
@@ -219,6 +281,13 @@ export function PreviewFrame() {
219
281
 
220
282
  const showHandles = !isFullWidth && connectionStatus === 'connected'
221
283
 
284
+ const handleFinishAuth = () => {
285
+ // Force a fresh load by clearing lastSrcRef so the connecting effect
286
+ // picks up the new (non-auth) URL even if the path hasn't changed.
287
+ lastSrcRef.current = null
288
+ setConnectionStatus('connecting')
289
+ }
290
+
222
291
  return (
223
292
  <div
224
293
  className="flex flex-col h-full"
@@ -226,6 +295,28 @@ export function PreviewFrame() {
226
295
  >
227
296
  {connectionStatus === 'connected' && <ResponsiveToolbar />}
228
297
 
298
+ {/* Auth mode banner */}
299
+ {connectionStatus === 'authenticating' && (
300
+ <div
301
+ className="flex items-center justify-between px-4 py-2 flex-shrink-0"
302
+ style={{
303
+ background: 'var(--bg-secondary)',
304
+ borderBottom: '1px solid var(--border)',
305
+ }}
306
+ >
307
+ <span className="text-xs" style={{ color: 'var(--text-secondary)' }}>
308
+ Log in to your project below, then click <strong>Connect Editor</strong> when ready.
309
+ </span>
310
+ <button
311
+ onClick={handleFinishAuth}
312
+ className="px-3 py-1 text-xs rounded font-medium ml-3 flex-shrink-0"
313
+ style={{ background: 'var(--accent)', color: '#fff' }}
314
+ >
315
+ Connect Editor
316
+ </button>
317
+ </div>
318
+ )}
319
+
229
320
  <div
230
321
  ref={containerRef}
231
322
  className="flex items-start justify-center flex-1 overflow-auto relative"
@@ -27,6 +27,7 @@ export function TargetSelector() {
27
27
  const [error, setError] = useState<string | null>(null)
28
28
 
29
29
  const isConnected = connectionStatus === 'connected'
30
+ const isAuthenticating = connectionStatus === 'authenticating'
30
31
 
31
32
  const handleConnect = () => {
32
33
  setError(null)
@@ -41,6 +42,19 @@ export function TargetSelector() {
41
42
  addRecentUrl(normalized)
42
43
  }
43
44
 
45
+ const handleLoginFirst = () => {
46
+ setError(null)
47
+ const raw = urlMode ? customUrl.trim() : `http://localhost:${selectedPort}`
48
+ if (urlMode && !raw) {
49
+ setError('Enter a URL')
50
+ return
51
+ }
52
+ const normalized = normalizeTargetUrl(raw)
53
+ setTargetUrl(normalized)
54
+ setConnectionStatus('authenticating')
55
+ addRecentUrl(normalized)
56
+ }
57
+
44
58
  const handleDisconnect = () => {
45
59
  // Clear persisted changes for this URL so reconnect loads fresh content
46
60
  if (targetUrl) {
@@ -70,11 +84,13 @@ export function TargetSelector() {
70
84
  const statusColor =
71
85
  connectionStatus === 'connected'
72
86
  ? 'var(--success)'
73
- : connectionStatus === 'connecting' ||
74
- connectionStatus === 'confirming' ||
75
- connectionStatus === 'scanning'
76
- ? 'var(--warning)'
77
- : 'var(--error)'
87
+ : connectionStatus === 'authenticating'
88
+ ? 'var(--accent)'
89
+ : connectionStatus === 'connecting' ||
90
+ connectionStatus === 'confirming' ||
91
+ connectionStatus === 'scanning'
92
+ ? 'var(--warning)'
93
+ : 'var(--error)'
78
94
 
79
95
  return (
80
96
  <div className="flex items-center gap-2 relative">
@@ -88,7 +104,7 @@ export function TargetSelector() {
88
104
  {/* Toggle between dropdown and URL input */}
89
105
  <button
90
106
  onClick={() => {
91
- if (!isConnected) {
107
+ if (!isConnected && !isAuthenticating) {
92
108
  setUrlMode(!urlMode)
93
109
  setError(null)
94
110
  }
@@ -96,8 +112,8 @@ export function TargetSelector() {
96
112
  className="p-1 rounded hover:bg-[var(--bg-hover)] transition-colors flex-shrink-0"
97
113
  style={{
98
114
  color: urlMode ? 'var(--accent)' : 'var(--text-muted)',
99
- cursor: isConnected ? 'default' : 'pointer',
100
- opacity: isConnected ? 0.5 : 1,
115
+ cursor: isConnected || isAuthenticating ? 'default' : 'pointer',
116
+ opacity: isConnected || isAuthenticating ? 0.5 : 1,
101
117
  }}
102
118
  title={urlMode ? 'Switch to port selector' : 'Switch to URL input'}
103
119
  >
@@ -133,7 +149,7 @@ export function TargetSelector() {
133
149
  )}
134
150
  </button>
135
151
 
136
- {isConnected ? (
152
+ {isConnected || isAuthenticating ? (
137
153
  <div
138
154
  className="w-56 text-sm rounded px-2 py-1 truncate"
139
155
  style={{
@@ -172,20 +188,38 @@ export function TargetSelector() {
172
188
  </select>
173
189
  )}
174
190
 
191
+ {/* Login first button — only shown when disconnected */}
192
+ {!isConnected && !isAuthenticating && connectionStatus !== 'connecting' && (
193
+ <button
194
+ onClick={handleLoginFirst}
195
+ className="px-2 py-1 text-xs rounded transition-colors font-medium"
196
+ style={{
197
+ background: 'var(--bg-hover)',
198
+ color: 'var(--text-secondary)',
199
+ border: '1px solid var(--border)',
200
+ }}
201
+ title="Log in to your project first, then connect the editor"
202
+ >
203
+ Login first
204
+ </button>
205
+ )}
206
+
175
207
  {/* Connect / Disconnect button */}
176
208
  <button
177
- onClick={isConnected ? handleDisconnect : handleConnect}
209
+ onClick={isConnected || isAuthenticating ? handleDisconnect : handleConnect}
178
210
  className="px-3 py-1 text-xs rounded transition-colors font-medium"
179
211
  style={{
180
- background: isConnected ? 'var(--bg-hover)' : 'var(--accent)',
181
- color: isConnected ? 'var(--text-secondary)' : '#fff',
212
+ background: isConnected || isAuthenticating ? 'var(--bg-hover)' : 'var(--accent)',
213
+ color: isConnected || isAuthenticating ? 'var(--text-secondary)' : '#fff',
182
214
  }}
183
215
  >
184
216
  {isConnected
185
217
  ? 'Disconnect'
186
- : connectionStatus === 'connecting'
187
- ? 'Connecting...'
188
- : 'Connect'}
218
+ : isAuthenticating
219
+ ? 'Cancel'
220
+ : connectionStatus === 'connecting'
221
+ ? 'Connecting...'
222
+ : 'Connect'}
189
223
  </button>
190
224
 
191
225
  {/* Bridge status indicator (shown when running on Vercel) */}
@@ -455,10 +455,13 @@ export function LayerNode({
455
455
  const IconComponent = ICON_MAP[category]
456
456
  const label = getDisplayLabel(node)
457
457
 
458
- // Scroll selected layer into view (expansion is handled by usePostMessage)
458
+ // Scroll selected layer into view after tree expansion renders it.
459
+ // Use requestAnimationFrame to wait for layout after mount/expansion.
459
460
  useEffect(() => {
460
461
  if (isSelected && rowRef.current) {
461
- rowRef.current.scrollIntoView({ block: 'center', behavior: 'instant' })
462
+ requestAnimationFrame(() => {
463
+ rowRef.current?.scrollIntoView({ block: 'center', behavior: 'instant' })
464
+ })
462
465
  }
463
466
  }, [isSelected])
464
467
 
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useMemo } from 'react'
3
+ import { useEffect, useMemo } from 'react'
4
4
  import { useEditorStore } from '@/store'
5
5
  import { LayerNode } from './LayerNode'
6
6
  import { LayerSearch } from './LayerSearch'
@@ -9,6 +9,15 @@ export function LayersPanel() {
9
9
  const rootNode = useEditorStore((s) => s.rootNode)
10
10
  const searchQuery = useEditorStore((s) => s.searchQuery)
11
11
  const styleChanges = useEditorStore((s) => s.styleChanges)
12
+ const selectorPath = useEditorStore((s) => s.selectorPath)
13
+
14
+ // Auto-expand tree to the selected element whenever selection changes.
15
+ // This also re-expands after DOM_UPDATED replaces the tree.
16
+ useEffect(() => {
17
+ if (selectorPath) {
18
+ useEditorStore.getState().expandToNode(selectorPath)
19
+ }
20
+ }, [selectorPath, rootNode])
12
21
 
13
22
  const { changedSelectors, deletedSelectors } = useMemo(() => {
14
23
  const changed = new Set<string>()