@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.
@@ -280,7 +280,13 @@ function handleMessage(event: MessageEvent) {
280
280
  break
281
281
 
282
282
  case 'PAGE_LINKS':
283
- store.setPageLinks(msg.payload.links)
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
+ }
@@ -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
- const { expandedNodeIds: prev } = get()
37
- // On first load (no previous state), start collapsed.
38
- // On subsequent updates (DOM_UPDATED), preserve user-expanded state.
39
- if (prev.size === 0) {
40
- set({ rootNode: node, expandedNodeIds: new Set<string>() })
41
- } else {
42
- set({ rootNode: node })
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 next = new Set(expandedNodeIds)
51
- if (next.has(nodeId)) {
52
- next.delete(nodeId)
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
- next.add(nodeId)
62
+ nextExpanded.add(nodeId)
63
+ nextCollapsed.delete(nodeId)
55
64
  }
56
- set({ expandedNodeIds: next })
65
+ set({ expandedNodeIds: nextExpanded, collapsedNodeIds: nextCollapsed })
57
66
  },
58
67
 
59
68
  expandToNode: (nodeId) => {
60
- // Merge ancestors into existing expanded state (preserve user-toggled branches)
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
- next.add(parts.slice(0, i).join(' > '))
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
  })
@@ -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)