@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,482 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
4
|
+
import { useEditorStore } from '@/store'
|
|
5
|
+
import { formatChangelog } from '@/lib/utils'
|
|
6
|
+
import { BREAKPOINTS } from '@/lib/constants'
|
|
7
|
+
import { getApiBase } from '@/lib/apiBase'
|
|
8
|
+
import { consumeClaudeStream, formatStderrLine } from '@/lib/claude-stream'
|
|
9
|
+
import { SetupFlow } from './SetupFlow'
|
|
10
|
+
import { ClaudeProgressIndicator } from './ClaudeProgressIndicator'
|
|
11
|
+
import { DiffViewer } from './DiffViewer'
|
|
12
|
+
import { ResultsSummary } from './ResultsSummary'
|
|
13
|
+
import { ClaudeErrorState } from './ClaudeErrorState'
|
|
14
|
+
import type {
|
|
15
|
+
ClaudeAnalyzeResponse,
|
|
16
|
+
ClaudeApplyResponse,
|
|
17
|
+
ClaudeErrorCode,
|
|
18
|
+
} from '@/types/claude'
|
|
19
|
+
|
|
20
|
+
export function ClaudeIntegrationPanel() {
|
|
21
|
+
const claudeStatus = useEditorStore((s) => s.claudeStatus)
|
|
22
|
+
const portRoots = useEditorStore((s) => s.portRoots)
|
|
23
|
+
const cliAvailable = useEditorStore((s) => s.cliAvailable)
|
|
24
|
+
const claudeError = useEditorStore((s) => s.claudeError)
|
|
25
|
+
const sessionId = useEditorStore((s) => s.sessionId)
|
|
26
|
+
const parsedDiffs = useEditorStore((s) => s.parsedDiffs)
|
|
27
|
+
|
|
28
|
+
const setClaudeStatus = useEditorStore((s) => s.setClaudeStatus)
|
|
29
|
+
const setClaudeError = useEditorStore((s) => s.setClaudeError)
|
|
30
|
+
const setSessionId = useEditorStore((s) => s.setSessionId)
|
|
31
|
+
const setParsedDiffs = useEditorStore((s) => s.setParsedDiffs)
|
|
32
|
+
const resetClaude = useEditorStore((s) => s.resetClaude)
|
|
33
|
+
const loadPersistedClaude = useEditorStore((s) => s.loadPersistedClaude)
|
|
34
|
+
|
|
35
|
+
const targetUrl = useEditorStore((s) => s.targetUrl)
|
|
36
|
+
const activeBreakpoint = useEditorStore((s) => s.activeBreakpoint)
|
|
37
|
+
const currentPagePath = useEditorStore((s) => s.currentPagePath)
|
|
38
|
+
const styleChanges = useEditorStore((s) => s.styleChanges)
|
|
39
|
+
const aiScanResult = useEditorStore((s) => s.aiScanResult)
|
|
40
|
+
const aiScanStatus = useEditorStore((s) => s.aiScanStatus)
|
|
41
|
+
const resetAiScan = useEditorStore((s) => s.resetAiScan)
|
|
42
|
+
const showToast = useEditorStore((s) => s.showToast)
|
|
43
|
+
const setActiveLeftTab = useEditorStore((s) => s.setActiveLeftTab)
|
|
44
|
+
|
|
45
|
+
const [analysisSummary, setAnalysisSummary] = useState('')
|
|
46
|
+
const [appliedFiles, setAppliedFiles] = useState<string[]>([])
|
|
47
|
+
const [setupComplete, setSetupComplete] = useState(false)
|
|
48
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
49
|
+
|
|
50
|
+
// Derive projectRoot from per-port mapping
|
|
51
|
+
const projectRoot = targetUrl ? (portRoots[targetUrl] ?? null) : null
|
|
52
|
+
|
|
53
|
+
const totalChanges = styleChanges.length
|
|
54
|
+
|
|
55
|
+
// Load persisted state on mount
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
loadPersistedClaude()
|
|
58
|
+
}, [loadPersistedClaude])
|
|
59
|
+
|
|
60
|
+
// Reset setupComplete when targetUrl changes so setup re-shows if needed
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setSetupComplete(false)
|
|
63
|
+
}, [targetUrl])
|
|
64
|
+
|
|
65
|
+
// Check if setup is needed
|
|
66
|
+
const needsSetup =
|
|
67
|
+
!setupComplete &&
|
|
68
|
+
(cliAvailable === null || cliAvailable === false || !projectRoot)
|
|
69
|
+
|
|
70
|
+
const handleSetupComplete = useCallback(() => {
|
|
71
|
+
setSetupComplete(true)
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
// Shared analysis logic — sends to /api/claude/analyze with SSE streaming
|
|
75
|
+
const runAnalysis = useCallback(
|
|
76
|
+
(payload: {
|
|
77
|
+
changelog?: string
|
|
78
|
+
smartPrompt?: string
|
|
79
|
+
projectRoot: string
|
|
80
|
+
}) => {
|
|
81
|
+
setClaudeStatus('analyzing')
|
|
82
|
+
setClaudeError(null)
|
|
83
|
+
setAnalysisSummary('')
|
|
84
|
+
|
|
85
|
+
// Auto-switch to Terminal tab so user sees progress
|
|
86
|
+
setActiveLeftTab('terminal')
|
|
87
|
+
|
|
88
|
+
// Write header to terminal
|
|
89
|
+
const write = useEditorStore.getState().writeToTerminal
|
|
90
|
+
write?.('\r\n\x1b[1;34m Claude Code: Analyzing...\x1b[0m\r\n')
|
|
91
|
+
|
|
92
|
+
// Abort any previous stream
|
|
93
|
+
abortRef.current?.abort()
|
|
94
|
+
|
|
95
|
+
const controller = consumeClaudeStream<ClaudeAnalyzeResponse>(
|
|
96
|
+
'/api/claude/analyze',
|
|
97
|
+
payload,
|
|
98
|
+
{
|
|
99
|
+
onStderr: (line) => {
|
|
100
|
+
const w = useEditorStore.getState().writeToTerminal
|
|
101
|
+
const formatted = formatStderrLine(line)
|
|
102
|
+
if (formatted) w?.(formatted + '\r\n')
|
|
103
|
+
},
|
|
104
|
+
onResult: (data) => {
|
|
105
|
+
setSessionId(data.sessionId)
|
|
106
|
+
setParsedDiffs(data.diffs)
|
|
107
|
+
setAnalysisSummary(data.summary || '')
|
|
108
|
+
setClaudeStatus('complete')
|
|
109
|
+
const w = useEditorStore.getState().writeToTerminal
|
|
110
|
+
w?.('\x1b[32m Analysis complete.\x1b[0m\r\n')
|
|
111
|
+
},
|
|
112
|
+
onError: (err) => {
|
|
113
|
+
setClaudeStatus('error')
|
|
114
|
+
setClaudeError({
|
|
115
|
+
code: (err.code as ClaudeErrorCode) || 'UNKNOWN',
|
|
116
|
+
message: err.message,
|
|
117
|
+
})
|
|
118
|
+
const w = useEditorStore.getState().writeToTerminal
|
|
119
|
+
w?.(`\x1b[31m Error: ${err.message}\x1b[0m\r\n`)
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
abortRef.current = controller
|
|
125
|
+
},
|
|
126
|
+
[
|
|
127
|
+
setClaudeStatus,
|
|
128
|
+
setClaudeError,
|
|
129
|
+
setSessionId,
|
|
130
|
+
setParsedDiffs,
|
|
131
|
+
setActiveLeftTab,
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const handleAnalyze = useCallback(async () => {
|
|
136
|
+
if (!targetUrl || !projectRoot || totalChanges === 0) return
|
|
137
|
+
|
|
138
|
+
const changelog = formatChangelog({
|
|
139
|
+
targetUrl,
|
|
140
|
+
pagePath: currentPagePath,
|
|
141
|
+
breakpoint: activeBreakpoint,
|
|
142
|
+
breakpointWidth: BREAKPOINTS[activeBreakpoint].width,
|
|
143
|
+
styleChanges,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
await runAnalysis({ changelog, projectRoot })
|
|
147
|
+
}, [
|
|
148
|
+
targetUrl,
|
|
149
|
+
projectRoot,
|
|
150
|
+
totalChanges,
|
|
151
|
+
currentPagePath,
|
|
152
|
+
activeBreakpoint,
|
|
153
|
+
styleChanges,
|
|
154
|
+
runAnalysis,
|
|
155
|
+
])
|
|
156
|
+
|
|
157
|
+
// Auto-trigger analysis when arriving from "Send to Claude Code" in AI Scan
|
|
158
|
+
const hasTriggeredScanRef = useRef(false)
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (
|
|
161
|
+
aiScanStatus === 'complete' &&
|
|
162
|
+
aiScanResult?.smartPrompt &&
|
|
163
|
+
projectRoot &&
|
|
164
|
+
claudeStatus === 'idle' &&
|
|
165
|
+
!hasTriggeredScanRef.current
|
|
166
|
+
) {
|
|
167
|
+
hasTriggeredScanRef.current = true
|
|
168
|
+
const prompt = aiScanResult.smartPrompt
|
|
169
|
+
// Clear the scan result so it doesn't re-trigger
|
|
170
|
+
resetAiScan()
|
|
171
|
+
runAnalysis({ smartPrompt: prompt, projectRoot })
|
|
172
|
+
}
|
|
173
|
+
// Reset the guard when scan result is cleared
|
|
174
|
+
if (aiScanStatus !== 'complete') {
|
|
175
|
+
hasTriggeredScanRef.current = false
|
|
176
|
+
}
|
|
177
|
+
}, [
|
|
178
|
+
aiScanStatus,
|
|
179
|
+
aiScanResult,
|
|
180
|
+
projectRoot,
|
|
181
|
+
claudeStatus,
|
|
182
|
+
resetAiScan,
|
|
183
|
+
runAnalysis,
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
const handleApplyAll = useCallback(async () => {
|
|
187
|
+
if (!sessionId || !projectRoot) return
|
|
188
|
+
|
|
189
|
+
setClaudeStatus('applying')
|
|
190
|
+
setClaudeError(null)
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const res = await fetch(`${getApiBase()}/api/claude/apply`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ sessionId, projectRoot }),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
const data = await res.json().catch(() => ({}))
|
|
201
|
+
setClaudeStatus('error')
|
|
202
|
+
setClaudeError({
|
|
203
|
+
code: data.code || 'UNKNOWN',
|
|
204
|
+
message: data.error || `Apply failed with status ${res.status}`,
|
|
205
|
+
})
|
|
206
|
+
showToast('error', data.error || 'Failed to apply changes')
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const data: ClaudeApplyResponse = await res.json()
|
|
211
|
+
if (data.success) {
|
|
212
|
+
setAppliedFiles(data.filesModified || [])
|
|
213
|
+
setClaudeStatus('applied')
|
|
214
|
+
const fileCount = data.filesModified?.length || 0
|
|
215
|
+
showToast(
|
|
216
|
+
'success',
|
|
217
|
+
`Changes applied successfully — ${fileCount} file${fileCount !== 1 ? 's' : ''} modified`,
|
|
218
|
+
)
|
|
219
|
+
} else {
|
|
220
|
+
setClaudeStatus('error')
|
|
221
|
+
setClaudeError({
|
|
222
|
+
code: 'UNKNOWN',
|
|
223
|
+
message: data.summary || 'Apply returned unsuccessful',
|
|
224
|
+
})
|
|
225
|
+
showToast('error', data.summary || 'Apply returned unsuccessful')
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
setClaudeStatus('error')
|
|
229
|
+
setClaudeError({
|
|
230
|
+
code: 'UNKNOWN',
|
|
231
|
+
message: err instanceof Error ? err.message : 'Network error',
|
|
232
|
+
})
|
|
233
|
+
showToast('error', err instanceof Error ? err.message : 'Network error')
|
|
234
|
+
}
|
|
235
|
+
}, [sessionId, projectRoot, setClaudeStatus, setClaudeError, showToast])
|
|
236
|
+
|
|
237
|
+
const handleRetry = useCallback(() => {
|
|
238
|
+
abortRef.current?.abort()
|
|
239
|
+
abortRef.current = null
|
|
240
|
+
resetClaude()
|
|
241
|
+
setAnalysisSummary('')
|
|
242
|
+
setAppliedFiles([])
|
|
243
|
+
}, [resetClaude])
|
|
244
|
+
|
|
245
|
+
const handleStartOver = useCallback(() => {
|
|
246
|
+
abortRef.current?.abort()
|
|
247
|
+
abortRef.current = null
|
|
248
|
+
resetClaude()
|
|
249
|
+
setAnalysisSummary('')
|
|
250
|
+
setAppliedFiles([])
|
|
251
|
+
}, [resetClaude])
|
|
252
|
+
|
|
253
|
+
// No target connected — prompt user to connect first
|
|
254
|
+
if (!targetUrl) {
|
|
255
|
+
return (
|
|
256
|
+
<div className="flex flex-col items-center justify-center py-12 gap-3 px-4">
|
|
257
|
+
<div
|
|
258
|
+
className="text-2xl"
|
|
259
|
+
style={{ color: 'var(--text-muted)', opacity: 0.5 }}
|
|
260
|
+
>
|
|
261
|
+
∘
|
|
262
|
+
</div>
|
|
263
|
+
<p
|
|
264
|
+
className="text-xs text-center"
|
|
265
|
+
style={{ color: 'var(--text-muted)' }}
|
|
266
|
+
>
|
|
267
|
+
Connect to a project first using the URL bar above, then configure
|
|
268
|
+
Claude Code here.
|
|
269
|
+
</p>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Setup flow
|
|
275
|
+
if (needsSetup) {
|
|
276
|
+
return <SetupFlow targetUrl={targetUrl} onComplete={handleSetupComplete} />
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// State machine UI
|
|
280
|
+
return (
|
|
281
|
+
<div className="flex flex-col h-full">
|
|
282
|
+
{/* Header */}
|
|
283
|
+
<div
|
|
284
|
+
className="flex items-center justify-between px-3 py-2 flex-shrink-0"
|
|
285
|
+
style={{ borderBottom: '1px solid var(--border)' }}
|
|
286
|
+
>
|
|
287
|
+
<div className="flex items-center gap-2">
|
|
288
|
+
<div
|
|
289
|
+
className="w-2 h-2 rounded-full"
|
|
290
|
+
style={{
|
|
291
|
+
background:
|
|
292
|
+
claudeStatus === 'error'
|
|
293
|
+
? 'var(--error)'
|
|
294
|
+
: claudeStatus === 'complete' || claudeStatus === 'applied'
|
|
295
|
+
? 'var(--success)'
|
|
296
|
+
: claudeStatus === 'analyzing' ||
|
|
297
|
+
claudeStatus === 'applying'
|
|
298
|
+
? 'var(--accent)'
|
|
299
|
+
: 'var(--text-muted)',
|
|
300
|
+
}}
|
|
301
|
+
/>
|
|
302
|
+
<span
|
|
303
|
+
className="text-xs font-medium"
|
|
304
|
+
style={{ color: 'var(--text-primary)' }}
|
|
305
|
+
>
|
|
306
|
+
Claude Code
|
|
307
|
+
</span>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{claudeStatus !== 'idle' && (
|
|
311
|
+
<button
|
|
312
|
+
onClick={handleStartOver}
|
|
313
|
+
className="text-[11px] px-2 py-0.5 rounded transition-colors hover:bg-[var(--bg-hover)]"
|
|
314
|
+
style={{ color: 'var(--text-muted)' }}
|
|
315
|
+
>
|
|
316
|
+
Reset
|
|
317
|
+
</button>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* Content area */}
|
|
322
|
+
<div className="flex-1 overflow-y-auto">
|
|
323
|
+
{/* Idle state */}
|
|
324
|
+
{claudeStatus === 'idle' && (
|
|
325
|
+
<div className="flex flex-col gap-3 p-4">
|
|
326
|
+
{totalChanges > 0 ? (
|
|
327
|
+
<>
|
|
328
|
+
<p
|
|
329
|
+
className="text-xs"
|
|
330
|
+
style={{ color: 'var(--text-secondary)' }}
|
|
331
|
+
>
|
|
332
|
+
Send your {totalChanges} tracked change
|
|
333
|
+
{totalChanges !== 1 ? 's' : ''} to Claude Code for analysis.
|
|
334
|
+
Claude will generate diffs that can be applied to your source
|
|
335
|
+
files.
|
|
336
|
+
</p>
|
|
337
|
+
|
|
338
|
+
{projectRoot && (
|
|
339
|
+
<div
|
|
340
|
+
className="flex items-center gap-2 text-[11px] px-2 py-1.5 rounded"
|
|
341
|
+
style={{
|
|
342
|
+
background: 'var(--bg-tertiary)',
|
|
343
|
+
color: 'var(--text-muted)',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
<span>Project:</span>
|
|
347
|
+
<span className="font-mono truncate" title={projectRoot}>
|
|
348
|
+
{projectRoot}
|
|
349
|
+
</span>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
<button
|
|
354
|
+
onClick={handleAnalyze}
|
|
355
|
+
className="w-full py-2 px-3 rounded text-xs font-medium transition-colors"
|
|
356
|
+
style={{ background: 'var(--accent)', color: '#fff' }}
|
|
357
|
+
>
|
|
358
|
+
Send to Claude Code
|
|
359
|
+
</button>
|
|
360
|
+
</>
|
|
361
|
+
) : (
|
|
362
|
+
<div className="flex flex-col items-center justify-center py-8 gap-2">
|
|
363
|
+
<div
|
|
364
|
+
className="text-2xl"
|
|
365
|
+
style={{ color: 'var(--text-muted)', opacity: 0.5 }}
|
|
366
|
+
>
|
|
367
|
+
∘
|
|
368
|
+
</div>
|
|
369
|
+
<p
|
|
370
|
+
className="text-xs text-center"
|
|
371
|
+
style={{ color: 'var(--text-muted)' }}
|
|
372
|
+
>
|
|
373
|
+
No changes to analyze. Make some visual edits first, then send
|
|
374
|
+
them to Claude Code.
|
|
375
|
+
</p>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
{/* Analyzing state */}
|
|
382
|
+
{claudeStatus === 'analyzing' && (
|
|
383
|
+
<ClaudeProgressIndicator status="analyzing" />
|
|
384
|
+
)}
|
|
385
|
+
|
|
386
|
+
{/* Complete state - show diffs and results */}
|
|
387
|
+
{claudeStatus === 'complete' && (
|
|
388
|
+
<div className="flex flex-col">
|
|
389
|
+
<div className="p-3">
|
|
390
|
+
<DiffViewer />
|
|
391
|
+
</div>
|
|
392
|
+
<ResultsSummary
|
|
393
|
+
summary={analysisSummary}
|
|
394
|
+
onApplyAll={handleApplyAll}
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{/* Applying state */}
|
|
400
|
+
{claudeStatus === 'applying' && (
|
|
401
|
+
<ClaudeProgressIndicator status="applying" />
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{/* Applied state */}
|
|
405
|
+
{claudeStatus === 'applied' && (
|
|
406
|
+
<div className="flex flex-col gap-3 p-4">
|
|
407
|
+
<div
|
|
408
|
+
className="flex items-center gap-2 px-3 py-2 rounded"
|
|
409
|
+
style={{
|
|
410
|
+
background: 'rgba(78, 201, 176, 0.1)',
|
|
411
|
+
border: '1px solid var(--success)',
|
|
412
|
+
}}
|
|
413
|
+
>
|
|
414
|
+
<span style={{ color: 'var(--success)' }}>✓</span>
|
|
415
|
+
<span
|
|
416
|
+
className="text-xs font-medium"
|
|
417
|
+
style={{ color: 'var(--success)' }}
|
|
418
|
+
>
|
|
419
|
+
Changes applied successfully
|
|
420
|
+
</span>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{appliedFiles.length > 0 && (
|
|
424
|
+
<div className="flex flex-col gap-1">
|
|
425
|
+
<span
|
|
426
|
+
className="text-[11px] font-medium"
|
|
427
|
+
style={{ color: 'var(--text-secondary)' }}
|
|
428
|
+
>
|
|
429
|
+
Modified files:
|
|
430
|
+
</span>
|
|
431
|
+
{appliedFiles.map((file) => (
|
|
432
|
+
<div
|
|
433
|
+
key={file}
|
|
434
|
+
className="flex items-center gap-2 px-2 py-1 rounded text-[11px] font-mono"
|
|
435
|
+
style={{
|
|
436
|
+
background: 'var(--bg-tertiary)',
|
|
437
|
+
color: 'var(--text-primary)',
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
<span style={{ color: 'var(--success)' }}>●</span>
|
|
441
|
+
<span className="truncate" title={file}>
|
|
442
|
+
{file}
|
|
443
|
+
</span>
|
|
444
|
+
</div>
|
|
445
|
+
))}
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
|
|
449
|
+
{/* Still show the diffs for reference */}
|
|
450
|
+
{parsedDiffs.length > 0 && (
|
|
451
|
+
<div className="mt-2">
|
|
452
|
+
<div
|
|
453
|
+
className="text-[11px] font-medium mb-2"
|
|
454
|
+
style={{ color: 'var(--text-secondary)' }}
|
|
455
|
+
>
|
|
456
|
+
Applied diffs:
|
|
457
|
+
</div>
|
|
458
|
+
<DiffViewer />
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
<button
|
|
463
|
+
onClick={handleStartOver}
|
|
464
|
+
className="w-full py-1.5 px-3 rounded text-xs font-medium transition-colors"
|
|
465
|
+
style={{
|
|
466
|
+
background: 'var(--bg-hover)',
|
|
467
|
+
color: 'var(--text-secondary)',
|
|
468
|
+
}}
|
|
469
|
+
>
|
|
470
|
+
Start New Analysis
|
|
471
|
+
</button>
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
474
|
+
|
|
475
|
+
{/* Error state */}
|
|
476
|
+
{claudeStatus === 'error' && claudeError && (
|
|
477
|
+
<ClaudeErrorState error={claudeError} onRetry={handleRetry} />
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ClaudeStatus } from '@/types/claude'
|
|
4
|
+
|
|
5
|
+
interface ClaudeProgressIndicatorProps {
|
|
6
|
+
status: ClaudeStatus
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ClaudeProgressIndicator({
|
|
10
|
+
status,
|
|
11
|
+
}: ClaudeProgressIndicatorProps) {
|
|
12
|
+
const message =
|
|
13
|
+
status === 'applying'
|
|
14
|
+
? 'Applying changes...'
|
|
15
|
+
: 'Analyzing with Claude Code...'
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex flex-col items-center justify-center gap-4 py-8 px-4">
|
|
19
|
+
{/* CSS-only spinner */}
|
|
20
|
+
<div
|
|
21
|
+
className="w-8 h-8 rounded-full"
|
|
22
|
+
style={{
|
|
23
|
+
border: '2px solid var(--bg-hover)',
|
|
24
|
+
borderTopColor: 'var(--accent)',
|
|
25
|
+
animation: 'claude-spin 0.8s linear infinite',
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
{/* Status message with animated dots */}
|
|
30
|
+
<div className="flex items-center gap-0.5">
|
|
31
|
+
<span className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
|
32
|
+
{message}
|
|
33
|
+
</span>
|
|
34
|
+
<span
|
|
35
|
+
className="inline-flex text-xs"
|
|
36
|
+
style={{ color: 'var(--text-muted)' }}
|
|
37
|
+
>
|
|
38
|
+
<span style={{ animation: 'claude-dot 1.4s infinite 0s' }}>.</span>
|
|
39
|
+
<span style={{ animation: 'claude-dot 1.4s infinite 0.2s' }}>.</span>
|
|
40
|
+
<span style={{ animation: 'claude-dot 1.4s infinite 0.4s' }}>.</span>
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Subtle pulse bar */}
|
|
45
|
+
<div
|
|
46
|
+
className="w-32 h-1 rounded-full overflow-hidden"
|
|
47
|
+
style={{ background: 'var(--bg-tertiary)' }}
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
className="h-full rounded-full"
|
|
51
|
+
style={{
|
|
52
|
+
background: 'var(--accent)',
|
|
53
|
+
animation: 'claude-pulse 1.5s ease-in-out infinite',
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Inline keyframes */}
|
|
59
|
+
<style>{`
|
|
60
|
+
@keyframes claude-spin {
|
|
61
|
+
to { transform: rotate(360deg); }
|
|
62
|
+
}
|
|
63
|
+
@keyframes claude-dot {
|
|
64
|
+
0%, 20% { opacity: 0; }
|
|
65
|
+
50% { opacity: 1; }
|
|
66
|
+
100% { opacity: 0; }
|
|
67
|
+
}
|
|
68
|
+
@keyframes claude-pulse {
|
|
69
|
+
0% { width: 0%; margin-left: 0%; }
|
|
70
|
+
50% { width: 60%; margin-left: 20%; }
|
|
71
|
+
100% { width: 0%; margin-left: 100%; }
|
|
72
|
+
}
|
|
73
|
+
`}</style>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { ParsedDiff } from '@/types/claude'
|
|
5
|
+
|
|
6
|
+
interface DiffCardProps {
|
|
7
|
+
diff: ParsedDiff
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DiffCard({ diff }: DiffCardProps) {
|
|
11
|
+
const [expanded, setExpanded] = useState(true)
|
|
12
|
+
|
|
13
|
+
const fileName = diff.filePath.split('/').pop() || diff.filePath
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className="rounded overflow-hidden"
|
|
18
|
+
style={{
|
|
19
|
+
border: '1px solid var(--border)',
|
|
20
|
+
background: 'var(--bg-secondary)',
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
{/* File header - collapsible */}
|
|
24
|
+
<button
|
|
25
|
+
onClick={() => setExpanded(!expanded)}
|
|
26
|
+
className="flex items-center w-full px-3 py-2 text-left gap-2 hover:bg-[var(--bg-hover)] transition-colors"
|
|
27
|
+
style={{ background: 'var(--bg-tertiary)' }}
|
|
28
|
+
>
|
|
29
|
+
<span
|
|
30
|
+
className="text-[10px] transition-transform flex-shrink-0"
|
|
31
|
+
style={{
|
|
32
|
+
color: 'var(--text-muted)',
|
|
33
|
+
transform: expanded ? 'rotate(0deg)' : 'rotate(-90deg)',
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
▼
|
|
37
|
+
</span>
|
|
38
|
+
|
|
39
|
+
<span
|
|
40
|
+
className="flex-1 text-xs font-mono truncate"
|
|
41
|
+
style={{ color: 'var(--text-primary)' }}
|
|
42
|
+
title={diff.filePath}
|
|
43
|
+
>
|
|
44
|
+
{fileName}
|
|
45
|
+
</span>
|
|
46
|
+
|
|
47
|
+
<div className="flex items-center gap-2 flex-shrink-0 text-[11px]">
|
|
48
|
+
{diff.linesAdded > 0 && (
|
|
49
|
+
<span style={{ color: 'var(--success)' }}>+{diff.linesAdded}</span>
|
|
50
|
+
)}
|
|
51
|
+
{diff.linesRemoved > 0 && (
|
|
52
|
+
<span style={{ color: 'var(--error)' }}>-{diff.linesRemoved}</span>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
{/* File path subtitle */}
|
|
58
|
+
{expanded && diff.filePath !== fileName && (
|
|
59
|
+
<div
|
|
60
|
+
className="px-3 py-1 text-[10px] font-mono truncate"
|
|
61
|
+
style={{
|
|
62
|
+
color: 'var(--text-muted)',
|
|
63
|
+
background: 'var(--bg-tertiary)',
|
|
64
|
+
}}
|
|
65
|
+
title={diff.filePath}
|
|
66
|
+
>
|
|
67
|
+
{diff.filePath}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{/* Diff hunks */}
|
|
72
|
+
{expanded && (
|
|
73
|
+
<div className="overflow-x-auto">
|
|
74
|
+
{diff.hunks.map((hunk, hunkIdx) => (
|
|
75
|
+
<div key={hunkIdx}>
|
|
76
|
+
{/* Hunk header */}
|
|
77
|
+
<div
|
|
78
|
+
className="px-3 py-1 text-[10px] font-mono"
|
|
79
|
+
style={{
|
|
80
|
+
color: 'var(--accent)',
|
|
81
|
+
background: 'rgba(74, 158, 255, 0.06)',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{hunk.header}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Diff lines */}
|
|
88
|
+
{hunk.lines.map((line, lineIdx) => {
|
|
89
|
+
let bgColor = 'transparent'
|
|
90
|
+
let prefixChar = ' '
|
|
91
|
+
let textColor = 'var(--text-primary)'
|
|
92
|
+
|
|
93
|
+
if (line.type === 'addition') {
|
|
94
|
+
bgColor = '#2ea04333'
|
|
95
|
+
prefixChar = '+'
|
|
96
|
+
textColor = '#4ec9b0'
|
|
97
|
+
} else if (line.type === 'removal') {
|
|
98
|
+
bgColor = '#f4474733'
|
|
99
|
+
prefixChar = '-'
|
|
100
|
+
textColor = '#f44747'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
key={lineIdx}
|
|
106
|
+
className="px-3 font-mono text-[11px] leading-5 whitespace-pre"
|
|
107
|
+
style={{ background: bgColor, color: textColor }}
|
|
108
|
+
>
|
|
109
|
+
<span
|
|
110
|
+
className="inline-block w-4 text-center select-none flex-shrink-0"
|
|
111
|
+
style={{
|
|
112
|
+
color:
|
|
113
|
+
line.type === 'context'
|
|
114
|
+
? 'var(--text-muted)'
|
|
115
|
+
: textColor,
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{prefixChar}
|
|
119
|
+
</span>
|
|
120
|
+
{line.content}
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|