@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.
@@ -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>()
@@ -1198,7 +1198,12 @@ export function ChangesPanel() {
1198
1198
  ])
1199
1199
 
1200
1200
  const handleAiScan = useCallback(() => {
1201
- if (!targetUrl || !projectRoot || breakpointChanges.length === 0) return
1201
+ if (!targetUrl || breakpointChanges.length === 0) return
1202
+ if (!projectRoot) {
1203
+ showToast('error', 'Set a project folder first — click the folder icon in the Claude tab')
1204
+ setActiveRightTab('claude')
1205
+ return
1206
+ }
1202
1207
 
1203
1208
  setAiScanStatus('scanning')
1204
1209
  setAiScanError(null)
@@ -1251,6 +1256,7 @@ export function ChangesPanel() {
1251
1256
  setAiScanResult,
1252
1257
  showToast,
1253
1258
  setActiveLeftTab,
1259
+ setActiveRightTab,
1254
1260
  ])
1255
1261
 
1256
1262
  const handleSendToClaudeCode = useCallback(
@@ -261,6 +261,32 @@ export function performRevertAll() {
261
261
  }
262
262
  }
263
263
 
264
+ // Module-level singletons for auto-persist and load — prevents duplicate
265
+ // subscriptions and stale-data overwrites when multiple components call
266
+ // useChangeTracker() and some of them remount (e.g. LayerNode after tree updates).
267
+ let persistSubscribed = false
268
+ let persistTimeout: ReturnType<typeof setTimeout> | null = null
269
+ let prevChangesRef: unknown = null
270
+ let lastLoadedUrl: string | null = null
271
+
272
+ function ensurePersistSubscription() {
273
+ if (persistSubscribed) return
274
+ persistSubscribed = true
275
+ useEditorStore.subscribe((state) => {
276
+ const ref = state.styleChanges
277
+ if (ref === prevChangesRef) return
278
+ prevChangesRef = ref
279
+
280
+ const url = state.targetUrl
281
+ if (!url) return
282
+
283
+ if (persistTimeout) clearTimeout(persistTimeout)
284
+ persistTimeout = setTimeout(() => {
285
+ useEditorStore.getState().persistChanges(url)
286
+ }, 300)
287
+ })
288
+ }
289
+
264
290
  /**
265
291
  * Hook that tracks style changes, sends PREVIEW_CHANGE to inspector,
266
292
  * and auto-persists changes to localStorage.
@@ -273,36 +299,18 @@ export function useChangeTracker() {
273
299
  const pushUndo = useEditorStore((s) => s.pushUndo)
274
300
  const { sendToInspector } = usePostMessage()
275
301
 
276
- // Auto-persist changes when they update (count OR content)
277
- const persistTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
278
- const prevChangesRef = useRef<unknown>(null)
279
-
302
+ // Set up singleton auto-persist subscription (once globally, not per component)
280
303
  useEffect(() => {
281
- const unsubscribe = useEditorStore.subscribe((state) => {
282
- // Trigger on any styleChanges or elementSnapshots reference change
283
- const ref = state.styleChanges
284
- if (ref === prevChangesRef.current) return
285
- prevChangesRef.current = ref
286
-
287
- const url = state.targetUrl
288
- if (!url) return
289
-
290
- // Debounce persistence
291
- if (persistTimeoutRef.current) clearTimeout(persistTimeoutRef.current)
292
- persistTimeoutRef.current = setTimeout(() => {
293
- useEditorStore.getState().persistChanges(url)
294
- }, 300)
295
- })
296
-
297
- return () => {
298
- unsubscribe()
299
- if (persistTimeoutRef.current) clearTimeout(persistTimeoutRef.current)
300
- }
304
+ ensurePersistSubscription()
301
305
  }, [])
302
306
 
303
- // Load persisted changes when target URL changes
307
+ // Load persisted changes only when target URL actually changes,
308
+ // not on every component mount. This prevents remounting components
309
+ // (e.g. LayerNode after tree updates) from overwriting in-memory
310
+ // changes with stale localStorage data.
304
311
  useEffect(() => {
305
- if (targetUrl) {
312
+ if (targetUrl && targetUrl !== lastLoadedUrl) {
313
+ lastLoadedUrl = targetUrl
306
314
  useEditorStore.getState().loadPersistedChanges(targetUrl)
307
315
  }
308
316
  }, [targetUrl])
@@ -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':
@@ -450,6 +456,12 @@ function handleMessage(event: MessageEvent) {
450
456
  newParentSelectorPath: mvNewParent,
451
457
  oldIndex: mvOldIndex,
452
458
  newIndex: mvNewIndex,
459
+ tagName: mvTag,
460
+ className: mvClass,
461
+ elementId: mvId,
462
+ innerText: mvText,
463
+ attributes: mvAttrs,
464
+ computedStyles: mvStyles,
453
465
  } = msg.payload
454
466
  const mvProperty = '__element_moved__'
455
467
 
@@ -464,6 +476,20 @@ function handleMessage(event: MessageEvent) {
464
476
  changeScope: store.changeScope,
465
477
  })
466
478
 
479
+ // Save element snapshot so move appears in Changes panel
480
+ store.saveElementSnapshot({
481
+ selectorPath: mvNewSelector,
482
+ tagName: mvTag || 'unknown',
483
+ className: mvClass ?? null,
484
+ elementId: mvId ?? null,
485
+ attributes: mvAttrs || {},
486
+ innerText: mvText,
487
+ computedStyles: mvStyles ? { ...mvStyles } : {},
488
+ pagePath: store.currentPagePath,
489
+ changeScope: store.changeScope,
490
+ sourceInfo: null,
491
+ })
492
+
467
493
  // Track the move
468
494
  store.addStyleChange({
469
495
  id: generateId(),
@@ -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)
@@ -337,6 +337,12 @@ export interface ElementMovedMessage {
337
337
  newParentSelectorPath: string
338
338
  oldIndex: number
339
339
  newIndex: number
340
+ tagName: string
341
+ className: string | null
342
+ elementId: string | null
343
+ innerText: string | null
344
+ attributes: Record<string, string>
345
+ computedStyles: Record<string, string>
340
346
  }
341
347
  }
342
348