@antigenic-oss/paint 0.2.8 → 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.
- package/LICENSE +1 -1
- package/NOTICE +2 -2
- package/README.md +42 -15
- package/bin/paint.js +32 -0
- package/next.config.mjs +8 -0
- package/package.json +10 -8
- package/public/dev-editor-inspector.js +14 -0
- package/public/sw-proxy/sw.js +886 -0
- package/src/app/api/proxy/[[...path]]/route.ts +12 -1
- package/src/app/api/sw-fetch/[[...path]]/route.ts +149 -0
- package/src/app/docs/DocsClient.tsx +1 -1
- package/src/app/docs/page.tsx +134 -407
- package/src/app/layout.tsx +48 -2
- package/src/app/page.tsx +2 -0
- package/src/components/ConnectModal.tsx +98 -181
- package/src/components/PreviewFrame.tsx +91 -0
- package/src/components/TargetSelector.tsx +49 -15
- package/src/components/left-panel/LayerNode.tsx +5 -2
- package/src/components/left-panel/LayersPanel.tsx +10 -1
- package/src/components/right-panel/changes/ChangesPanel.tsx +7 -1
- package/src/hooks/useChangeTracker.ts +34 -26
- package/src/hooks/usePostMessage.ts +27 -1
- package/src/lib/serviceWorkerRegistration.ts +163 -0
- package/src/store/treeSlice.ts +29 -17
- package/src/store/uiSlice.ts +6 -0
- package/src/types/messages.ts +6 -0
package/src/app/layout.tsx
CHANGED
|
@@ -2,8 +2,54 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import './globals.css'
|
|
3
3
|
|
|
4
4
|
export const metadata: Metadata = {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
metadataBase: new URL('https://dev-editor-flow.vercel.app'),
|
|
6
|
+
title: {
|
|
7
|
+
default: 'pAInt | Visual Editor for Localhost Projects',
|
|
8
|
+
template: '%s | pAInt',
|
|
9
|
+
},
|
|
10
|
+
description:
|
|
11
|
+
'pAInt is a visual editor for localhost apps with bridge, server, and terminal workflows. Edit first, export precise changelogs, and save AI tokens with focused Claude Code handoff.',
|
|
12
|
+
keywords: [
|
|
13
|
+
'pAInt',
|
|
14
|
+
'visual editor',
|
|
15
|
+
'localhost web editor',
|
|
16
|
+
'bridge server',
|
|
17
|
+
'terminal server',
|
|
18
|
+
'Claude Code',
|
|
19
|
+
'AI token efficiency',
|
|
20
|
+
'CSS visual editing',
|
|
21
|
+
'Next.js visual editor',
|
|
22
|
+
'developer tooling',
|
|
23
|
+
],
|
|
24
|
+
alternates: {
|
|
25
|
+
canonical: '/',
|
|
26
|
+
},
|
|
27
|
+
openGraph: {
|
|
28
|
+
type: 'website',
|
|
29
|
+
url: '/',
|
|
30
|
+
siteName: 'pAInt',
|
|
31
|
+
title: 'pAInt | Visual Editor for Localhost Projects',
|
|
32
|
+
description:
|
|
33
|
+
'Inspect, edit, and export structured UI changes. Use bridge/server/terminal modes to ship faster and reduce AI token usage.',
|
|
34
|
+
},
|
|
35
|
+
twitter: {
|
|
36
|
+
card: 'summary_large_image',
|
|
37
|
+
title: 'pAInt | Visual Editor for Localhost Projects',
|
|
38
|
+
description:
|
|
39
|
+
'Visual-first editing plus focused changelogs for Claude Code means fewer tokens and faster delivery.',
|
|
40
|
+
},
|
|
41
|
+
robots: {
|
|
42
|
+
index: true,
|
|
43
|
+
follow: true,
|
|
44
|
+
googleBot: {
|
|
45
|
+
index: true,
|
|
46
|
+
follow: true,
|
|
47
|
+
'max-image-preview': 'large',
|
|
48
|
+
'max-snippet': -1,
|
|
49
|
+
'max-video-preview': -1,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
category: 'developer tools',
|
|
7
53
|
}
|
|
8
54
|
|
|
9
55
|
export default function RootLayout({
|
package/src/app/page.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
4
|
import { Editor } from '@/components/Editor'
|
|
5
5
|
import { useEditorStore } from '@/store'
|
|
6
|
+
import { registerSwProxy } from '@/lib/serviceWorkerRegistration'
|
|
6
7
|
|
|
7
8
|
export default function Home() {
|
|
8
9
|
const loadPersistedUI = useEditorStore((s) => s.loadPersistedUI)
|
|
@@ -26,6 +27,7 @@ export default function Home() {
|
|
|
26
27
|
|
|
27
28
|
loadPersistedUI()
|
|
28
29
|
loadPersistedClaude()
|
|
30
|
+
registerSwProxy()
|
|
29
31
|
|
|
30
32
|
// Suppress HMR errors caused by proxied routes leaking into the
|
|
31
33
|
// editor's route tree (e.g. "unrecognized HMR message").
|
|
@@ -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
|
-
:
|
|
246
|
-
? '
|
|
247
|
-
:
|
|
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
|
-
{/*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
598
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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'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
|
|
771
|
+
{/* SETUP footer: Connect + Login first buttons */}
|
|
901
772
|
{connectionStatus === 'disconnected' && (
|
|
902
773
|
<>
|
|
903
|
-
<
|
|
904
|
-
|
|
905
|
-
|
|
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: '
|
|
908
|
-
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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"
|