@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.
- package/LICENSE +1 -1
- package/NOTICE +2 -2
- package/bin/paint.js +32 -0
- package/next.config.mjs +8 -0
- package/package.json +1 -1
- package/public/dev-editor-inspector.js +14 -0
- package/public/sw-proxy/sw.js +886 -0
- 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 +112 -405
- 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/hooks/usePostMessage.ts +7 -1
- package/src/lib/serviceWorkerRegistration.ts +163 -0
- package/src/store/treeSlice.ts +29 -17
- package/src/store/uiSlice.ts +6 -0
|
@@ -280,7 +280,13 @@ function handleMessage(event: MessageEvent) {
|
|
|
280
280
|
break
|
|
281
281
|
|
|
282
282
|
case 'PAGE_LINKS':
|
|
283
|
-
|
|
283
|
+
// Strip /sw-proxy/ or /api/proxy prefix from page link paths
|
|
284
|
+
store.setPageLinks(
|
|
285
|
+
msg.payload.links.map((link: { href: string; text: string }) => ({
|
|
286
|
+
...link,
|
|
287
|
+
href: link.href.replace(/^\/(sw-proxy|api\/proxy)/, '') || '/',
|
|
288
|
+
})),
|
|
289
|
+
)
|
|
284
290
|
break
|
|
285
291
|
|
|
286
292
|
case 'COMPONENTS_DETECTED':
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useEditorStore } from '@/store'
|
|
2
|
+
|
|
3
|
+
let swReady = false
|
|
4
|
+
|
|
5
|
+
// Must match SW_VERSION in /public/sw-proxy/sw.js
|
|
6
|
+
const EXPECTED_SW_VERSION = 10
|
|
7
|
+
|
|
8
|
+
const log = (...args: unknown[]) =>
|
|
9
|
+
console.debug('[sw-proxy]', ...args)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check the active SW's version via message. Returns the version number,
|
|
13
|
+
* or 0 if the SW doesn't respond (old version without VERSION_CHECK).
|
|
14
|
+
*/
|
|
15
|
+
function checkSwVersion(sw: ServiceWorker): Promise<number> {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const timeout = setTimeout(() => resolve(0), 1000)
|
|
18
|
+
const channel = new MessageChannel()
|
|
19
|
+
channel.port1.onmessage = (event) => {
|
|
20
|
+
clearTimeout(timeout)
|
|
21
|
+
resolve(event.data?.version ?? 0)
|
|
22
|
+
}
|
|
23
|
+
sw.postMessage({ type: 'VERSION_CHECK' }, [channel.port2])
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register the SW proxy at /sw-proxy/sw.js with scope /sw-proxy/.
|
|
29
|
+
* Sets swProxyReady in the Zustand store when activation completes.
|
|
30
|
+
* Returns true if the SW is active, false otherwise.
|
|
31
|
+
*
|
|
32
|
+
* Handles upgrades from old SW versions by checking the version number.
|
|
33
|
+
* If the active SW is outdated, it's unregistered and a fresh install
|
|
34
|
+
* is performed — this ensures existing users get the latest SW code.
|
|
35
|
+
*/
|
|
36
|
+
export async function registerSwProxy(): Promise<boolean> {
|
|
37
|
+
if (!('serviceWorker' in navigator)) {
|
|
38
|
+
log('Service Workers not supported — using fallback proxy')
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
log('Registering...')
|
|
44
|
+
let registration = await navigator.serviceWorker.register(
|
|
45
|
+
'/sw-proxy/sw.js',
|
|
46
|
+
{ scope: '/sw-proxy/', updateViaCache: 'none' },
|
|
47
|
+
)
|
|
48
|
+
log('Registration successful, scope:', registration.scope)
|
|
49
|
+
|
|
50
|
+
// If there's an active SW, check its version
|
|
51
|
+
if (registration.active) {
|
|
52
|
+
const version = await checkSwVersion(registration.active)
|
|
53
|
+
log('Active SW version:', version, '(expected:', EXPECTED_SW_VERSION + ')')
|
|
54
|
+
|
|
55
|
+
if (version < EXPECTED_SW_VERSION) {
|
|
56
|
+
// Old SW — force unregister and re-register clean
|
|
57
|
+
log('Stale SW detected — forcing clean re-registration')
|
|
58
|
+
await registration.unregister()
|
|
59
|
+
registration = await navigator.serviceWorker.register(
|
|
60
|
+
'/sw-proxy/sw.js',
|
|
61
|
+
{ scope: '/sw-proxy/', updateViaCache: 'none' },
|
|
62
|
+
)
|
|
63
|
+
// Fall through to wait for fresh install below
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Helper to mark SW as ready
|
|
68
|
+
const markReady = () => {
|
|
69
|
+
if (!swReady) {
|
|
70
|
+
swReady = true
|
|
71
|
+
useEditorStore.getState().setSwProxyReady(true)
|
|
72
|
+
log('Ready')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If there's a waiting SW, tell it to activate immediately
|
|
77
|
+
if (registration.waiting) {
|
|
78
|
+
log('Found waiting SW — sending SKIP_WAITING')
|
|
79
|
+
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If an update is installing, wait for it before marking ready
|
|
83
|
+
const installing = registration.installing
|
|
84
|
+
if (installing) {
|
|
85
|
+
log('SW installing, waiting for activation...')
|
|
86
|
+
return new Promise<boolean>((resolve) => {
|
|
87
|
+
installing.addEventListener('statechange', () => {
|
|
88
|
+
log('Installing SW state:', installing.state)
|
|
89
|
+
if (installing.state === 'installed' && registration.waiting) {
|
|
90
|
+
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
|
91
|
+
}
|
|
92
|
+
if (installing.state === 'activated') {
|
|
93
|
+
markReady()
|
|
94
|
+
resolve(true)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Listen for future updates during this page session
|
|
101
|
+
registration.addEventListener('updatefound', () => {
|
|
102
|
+
const newSw = registration.installing
|
|
103
|
+
if (!newSw) return
|
|
104
|
+
log('Update found, tracking new SW...')
|
|
105
|
+
newSw.addEventListener('statechange', () => {
|
|
106
|
+
if (newSw.state === 'installed' && registration.waiting) {
|
|
107
|
+
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
|
108
|
+
}
|
|
109
|
+
if (newSw.state === 'activated') {
|
|
110
|
+
markReady()
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// No update in progress — if already active, we're ready
|
|
116
|
+
if (registration.active) {
|
|
117
|
+
markReady()
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// First-time install — wait for activation
|
|
122
|
+
const sw = registration.waiting
|
|
123
|
+
if (!sw) {
|
|
124
|
+
log('No SW instance found')
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
log('Waiting for activation, state:', sw.state)
|
|
129
|
+
return new Promise<boolean>((resolve) => {
|
|
130
|
+
sw.addEventListener('statechange', () => {
|
|
131
|
+
log('State changed to:', sw.state)
|
|
132
|
+
if (sw.state === 'activated') {
|
|
133
|
+
markReady()
|
|
134
|
+
resolve(true)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.warn('[sw-proxy] Registration failed:', err)
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Synchronous readiness check. */
|
|
145
|
+
export function isSwProxyReady(): boolean {
|
|
146
|
+
return swReady
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Unregister the SW proxy and reset state. */
|
|
150
|
+
export async function unregisterSwProxy(): Promise<void> {
|
|
151
|
+
swReady = false
|
|
152
|
+
useEditorStore.getState().setSwProxyReady(false)
|
|
153
|
+
log('Unregistered')
|
|
154
|
+
|
|
155
|
+
if (!('serviceWorker' in navigator)) return
|
|
156
|
+
|
|
157
|
+
const registrations = await navigator.serviceWorker.getRegistrations()
|
|
158
|
+
for (const reg of registrations) {
|
|
159
|
+
if (reg.scope.includes('/sw-proxy/')) {
|
|
160
|
+
await reg.unregister()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/store/treeSlice.ts
CHANGED
|
@@ -13,6 +13,7 @@ function _collectAllIds(node: TreeNode, ids: Set<string>) {
|
|
|
13
13
|
export interface TreeSlice {
|
|
14
14
|
rootNode: TreeNode | null
|
|
15
15
|
expandedNodeIds: Set<string>
|
|
16
|
+
collapsedNodeIds: Set<string>
|
|
16
17
|
searchQuery: string
|
|
17
18
|
highlightedNodeId: string | null
|
|
18
19
|
|
|
@@ -29,41 +30,52 @@ export const createTreeSlice: StateCreator<TreeSlice, [], [], TreeSlice> = (
|
|
|
29
30
|
) => ({
|
|
30
31
|
rootNode: null,
|
|
31
32
|
expandedNodeIds: new Set<string>(),
|
|
33
|
+
collapsedNodeIds: new Set<string>(),
|
|
32
34
|
searchQuery: '',
|
|
33
35
|
highlightedNodeId: null,
|
|
34
36
|
|
|
35
37
|
setRootNode: (node) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
if (!node) {
|
|
39
|
+
set({ rootNode: null, expandedNodeIds: new Set<string>(), collapsedNodeIds: new Set<string>() })
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
// Expand all nodes by default, but respect user-collapsed nodes.
|
|
43
|
+
const all = new Set<string>()
|
|
44
|
+
_collectAllIds(node, all)
|
|
45
|
+
const { collapsedNodeIds } = get()
|
|
46
|
+
for (const id of collapsedNodeIds) {
|
|
47
|
+
all.delete(id)
|
|
43
48
|
}
|
|
49
|
+
set({ rootNode: node, expandedNodeIds: all })
|
|
44
50
|
},
|
|
45
51
|
setSearchQuery: (query) => set({ searchQuery: query }),
|
|
46
52
|
setHighlightedNodeId: (id) => set({ highlightedNodeId: id }),
|
|
47
53
|
|
|
48
54
|
toggleNodeExpanded: (nodeId) => {
|
|
49
|
-
const { expandedNodeIds } = get()
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
const { expandedNodeIds, collapsedNodeIds } = get()
|
|
56
|
+
const nextExpanded = new Set(expandedNodeIds)
|
|
57
|
+
const nextCollapsed = new Set(collapsedNodeIds)
|
|
58
|
+
if (nextExpanded.has(nodeId)) {
|
|
59
|
+
nextExpanded.delete(nodeId)
|
|
60
|
+
nextCollapsed.add(nodeId)
|
|
53
61
|
} else {
|
|
54
|
-
|
|
62
|
+
nextExpanded.add(nodeId)
|
|
63
|
+
nextCollapsed.delete(nodeId)
|
|
55
64
|
}
|
|
56
|
-
set({ expandedNodeIds:
|
|
65
|
+
set({ expandedNodeIds: nextExpanded, collapsedNodeIds: nextCollapsed })
|
|
57
66
|
},
|
|
58
67
|
|
|
59
68
|
expandToNode: (nodeId) => {
|
|
60
|
-
// Merge ancestors into existing expanded state
|
|
61
|
-
const { expandedNodeIds: prev } = get()
|
|
69
|
+
// Merge ancestors into existing expanded state and clear them from collapsed
|
|
70
|
+
const { expandedNodeIds: prev, collapsedNodeIds: prevCollapsed } = get()
|
|
62
71
|
const next = new Set(prev)
|
|
72
|
+
const nextCollapsed = new Set(prevCollapsed)
|
|
63
73
|
const parts = nodeId.split(' > ')
|
|
64
74
|
for (let i = 1; i <= parts.length; i++) {
|
|
65
|
-
|
|
75
|
+
const ancestor = parts.slice(0, i).join(' > ')
|
|
76
|
+
next.add(ancestor)
|
|
77
|
+
nextCollapsed.delete(ancestor)
|
|
66
78
|
}
|
|
67
|
-
set({ expandedNodeIds: next })
|
|
79
|
+
set({ expandedNodeIds: next, collapsedNodeIds: nextCollapsed })
|
|
68
80
|
},
|
|
69
81
|
})
|
package/src/store/uiSlice.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type ConnectionStatus =
|
|
|
18
18
|
| 'disconnected'
|
|
19
19
|
| 'confirming'
|
|
20
20
|
| 'scanning'
|
|
21
|
+
| 'authenticating'
|
|
21
22
|
| 'connecting'
|
|
22
23
|
| 'connected'
|
|
23
24
|
export type BridgeStatus =
|
|
@@ -48,6 +49,7 @@ export interface UISlice {
|
|
|
48
49
|
toasts: Toast[]
|
|
49
50
|
bridgeUrl: string | null
|
|
50
51
|
bridgeStatus: BridgeStatus
|
|
52
|
+
swProxyReady: boolean
|
|
51
53
|
|
|
52
54
|
setTargetUrl: (url: string | null) => void
|
|
53
55
|
setConnectionStatus: (status: ConnectionStatus) => void
|
|
@@ -77,6 +79,7 @@ export interface UISlice {
|
|
|
77
79
|
dismissToast: (id: string) => void
|
|
78
80
|
setBridgeUrl: (url: string | null) => void
|
|
79
81
|
setBridgeStatus: (status: BridgeStatus) => void
|
|
82
|
+
setSwProxyReady: (ready: boolean) => void
|
|
80
83
|
loadPersistedUI: () => void
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -105,6 +108,7 @@ export const createUISlice: StateCreator<UISlice, [], [], UISlice> = (
|
|
|
105
108
|
toasts: [],
|
|
106
109
|
bridgeUrl: null,
|
|
107
110
|
bridgeStatus: 'disconnected',
|
|
111
|
+
swProxyReady: false,
|
|
108
112
|
|
|
109
113
|
setTargetUrl: (url) => {
|
|
110
114
|
set({ targetUrl: url })
|
|
@@ -300,6 +304,8 @@ export const createUISlice: StateCreator<UISlice, [], [], UISlice> = (
|
|
|
300
304
|
|
|
301
305
|
setBridgeStatus: (status) => set({ bridgeStatus: status }),
|
|
302
306
|
|
|
307
|
+
setSwProxyReady: (ready) => set({ swProxyReady: ready }),
|
|
308
|
+
|
|
303
309
|
loadPersistedUI: () => {
|
|
304
310
|
try {
|
|
305
311
|
const urls = localStorage.getItem(LOCAL_STORAGE_KEYS.RECENT_URLS)
|