@antigenic-oss/paint 0.1.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.
Files changed (158) hide show
  1. package/LICENSE +178 -0
  2. package/NOTICE +4 -0
  3. package/README.md +180 -0
  4. package/bin/paint.js +266 -0
  5. package/next-env.d.ts +6 -0
  6. package/next.config.ts +19 -0
  7. package/package.json +81 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/dev-editor-inspector.js +1872 -0
  10. package/src/app/api/claude/analyze/route.ts +319 -0
  11. package/src/app/api/claude/apply/route.ts +185 -0
  12. package/src/app/api/claude/pick-folder/route.ts +64 -0
  13. package/src/app/api/claude/scan/route.ts +221 -0
  14. package/src/app/api/claude/status/route.ts +55 -0
  15. package/src/app/api/project/scan/route.ts +634 -0
  16. package/src/app/api/project-scan/css-variables/route.ts +238 -0
  17. package/src/app/api/project-scan/route.ts +40 -0
  18. package/src/app/api/project-scan/tailwind-config/route.ts +172 -0
  19. package/src/app/api/proxy/[[...path]]/route.ts +2400 -0
  20. package/src/app/docs/DocsClient.tsx +322 -0
  21. package/src/app/docs/layout.tsx +7 -0
  22. package/src/app/docs/page.tsx +855 -0
  23. package/src/app/globals.css +176 -0
  24. package/src/app/layout.tsx +19 -0
  25. package/src/app/page.tsx +46 -0
  26. package/src/bridge/api-handlers.ts +885 -0
  27. package/src/bridge/proxy-handler.ts +329 -0
  28. package/src/bridge/server.ts +113 -0
  29. package/src/components/BreakpointTabs.tsx +72 -0
  30. package/src/components/ChangeSummaryModal.tsx +267 -0
  31. package/src/components/ConnectModal.tsx +994 -0
  32. package/src/components/Editor.tsx +90 -0
  33. package/src/components/PageSelector.tsx +208 -0
  34. package/src/components/PreviewFrame.tsx +299 -0
  35. package/src/components/ProjectFolderBanner.tsx +91 -0
  36. package/src/components/ResponsiveToolbar.tsx +222 -0
  37. package/src/components/TargetSelector.tsx +243 -0
  38. package/src/components/TopBar.tsx +315 -0
  39. package/src/components/common/CollapsibleSection.tsx +36 -0
  40. package/src/components/common/ColorPicker.tsx +920 -0
  41. package/src/components/common/EditablePre.tsx +136 -0
  42. package/src/components/common/ErrorBoundary.tsx +65 -0
  43. package/src/components/common/ResizablePanel.tsx +83 -0
  44. package/src/components/common/ScanAnimation.tsx +76 -0
  45. package/src/components/common/ToastContainer.tsx +97 -0
  46. package/src/components/common/UnitInput.tsx +77 -0
  47. package/src/components/common/VariableColorPicker.tsx +622 -0
  48. package/src/components/left-panel/AddElementPanel.tsx +237 -0
  49. package/src/components/left-panel/ComponentsPanel.tsx +609 -0
  50. package/src/components/left-panel/IconSidebar.tsx +99 -0
  51. package/src/components/left-panel/LayerNode.tsx +874 -0
  52. package/src/components/left-panel/LayerSearch.tsx +23 -0
  53. package/src/components/left-panel/LayersPanel.tsx +52 -0
  54. package/src/components/left-panel/LeftPanel.tsx +122 -0
  55. package/src/components/left-panel/PagesPanel.tsx +114 -0
  56. package/src/components/left-panel/icons.tsx +162 -0
  57. package/src/components/left-panel/terminal/ScanOverlay.tsx +66 -0
  58. package/src/components/left-panel/terminal/TerminalPanel.tsx +217 -0
  59. package/src/components/right-panel/ElementLogBox.tsx +248 -0
  60. package/src/components/right-panel/PanelTabs.tsx +83 -0
  61. package/src/components/right-panel/RightPanel.tsx +41 -0
  62. package/src/components/right-panel/changes/AiScanResultPanel.tsx +285 -0
  63. package/src/components/right-panel/changes/ChangeEntry.tsx +59 -0
  64. package/src/components/right-panel/changes/ChangelogActions.tsx +105 -0
  65. package/src/components/right-panel/changes/ChangesPanel.tsx +1474 -0
  66. package/src/components/right-panel/claude/ApplyConfirmModal.tsx +376 -0
  67. package/src/components/right-panel/claude/ClaudeErrorState.tsx +125 -0
  68. package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +482 -0
  69. package/src/components/right-panel/claude/ClaudeProgressIndicator.tsx +76 -0
  70. package/src/components/right-panel/claude/DiffCard.tsx +130 -0
  71. package/src/components/right-panel/claude/DiffViewer.tsx +54 -0
  72. package/src/components/right-panel/claude/ProjectRootSelector.tsx +275 -0
  73. package/src/components/right-panel/claude/ResultsSummary.tsx +119 -0
  74. package/src/components/right-panel/claude/SetupFlow.tsx +315 -0
  75. package/src/components/right-panel/console/ConsolePanel.tsx +209 -0
  76. package/src/components/right-panel/design/AppearanceSection.tsx +703 -0
  77. package/src/components/right-panel/design/BackgroundSection.tsx +516 -0
  78. package/src/components/right-panel/design/BorderSection.tsx +161 -0
  79. package/src/components/right-panel/design/CSSRawView.tsx +412 -0
  80. package/src/components/right-panel/design/DesignCSSTabToggle.tsx +51 -0
  81. package/src/components/right-panel/design/DesignPanel.tsx +275 -0
  82. package/src/components/right-panel/design/ElementBreadcrumb.tsx +51 -0
  83. package/src/components/right-panel/design/GradientEditor.tsx +726 -0
  84. package/src/components/right-panel/design/LayoutSection.tsx +1948 -0
  85. package/src/components/right-panel/design/PositionSection.tsx +865 -0
  86. package/src/components/right-panel/design/PropertiesSection.tsx +86 -0
  87. package/src/components/right-panel/design/SVGSection.tsx +361 -0
  88. package/src/components/right-panel/design/ShadowBlurSection.tsx +227 -0
  89. package/src/components/right-panel/design/SizeSection.tsx +183 -0
  90. package/src/components/right-panel/design/TextSection.tsx +719 -0
  91. package/src/components/right-panel/design/icons.tsx +948 -0
  92. package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +467 -0
  93. package/src/components/right-panel/design/inputs/ColorInput.tsx +43 -0
  94. package/src/components/right-panel/design/inputs/CompactInput.tsx +333 -0
  95. package/src/components/right-panel/design/inputs/DraggableLabel.tsx +118 -0
  96. package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +54 -0
  97. package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +174 -0
  98. package/src/components/right-panel/design/inputs/SectionHeader.tsx +79 -0
  99. package/src/components/right-panel/variables/VariablesPanel.tsx +388 -0
  100. package/src/hooks/useBridge.ts +95 -0
  101. package/src/hooks/useChangeTracker.ts +563 -0
  102. package/src/hooks/useClaudeAPI.ts +118 -0
  103. package/src/hooks/useDOMTree.ts +25 -0
  104. package/src/hooks/useKeyboardShortcuts.ts +76 -0
  105. package/src/hooks/usePostMessage.ts +589 -0
  106. package/src/hooks/useProjectScan.ts +204 -0
  107. package/src/hooks/useResizable.ts +20 -0
  108. package/src/hooks/useSelectedElement.ts +51 -0
  109. package/src/hooks/useTargetUrl.ts +81 -0
  110. package/src/inspector/DOMTraverser.ts +71 -0
  111. package/src/inspector/ElementSelector.ts +23 -0
  112. package/src/inspector/HoverHighlighter.ts +54 -0
  113. package/src/inspector/SelectionHighlighter.ts +27 -0
  114. package/src/inspector/StyleExtractor.ts +19 -0
  115. package/src/inspector/inspector.ts +17 -0
  116. package/src/inspector/messaging.ts +30 -0
  117. package/src/lib/apiBase.ts +15 -0
  118. package/src/lib/classifyElement.ts +430 -0
  119. package/src/lib/claude-bin.ts +197 -0
  120. package/src/lib/claude-stream.ts +158 -0
  121. package/src/lib/clientProjectScanner.ts +344 -0
  122. package/src/lib/componentMatcher.ts +156 -0
  123. package/src/lib/constants.ts +573 -0
  124. package/src/lib/cssVariableUtils.ts +409 -0
  125. package/src/lib/diffParser.ts +206 -0
  126. package/src/lib/folderPicker.ts +84 -0
  127. package/src/lib/gradientParser.ts +160 -0
  128. package/src/lib/projectScanner.ts +355 -0
  129. package/src/lib/promptBuilder.ts +402 -0
  130. package/src/lib/shadowParser.ts +124 -0
  131. package/src/lib/tailwindClassParser.ts +248 -0
  132. package/src/lib/textShadowUtils.ts +106 -0
  133. package/src/lib/utils.ts +299 -0
  134. package/src/lib/validatePath.ts +40 -0
  135. package/src/proxy.ts +92 -0
  136. package/src/server/terminal-server.ts +104 -0
  137. package/src/store/changeSlice.ts +288 -0
  138. package/src/store/claudeSlice.ts +222 -0
  139. package/src/store/componentSlice.ts +90 -0
  140. package/src/store/consoleSlice.ts +51 -0
  141. package/src/store/cssVariableSlice.ts +94 -0
  142. package/src/store/elementSlice.ts +78 -0
  143. package/src/store/index.ts +35 -0
  144. package/src/store/terminalSlice.ts +30 -0
  145. package/src/store/treeSlice.ts +69 -0
  146. package/src/store/uiSlice.ts +327 -0
  147. package/src/types/changelog.ts +49 -0
  148. package/src/types/claude.ts +131 -0
  149. package/src/types/component.ts +49 -0
  150. package/src/types/cssVariables.ts +18 -0
  151. package/src/types/element.ts +21 -0
  152. package/src/types/file-system-access.d.ts +27 -0
  153. package/src/types/gradient.ts +12 -0
  154. package/src/types/messages.ts +392 -0
  155. package/src/types/shadow.ts +8 -0
  156. package/src/types/tree.ts +9 -0
  157. package/tsconfig.json +42 -0
  158. package/tsconfig.server.json +12 -0
@@ -0,0 +1,563 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { usePostMessage, sendViaIframe } from './usePostMessage'
6
+ import { generateId } from '@/lib/utils'
7
+
8
+ /**
9
+ * Perform undo — can be called outside React components (e.g., keyboard shortcuts).
10
+ * Pops from undo stack, reverts the change in the iframe, updates store.
11
+ */
12
+ export function performUndo() {
13
+ const action = useEditorStore.getState().popUndo()
14
+ if (!action) return
15
+
16
+ if (action.property === '__element_moved__') {
17
+ // Parse beforeValue: "parent:<selector>|index:<num>|selector:<oldSelector>"
18
+ const mvParts = action.beforeValue.split('|')
19
+ const mvOldParent = mvParts[0]?.replace('parent:', '') || ''
20
+ const mvOldIndex = parseInt(mvParts[1]?.replace('index:', '') || '0', 10)
21
+ sendViaIframe({
22
+ type: 'REVERT_MOVE_ELEMENT',
23
+ payload: {
24
+ selectorPath: action.elementSelector,
25
+ oldParentSelectorPath: mvOldParent,
26
+ oldIndex: mvOldIndex,
27
+ },
28
+ })
29
+ } else if (action.property === '__element_inserted__') {
30
+ sendViaIframe({
31
+ type: 'REMOVE_INSERTED_ELEMENT',
32
+ payload: { selectorPath: action.elementSelector },
33
+ })
34
+ } else if (action.property === '__element_deleted__') {
35
+ sendViaIframe({
36
+ type: 'REVERT_DELETE',
37
+ payload: {
38
+ selectorPath: action.elementSelector,
39
+ originalDisplay: action.beforeValue,
40
+ },
41
+ })
42
+ } else if (action.property === '__text_content__') {
43
+ if (action.wasNewChange) {
44
+ sendViaIframe({
45
+ type: 'REVERT_TEXT_CONTENT',
46
+ payload: {
47
+ selectorPath: action.elementSelector,
48
+ originalText: action.beforeValue,
49
+ },
50
+ })
51
+ } else {
52
+ sendViaIframe({
53
+ type: 'SET_TEXT_CONTENT',
54
+ payload: {
55
+ selectorPath: action.elementSelector,
56
+ text: action.beforeValue,
57
+ },
58
+ })
59
+ }
60
+ } else if (action.wasNewChange) {
61
+ sendViaIframe({
62
+ type: 'REVERT_CHANGE',
63
+ payload: {
64
+ selectorPath: action.elementSelector,
65
+ property: action.property,
66
+ },
67
+ })
68
+ } else {
69
+ sendViaIframe({
70
+ type: 'PREVIEW_CHANGE',
71
+ payload: {
72
+ selectorPath: action.elementSelector,
73
+ property: action.property,
74
+ value: action.beforeValue,
75
+ },
76
+ })
77
+ }
78
+
79
+ // Update local computedStyles for undo
80
+ if (
81
+ action.property !== '__text_content__' &&
82
+ action.property !== '__element_deleted__' &&
83
+ action.property !== '__element_inserted__' &&
84
+ action.property !== '__element_moved__'
85
+ ) {
86
+ const store = useEditorStore.getState()
87
+ store.updateComputedStyles({
88
+ ...store.computedStyles,
89
+ [action.property]: action.beforeValue,
90
+ })
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Perform redo — can be called outside React components (e.g., keyboard shortcuts).
96
+ * Pops from redo stack, re-applies the change in the iframe, updates store.
97
+ */
98
+ export function performRedo() {
99
+ // Check if this redo will auto-remove a change (value returns to original)
100
+ const { redoStack, styleChanges } = useEditorStore.getState()
101
+ if (redoStack.length === 0) return
102
+ const peekAction = redoStack[redoStack.length - 1]
103
+ const existingChange = styleChanges.find(
104
+ (c) =>
105
+ c.elementSelector === peekAction.elementSelector &&
106
+ c.property === peekAction.property,
107
+ )
108
+ const willAutoRemove =
109
+ existingChange && peekAction.afterValue === existingChange.originalValue
110
+
111
+ const action = useEditorStore.getState().popRedo()
112
+ if (!action) return
113
+
114
+ if (action.property === '__element_moved__') {
115
+ // Re-do the move: parse afterValue "parent:<selector>|index:<num>"
116
+ const rdMvParts = action.afterValue.split('|')
117
+ const rdMvParent = rdMvParts[0]?.replace('parent:', '') || ''
118
+ const rdMvIndex = parseInt(rdMvParts[1]?.replace('index:', '') || '0', 10)
119
+ // Parse beforeValue to get the old selector: "parent:...|index:...|selector:<oldSelector>"
120
+ const rdMvBefore = action.beforeValue.split('|')
121
+ const rdMvOldSelector =
122
+ rdMvBefore[2]?.replace('selector:', '') || action.elementSelector
123
+ sendViaIframe({
124
+ type: 'MOVE_ELEMENT',
125
+ payload: {
126
+ selectorPath: rdMvOldSelector,
127
+ newParentSelectorPath: rdMvParent,
128
+ newIndex: rdMvIndex,
129
+ },
130
+ })
131
+ } else if (action.property === '__element_inserted__') {
132
+ // Re-insert: parse the originalValue which stores parent and tag info
133
+ // For redo of element insertion, we'd need to re-insert, but since the element
134
+ // was removed from DOM, a full redo isn't trivially possible. Reload instead.
135
+ const iframe = document.querySelector<HTMLIFrameElement>(
136
+ 'iframe[title="Preview"]',
137
+ )
138
+ if (iframe?.src) iframe.src = iframe.src
139
+ } else if (action.property === '__element_deleted__') {
140
+ sendViaIframe({
141
+ type: 'PREVIEW_CHANGE',
142
+ payload: {
143
+ selectorPath: action.elementSelector,
144
+ property: 'display',
145
+ value: 'none',
146
+ },
147
+ })
148
+ } else if (action.property === '__text_content__') {
149
+ sendViaIframe({
150
+ type: 'SET_TEXT_CONTENT',
151
+ payload: {
152
+ selectorPath: action.elementSelector,
153
+ text: action.afterValue,
154
+ },
155
+ })
156
+ } else if (willAutoRemove) {
157
+ // Value returned to original — revert inline style entirely
158
+ sendViaIframe({
159
+ type: 'REVERT_CHANGE',
160
+ payload: {
161
+ selectorPath: action.elementSelector,
162
+ property: action.property,
163
+ },
164
+ })
165
+ } else {
166
+ sendViaIframe({
167
+ type: 'PREVIEW_CHANGE',
168
+ payload: {
169
+ selectorPath: action.elementSelector,
170
+ property: action.property,
171
+ value: action.afterValue,
172
+ },
173
+ })
174
+ }
175
+
176
+ // Update local computedStyles for redo
177
+ if (
178
+ action.property !== '__text_content__' &&
179
+ action.property !== '__element_deleted__' &&
180
+ action.property !== '__element_inserted__' &&
181
+ action.property !== '__element_moved__'
182
+ ) {
183
+ const store = useEditorStore.getState()
184
+ store.updateComputedStyles({
185
+ ...store.computedStyles,
186
+ [action.property]: action.afterValue,
187
+ })
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Revert all changes — can be called outside React components.
193
+ * Reverts text changes, clears the store, and reloads the iframe.
194
+ */
195
+ export function performRevertAll() {
196
+ const state = useEditorStore.getState()
197
+ const textChanges = state.styleChanges.filter(
198
+ (c) => c.property === '__text_content__',
199
+ )
200
+ for (const tc of textChanges) {
201
+ sendViaIframe({
202
+ type: 'REVERT_TEXT_CONTENT',
203
+ payload: {
204
+ selectorPath: tc.elementSelector,
205
+ originalText: tc.originalValue,
206
+ },
207
+ })
208
+ }
209
+ const deleteChanges = state.styleChanges.filter(
210
+ (c) => c.property === '__element_deleted__',
211
+ )
212
+ for (const dc of deleteChanges) {
213
+ sendViaIframe({
214
+ type: 'REVERT_DELETE',
215
+ payload: {
216
+ selectorPath: dc.elementSelector,
217
+ originalDisplay: dc.originalValue,
218
+ },
219
+ })
220
+ }
221
+ const insertChanges = state.styleChanges.filter(
222
+ (c) => c.property === '__element_inserted__',
223
+ )
224
+ for (const ic of insertChanges) {
225
+ sendViaIframe({
226
+ type: 'REMOVE_INSERTED_ELEMENT',
227
+ payload: { selectorPath: ic.elementSelector },
228
+ })
229
+ }
230
+ const moveChanges = state.styleChanges.filter(
231
+ (c) => c.property === '__element_moved__',
232
+ )
233
+ for (const mc of moveChanges) {
234
+ const parts = mc.originalValue.split('|')
235
+ const oldParent = parts[0]?.replace('parent:', '') || ''
236
+ const oldIndex = parseInt(parts[1]?.replace('index:', '') || '0', 10)
237
+ sendViaIframe({
238
+ type: 'REVERT_MOVE_ELEMENT',
239
+ payload: {
240
+ selectorPath: mc.elementSelector,
241
+ oldParentSelectorPath: oldParent,
242
+ oldIndex,
243
+ },
244
+ })
245
+ }
246
+
247
+ state.clearAllChanges()
248
+ state.clearComponents()
249
+
250
+ // Clear persisted changes from localStorage so they don't come back on refresh
251
+ if (state.targetUrl) {
252
+ state.persistChanges(state.targetUrl)
253
+ }
254
+
255
+ // Force-reload the iframe to guarantee a clean state
256
+ const iframe = document.querySelector<HTMLIFrameElement>(
257
+ 'iframe[title="Preview"]',
258
+ )
259
+ if (iframe?.src) {
260
+ iframe.src = iframe.src
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Hook that tracks style changes, sends PREVIEW_CHANGE to inspector,
266
+ * and auto-persists changes to localStorage.
267
+ */
268
+ export function useChangeTracker() {
269
+ const targetUrl = useEditorStore((s) => s.targetUrl)
270
+ const addStyleChange = useEditorStore((s) => s.addStyleChange)
271
+ const removeStyleChange = useEditorStore((s) => s.removeStyleChange)
272
+ const saveElementSnapshot = useEditorStore((s) => s.saveElementSnapshot)
273
+ const pushUndo = useEditorStore((s) => s.pushUndo)
274
+ const { sendToInspector } = usePostMessage()
275
+
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
+
280
+ 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
+ }
301
+ }, [])
302
+
303
+ // Load persisted changes when target URL changes
304
+ useEffect(() => {
305
+ if (targetUrl) {
306
+ useEditorStore.getState().loadPersistedChanges(targetUrl)
307
+ }
308
+ }, [targetUrl])
309
+
310
+ const applyChange = useCallback(
311
+ (property: string, value: string) => {
312
+ // Read latest state directly to avoid stale closures and
313
+ // prevent re-creating this callback on every computedStyles change.
314
+ const { selectorPath, computedStyles, activeBreakpoint } =
315
+ useEditorStore.getState()
316
+ if (!selectorPath) return
317
+
318
+ // Check if a change already exists for this element+property
319
+ const existing = useEditorStore
320
+ .getState()
321
+ .styleChanges.find(
322
+ (c) => c.elementSelector === selectorPath && c.property === property,
323
+ )
324
+
325
+ // When an existing change exists, compare against its newValue (exact format)
326
+ // rather than computedStyles which may have been reformatted by the browser
327
+ // (e.g., hex → rgb). This ensures rapid color picks always record the latest value.
328
+ const currentValue = existing
329
+ ? existing.newValue
330
+ : computedStyles[property] || ''
331
+ const originalValue = computedStyles[property] || ''
332
+
333
+ // Don't track if value hasn't changed
334
+ if (currentValue === value) return
335
+
336
+ // Detect auto-reset: value returning to the true original
337
+ const isAutoReset = existing && value === existing.originalValue
338
+
339
+ // Push undo action
340
+ const state0 = useEditorStore.getState()
341
+ pushUndo({
342
+ elementSelector: selectorPath,
343
+ property,
344
+ beforeValue: existing ? existing.newValue : originalValue,
345
+ afterValue: value,
346
+ breakpoint: activeBreakpoint,
347
+ wasNewChange: !existing,
348
+ changeScope: state0.changeScope,
349
+ })
350
+
351
+ if (isAutoReset) {
352
+ // Revert inline style in iframe (remove it entirely)
353
+ sendToInspector({
354
+ type: 'REVERT_CHANGE',
355
+ payload: { selectorPath, property },
356
+ })
357
+ } else {
358
+ // Send preview change to inspector
359
+ sendToInspector({
360
+ type: 'PREVIEW_CHANGE',
361
+ payload: { selectorPath, property, value },
362
+ })
363
+ }
364
+
365
+ // Update local computedStyles so UI reacts immediately
366
+ useEditorStore.getState().updateComputedStyles({
367
+ ...useEditorStore.getState().computedStyles,
368
+ [property]: value,
369
+ })
370
+
371
+ // Capture element snapshot at the time of change
372
+ const state = useEditorStore.getState()
373
+ saveElementSnapshot({
374
+ selectorPath,
375
+ tagName: state.tagName || 'unknown',
376
+ className: state.className,
377
+ elementId: state.elementId,
378
+ attributes: state.attributes,
379
+ innerText: state.innerText,
380
+ computedStyles: { ...state.computedStyles },
381
+ pagePath: state.currentPagePath,
382
+ changeScope: state.changeScope,
383
+ sourceInfo: state.sourceInfo,
384
+ })
385
+
386
+ // Track the change (addStyleChange auto-removes if newValue === originalValue)
387
+ addStyleChange({
388
+ id: generateId(),
389
+ elementSelector: selectorPath,
390
+ property,
391
+ originalValue,
392
+ newValue: value,
393
+ breakpoint: activeBreakpoint,
394
+ timestamp: Date.now(),
395
+ changeScope: state.changeScope,
396
+ })
397
+ },
398
+ [addStyleChange, saveElementSnapshot, sendToInspector, pushUndo],
399
+ )
400
+
401
+ const revertChange = useCallback(
402
+ (changeId: string, selectorPath: string, property: string) => {
403
+ if (property === '__element_moved__') {
404
+ const change = useEditorStore
405
+ .getState()
406
+ .styleChanges.find((c) => c.id === changeId)
407
+ if (change) {
408
+ const parts = change.originalValue.split('|')
409
+ const oldParent = parts[0]?.replace('parent:', '') || ''
410
+ const oldIndex = parseInt(parts[1]?.replace('index:', '') || '0', 10)
411
+ sendToInspector({
412
+ type: 'REVERT_MOVE_ELEMENT',
413
+ payload: {
414
+ selectorPath,
415
+ oldParentSelectorPath: oldParent,
416
+ oldIndex,
417
+ },
418
+ })
419
+ }
420
+ } else if (property === '__element_inserted__') {
421
+ sendToInspector({
422
+ type: 'REMOVE_INSERTED_ELEMENT',
423
+ payload: { selectorPath },
424
+ })
425
+ } else if (property === '__element_deleted__') {
426
+ const change = useEditorStore
427
+ .getState()
428
+ .styleChanges.find((c) => c.id === changeId)
429
+ if (change) {
430
+ sendToInspector({
431
+ type: 'REVERT_DELETE',
432
+ payload: { selectorPath, originalDisplay: change.originalValue },
433
+ })
434
+ }
435
+ } else if (property === '__text_content__') {
436
+ const change = useEditorStore
437
+ .getState()
438
+ .styleChanges.find((c) => c.id === changeId)
439
+ if (change) {
440
+ sendToInspector({
441
+ type: 'REVERT_TEXT_CONTENT',
442
+ payload: { selectorPath, originalText: change.originalValue },
443
+ })
444
+ }
445
+ } else {
446
+ sendToInspector({
447
+ type: 'REVERT_CHANGE',
448
+ payload: { selectorPath, property },
449
+ })
450
+ }
451
+ removeStyleChange(changeId)
452
+ },
453
+ [sendToInspector, removeStyleChange],
454
+ )
455
+
456
+ const revertAll = useCallback(() => {
457
+ // Revert text and delete changes before clearing (iframe reload handles style changes)
458
+ const state = useEditorStore.getState()
459
+ const textChanges = state.styleChanges.filter(
460
+ (c) => c.property === '__text_content__',
461
+ )
462
+ for (const tc of textChanges) {
463
+ sendToInspector({
464
+ type: 'REVERT_TEXT_CONTENT',
465
+ payload: {
466
+ selectorPath: tc.elementSelector,
467
+ originalText: tc.originalValue,
468
+ },
469
+ })
470
+ }
471
+ const deleteChanges = state.styleChanges.filter(
472
+ (c) => c.property === '__element_deleted__',
473
+ )
474
+ for (const dc of deleteChanges) {
475
+ sendToInspector({
476
+ type: 'REVERT_DELETE',
477
+ payload: {
478
+ selectorPath: dc.elementSelector,
479
+ originalDisplay: dc.originalValue,
480
+ },
481
+ })
482
+ }
483
+ const insertChanges = state.styleChanges.filter(
484
+ (c) => c.property === '__element_inserted__',
485
+ )
486
+ for (const ic of insertChanges) {
487
+ sendToInspector({
488
+ type: 'REMOVE_INSERTED_ELEMENT',
489
+ payload: { selectorPath: ic.elementSelector },
490
+ })
491
+ }
492
+ const moveChanges = state.styleChanges.filter(
493
+ (c) => c.property === '__element_moved__',
494
+ )
495
+ for (const mc of moveChanges) {
496
+ const parts = mc.originalValue.split('|')
497
+ const oldParent = parts[0]?.replace('parent:', '') || ''
498
+ const oldIndex = parseInt(parts[1]?.replace('index:', '') || '0', 10)
499
+ sendToInspector({
500
+ type: 'REVERT_MOVE_ELEMENT',
501
+ payload: {
502
+ selectorPath: mc.elementSelector,
503
+ oldParentSelectorPath: oldParent,
504
+ oldIndex,
505
+ },
506
+ })
507
+ }
508
+
509
+ state.clearAllChanges()
510
+
511
+ // Persist empty state to localStorage so changes don't reappear on reconnect
512
+ if (state.targetUrl) {
513
+ state.persistChanges(state.targetUrl)
514
+ }
515
+
516
+ // Force-reload the iframe to guarantee a clean state — removing
517
+ // inline styles via REVERT_ALL can leave layout artifacts.
518
+ const iframe = document.querySelector<HTMLIFrameElement>(
519
+ 'iframe[title="Preview"]',
520
+ )
521
+ if (iframe?.src) {
522
+ iframe.src = iframe.src
523
+ }
524
+ }, [sendToInspector])
525
+
526
+ const resetProperty = useCallback(
527
+ (property: string) => {
528
+ const { selectorPath, styleChanges, computedStyles } =
529
+ useEditorStore.getState()
530
+ if (!selectorPath) return
531
+
532
+ const change = styleChanges.find(
533
+ (c) => c.elementSelector === selectorPath && c.property === property,
534
+ )
535
+ if (!change) return
536
+
537
+ // Revert in iframe
538
+ sendToInspector({
539
+ type: 'REVERT_CHANGE',
540
+ payload: { selectorPath, property },
541
+ })
542
+
543
+ // Remove from tracked changes
544
+ removeStyleChange(change.id)
545
+
546
+ // Restore original computedStyles
547
+ useEditorStore.getState().updateComputedStyles({
548
+ ...useEditorStore.getState().computedStyles,
549
+ [property]: change.originalValue,
550
+ })
551
+ },
552
+ [sendToInspector, removeStyleChange],
553
+ )
554
+
555
+ return {
556
+ applyChange,
557
+ revertChange,
558
+ revertAll,
559
+ resetProperty,
560
+ undo: performUndo,
561
+ redo: performRedo,
562
+ }
563
+ }
@@ -0,0 +1,118 @@
1
+ 'use client'
2
+
3
+ import { useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { getApiBase } from '@/lib/apiBase'
6
+ import type {
7
+ ClaudeAnalyzeResponse,
8
+ ClaudeApplyResponse,
9
+ ClaudeStatusResponse,
10
+ } from '@/types/claude'
11
+
12
+ export function useClaudeAPI() {
13
+ const setClaudeStatus = useEditorStore((s) => s.setClaudeStatus)
14
+ const setCliAvailable = useEditorStore((s) => s.setCliAvailable)
15
+ const setSessionId = useEditorStore((s) => s.setSessionId)
16
+ const setParsedDiffs = useEditorStore((s) => s.setParsedDiffs)
17
+ const setClaudeError = useEditorStore((s) => s.setClaudeError)
18
+ const resetClaude = useEditorStore((s) => s.resetClaude)
19
+
20
+ const checkStatus = useCallback(async (): Promise<boolean> => {
21
+ try {
22
+ const res = await fetch(`${getApiBase()}/api/claude/status`)
23
+ const data: ClaudeStatusResponse = await res.json()
24
+ setCliAvailable(data.available)
25
+ return data.available
26
+ } catch {
27
+ setCliAvailable(false)
28
+ return false
29
+ }
30
+ }, [setCliAvailable])
31
+
32
+ const analyze = useCallback(
33
+ async (changelog: string, projectRoot: string) => {
34
+ resetClaude()
35
+ setClaudeStatus('analyzing')
36
+
37
+ try {
38
+ const res = await fetch(`${getApiBase()}/api/claude/analyze`, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ changelog, projectRoot }),
42
+ })
43
+
44
+ if (!res.ok) {
45
+ const err = await res.json()
46
+ setClaudeStatus('error')
47
+ setClaudeError({
48
+ code: err.code || 'UNKNOWN',
49
+ message: err.error || 'Analysis failed',
50
+ })
51
+ return
52
+ }
53
+
54
+ const data: ClaudeAnalyzeResponse = await res.json()
55
+ setSessionId(data.sessionId)
56
+ setParsedDiffs(data.diffs)
57
+ setClaudeStatus('complete')
58
+ } catch (e) {
59
+ setClaudeStatus('error')
60
+ setClaudeError({
61
+ code: 'UNKNOWN',
62
+ message: e instanceof Error ? e.message : 'Network error',
63
+ })
64
+ }
65
+ },
66
+ [
67
+ resetClaude,
68
+ setClaudeStatus,
69
+ setSessionId,
70
+ setParsedDiffs,
71
+ setClaudeError,
72
+ ],
73
+ )
74
+
75
+ const apply = useCallback(
76
+ async (sessionId: string, projectRoot: string) => {
77
+ setClaudeStatus('applying')
78
+
79
+ try {
80
+ const res = await fetch(`${getApiBase()}/api/claude/apply`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ sessionId, projectRoot }),
84
+ })
85
+
86
+ if (!res.ok) {
87
+ const err = await res.json()
88
+ setClaudeStatus('error')
89
+ setClaudeError({
90
+ code: err.code || 'UNKNOWN',
91
+ message: err.error || 'Apply failed',
92
+ })
93
+ return
94
+ }
95
+
96
+ const data: ClaudeApplyResponse = await res.json()
97
+ if (data.success) {
98
+ setClaudeStatus('applied')
99
+ } else {
100
+ setClaudeStatus('error')
101
+ setClaudeError({
102
+ code: 'UNKNOWN',
103
+ message: data.summary || 'Apply returned unsuccessful',
104
+ })
105
+ }
106
+ } catch (e) {
107
+ setClaudeStatus('error')
108
+ setClaudeError({
109
+ code: 'UNKNOWN',
110
+ message: e instanceof Error ? e.message : 'Network error',
111
+ })
112
+ }
113
+ },
114
+ [setClaudeStatus, setClaudeError],
115
+ )
116
+
117
+ return { checkStatus, analyze, apply }
118
+ }
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+
6
+ /**
7
+ * Hook that syncs DOM tree state from the inspector.
8
+ * The actual message handling is done in usePostMessage.
9
+ * This hook provides convenient access to tree state.
10
+ */
11
+ export function useDOMTree() {
12
+ const rootNode = useEditorStore((s) => s.rootNode)
13
+ const searchQuery = useEditorStore((s) => s.searchQuery)
14
+ const highlightedNodeId = useEditorStore((s) => s.highlightedNodeId)
15
+ const setSearchQuery = useEditorStore((s) => s.setSearchQuery)
16
+ const toggleNodeExpanded = useEditorStore((s) => s.toggleNodeExpanded)
17
+
18
+ return {
19
+ rootNode,
20
+ searchQuery,
21
+ highlightedNodeId,
22
+ setSearchQuery,
23
+ toggleNodeExpanded,
24
+ }
25
+ }