@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.
- package/LICENSE +178 -0
- package/NOTICE +4 -0
- package/README.md +180 -0
- package/bin/paint.js +266 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +19 -0
- package/package.json +81 -0
- package/postcss.config.mjs +8 -0
- package/public/dev-editor-inspector.js +1872 -0
- package/src/app/api/claude/analyze/route.ts +319 -0
- package/src/app/api/claude/apply/route.ts +185 -0
- package/src/app/api/claude/pick-folder/route.ts +64 -0
- package/src/app/api/claude/scan/route.ts +221 -0
- package/src/app/api/claude/status/route.ts +55 -0
- package/src/app/api/project/scan/route.ts +634 -0
- package/src/app/api/project-scan/css-variables/route.ts +238 -0
- package/src/app/api/project-scan/route.ts +40 -0
- package/src/app/api/project-scan/tailwind-config/route.ts +172 -0
- package/src/app/api/proxy/[[...path]]/route.ts +2400 -0
- package/src/app/docs/DocsClient.tsx +322 -0
- package/src/app/docs/layout.tsx +7 -0
- package/src/app/docs/page.tsx +855 -0
- package/src/app/globals.css +176 -0
- package/src/app/layout.tsx +19 -0
- package/src/app/page.tsx +46 -0
- package/src/bridge/api-handlers.ts +885 -0
- package/src/bridge/proxy-handler.ts +329 -0
- package/src/bridge/server.ts +113 -0
- package/src/components/BreakpointTabs.tsx +72 -0
- package/src/components/ChangeSummaryModal.tsx +267 -0
- package/src/components/ConnectModal.tsx +994 -0
- package/src/components/Editor.tsx +90 -0
- package/src/components/PageSelector.tsx +208 -0
- package/src/components/PreviewFrame.tsx +299 -0
- package/src/components/ProjectFolderBanner.tsx +91 -0
- package/src/components/ResponsiveToolbar.tsx +222 -0
- package/src/components/TargetSelector.tsx +243 -0
- package/src/components/TopBar.tsx +315 -0
- package/src/components/common/CollapsibleSection.tsx +36 -0
- package/src/components/common/ColorPicker.tsx +920 -0
- package/src/components/common/EditablePre.tsx +136 -0
- package/src/components/common/ErrorBoundary.tsx +65 -0
- package/src/components/common/ResizablePanel.tsx +83 -0
- package/src/components/common/ScanAnimation.tsx +76 -0
- package/src/components/common/ToastContainer.tsx +97 -0
- package/src/components/common/UnitInput.tsx +77 -0
- package/src/components/common/VariableColorPicker.tsx +622 -0
- package/src/components/left-panel/AddElementPanel.tsx +237 -0
- package/src/components/left-panel/ComponentsPanel.tsx +609 -0
- package/src/components/left-panel/IconSidebar.tsx +99 -0
- package/src/components/left-panel/LayerNode.tsx +874 -0
- package/src/components/left-panel/LayerSearch.tsx +23 -0
- package/src/components/left-panel/LayersPanel.tsx +52 -0
- package/src/components/left-panel/LeftPanel.tsx +122 -0
- package/src/components/left-panel/PagesPanel.tsx +114 -0
- package/src/components/left-panel/icons.tsx +162 -0
- package/src/components/left-panel/terminal/ScanOverlay.tsx +66 -0
- package/src/components/left-panel/terminal/TerminalPanel.tsx +217 -0
- package/src/components/right-panel/ElementLogBox.tsx +248 -0
- package/src/components/right-panel/PanelTabs.tsx +83 -0
- package/src/components/right-panel/RightPanel.tsx +41 -0
- package/src/components/right-panel/changes/AiScanResultPanel.tsx +285 -0
- package/src/components/right-panel/changes/ChangeEntry.tsx +59 -0
- package/src/components/right-panel/changes/ChangelogActions.tsx +105 -0
- package/src/components/right-panel/changes/ChangesPanel.tsx +1474 -0
- package/src/components/right-panel/claude/ApplyConfirmModal.tsx +376 -0
- package/src/components/right-panel/claude/ClaudeErrorState.tsx +125 -0
- package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +482 -0
- package/src/components/right-panel/claude/ClaudeProgressIndicator.tsx +76 -0
- package/src/components/right-panel/claude/DiffCard.tsx +130 -0
- package/src/components/right-panel/claude/DiffViewer.tsx +54 -0
- package/src/components/right-panel/claude/ProjectRootSelector.tsx +275 -0
- package/src/components/right-panel/claude/ResultsSummary.tsx +119 -0
- package/src/components/right-panel/claude/SetupFlow.tsx +315 -0
- package/src/components/right-panel/console/ConsolePanel.tsx +209 -0
- package/src/components/right-panel/design/AppearanceSection.tsx +703 -0
- package/src/components/right-panel/design/BackgroundSection.tsx +516 -0
- package/src/components/right-panel/design/BorderSection.tsx +161 -0
- package/src/components/right-panel/design/CSSRawView.tsx +412 -0
- package/src/components/right-panel/design/DesignCSSTabToggle.tsx +51 -0
- package/src/components/right-panel/design/DesignPanel.tsx +275 -0
- package/src/components/right-panel/design/ElementBreadcrumb.tsx +51 -0
- package/src/components/right-panel/design/GradientEditor.tsx +726 -0
- package/src/components/right-panel/design/LayoutSection.tsx +1948 -0
- package/src/components/right-panel/design/PositionSection.tsx +865 -0
- package/src/components/right-panel/design/PropertiesSection.tsx +86 -0
- package/src/components/right-panel/design/SVGSection.tsx +361 -0
- package/src/components/right-panel/design/ShadowBlurSection.tsx +227 -0
- package/src/components/right-panel/design/SizeSection.tsx +183 -0
- package/src/components/right-panel/design/TextSection.tsx +719 -0
- package/src/components/right-panel/design/icons.tsx +948 -0
- package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +467 -0
- package/src/components/right-panel/design/inputs/ColorInput.tsx +43 -0
- package/src/components/right-panel/design/inputs/CompactInput.tsx +333 -0
- package/src/components/right-panel/design/inputs/DraggableLabel.tsx +118 -0
- package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +54 -0
- package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +174 -0
- package/src/components/right-panel/design/inputs/SectionHeader.tsx +79 -0
- package/src/components/right-panel/variables/VariablesPanel.tsx +388 -0
- package/src/hooks/useBridge.ts +95 -0
- package/src/hooks/useChangeTracker.ts +563 -0
- package/src/hooks/useClaudeAPI.ts +118 -0
- package/src/hooks/useDOMTree.ts +25 -0
- package/src/hooks/useKeyboardShortcuts.ts +76 -0
- package/src/hooks/usePostMessage.ts +589 -0
- package/src/hooks/useProjectScan.ts +204 -0
- package/src/hooks/useResizable.ts +20 -0
- package/src/hooks/useSelectedElement.ts +51 -0
- package/src/hooks/useTargetUrl.ts +81 -0
- package/src/inspector/DOMTraverser.ts +71 -0
- package/src/inspector/ElementSelector.ts +23 -0
- package/src/inspector/HoverHighlighter.ts +54 -0
- package/src/inspector/SelectionHighlighter.ts +27 -0
- package/src/inspector/StyleExtractor.ts +19 -0
- package/src/inspector/inspector.ts +17 -0
- package/src/inspector/messaging.ts +30 -0
- package/src/lib/apiBase.ts +15 -0
- package/src/lib/classifyElement.ts +430 -0
- package/src/lib/claude-bin.ts +197 -0
- package/src/lib/claude-stream.ts +158 -0
- package/src/lib/clientProjectScanner.ts +344 -0
- package/src/lib/componentMatcher.ts +156 -0
- package/src/lib/constants.ts +573 -0
- package/src/lib/cssVariableUtils.ts +409 -0
- package/src/lib/diffParser.ts +206 -0
- package/src/lib/folderPicker.ts +84 -0
- package/src/lib/gradientParser.ts +160 -0
- package/src/lib/projectScanner.ts +355 -0
- package/src/lib/promptBuilder.ts +402 -0
- package/src/lib/shadowParser.ts +124 -0
- package/src/lib/tailwindClassParser.ts +248 -0
- package/src/lib/textShadowUtils.ts +106 -0
- package/src/lib/utils.ts +299 -0
- package/src/lib/validatePath.ts +40 -0
- package/src/proxy.ts +92 -0
- package/src/server/terminal-server.ts +104 -0
- package/src/store/changeSlice.ts +288 -0
- package/src/store/claudeSlice.ts +222 -0
- package/src/store/componentSlice.ts +90 -0
- package/src/store/consoleSlice.ts +51 -0
- package/src/store/cssVariableSlice.ts +94 -0
- package/src/store/elementSlice.ts +78 -0
- package/src/store/index.ts +35 -0
- package/src/store/terminalSlice.ts +30 -0
- package/src/store/treeSlice.ts +69 -0
- package/src/store/uiSlice.ts +327 -0
- package/src/types/changelog.ts +49 -0
- package/src/types/claude.ts +131 -0
- package/src/types/component.ts +49 -0
- package/src/types/cssVariables.ts +18 -0
- package/src/types/element.ts +21 -0
- package/src/types/file-system-access.d.ts +27 -0
- package/src/types/gradient.ts +12 -0
- package/src/types/messages.ts +392 -0
- package/src/types/shadow.ts +8 -0
- package/src/types/tree.ts +9 -0
- package/tsconfig.json +42 -0
- 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
|
+
}
|