@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,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
+ &#8728;
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
+ &#8728;
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)' }}>&#10003;</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)' }}>&#9679;</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
+ &#9660;
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
+ }