@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,54 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { DiffCard } from './DiffCard'
6
+
7
+ export function DiffViewer() {
8
+ const parsedDiffs = useEditorStore((s) => s.parsedDiffs)
9
+
10
+ const summary = useMemo(() => {
11
+ let totalFiles = parsedDiffs.length
12
+ let totalAdded = 0
13
+ let totalRemoved = 0
14
+ for (const diff of parsedDiffs) {
15
+ totalAdded += diff.linesAdded
16
+ totalRemoved += diff.linesRemoved
17
+ }
18
+ return { totalFiles, totalAdded, totalRemoved }
19
+ }, [parsedDiffs])
20
+
21
+ if (parsedDiffs.length === 0) {
22
+ return (
23
+ <div
24
+ className="flex items-center justify-center py-6 text-xs"
25
+ style={{ color: 'var(--text-muted)' }}
26
+ >
27
+ No diffs to display
28
+ </div>
29
+ )
30
+ }
31
+
32
+ return (
33
+ <div className="flex flex-col gap-3">
34
+ {/* Summary header */}
35
+ <div
36
+ className="flex items-center justify-between px-3 py-2 rounded text-xs"
37
+ style={{ background: 'var(--bg-tertiary)' }}
38
+ >
39
+ <span style={{ color: 'var(--text-secondary)' }}>
40
+ {summary.totalFiles} file{summary.totalFiles !== 1 ? 's' : ''} changed
41
+ </span>
42
+ <div className="flex items-center gap-3">
43
+ <span style={{ color: 'var(--success)' }}>+{summary.totalAdded}</span>
44
+ <span style={{ color: 'var(--error)' }}>-{summary.totalRemoved}</span>
45
+ </div>
46
+ </div>
47
+
48
+ {/* Diff cards */}
49
+ {parsedDiffs.map((diff, idx) => (
50
+ <DiffCard key={`${diff.filePath}-${idx}`} diff={diff} />
51
+ ))}
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,275 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { useProjectScan } from '@/hooks/useProjectScan'
6
+ import { pickFolder } from '@/lib/folderPicker'
7
+ import { isEditorOnLocalhost } from '@/hooks/usePostMessage'
8
+ import { getApiBase } from '@/lib/apiBase'
9
+
10
+ interface ProjectRootSelectorProps {
11
+ targetUrl: string
12
+ onSaved?: () => void
13
+ }
14
+
15
+ export function ProjectRootSelector({
16
+ targetUrl,
17
+ onSaved,
18
+ }: ProjectRootSelectorProps) {
19
+ const portRoots = useEditorStore((s) => s.portRoots)
20
+ const projectRoot = portRoots[targetUrl] ?? null
21
+ const setProjectRoot = useEditorStore((s) => s.setProjectRoot)
22
+ const scanStatus = useEditorStore((s) => s.scanStatus)
23
+ const scannedProjectName = useEditorStore((s) => s.scannedProjectName)
24
+ const componentFileMap = useEditorStore((s) => s.componentFileMap)
25
+ const setDirectoryHandle = useEditorStore((s) => s.setDirectoryHandle)
26
+ const directoryHandle = useEditorStore((s) => s.directoryHandle)
27
+ const { triggerScan, triggerClientScan } = useProjectScan()
28
+
29
+ const bridgeStatus = useEditorStore((s) => s.bridgeStatus)
30
+ const isLocal = typeof window !== 'undefined' && isEditorOnLocalhost()
31
+ const hasServerAccess = isLocal || bridgeStatus === 'connected'
32
+
33
+ const [inputValue, setInputValue] = useState(projectRoot || '')
34
+ const [validating, setValidating] = useState(false)
35
+ const [validationState, setValidationState] = useState<
36
+ 'idle' | 'success' | 'error'
37
+ >('idle')
38
+ const [validationMessage, setValidationMessage] = useState<string | null>(
39
+ null,
40
+ )
41
+ const [picking, setPicking] = useState(false)
42
+
43
+ const scanFeedbackCallbacks = {
44
+ onSuccess: (count: number, projectName: string) => {
45
+ setValidationState('success')
46
+ setValidationMessage(
47
+ count > 0
48
+ ? `Found ${count} components in ${projectName}`
49
+ : 'No component files found — check folder',
50
+ )
51
+ },
52
+ onError: (message: string) => {
53
+ setValidationState('error')
54
+ setValidationMessage(message)
55
+ },
56
+ }
57
+
58
+ const handlePickFolder = useCallback(async () => {
59
+ setPicking(true)
60
+ try {
61
+ const result = await pickFolder()
62
+ if (result.type === 'path') {
63
+ setInputValue(result.path)
64
+ setDirectoryHandle(null)
65
+ setValidationState('idle')
66
+ setValidationMessage(null)
67
+ } else if (result.type === 'handle') {
68
+ setInputValue(result.name)
69
+ setDirectoryHandle(result.handle)
70
+ setValidationState('idle')
71
+ setValidationMessage(null)
72
+ } else if (result.type === 'error') {
73
+ setValidationState('error')
74
+ setValidationMessage(result.message)
75
+ }
76
+ } catch {
77
+ /* user cancelled or error */
78
+ } finally {
79
+ setPicking(false)
80
+ }
81
+ }, [setDirectoryHandle])
82
+
83
+ const handleSave = useCallback(async () => {
84
+ const trimmed = inputValue.trim()
85
+ if (!trimmed) {
86
+ setValidationState('error')
87
+ setValidationMessage('Path cannot be empty')
88
+ return
89
+ }
90
+
91
+ setValidating(true)
92
+ setValidationState('idle')
93
+ setValidationMessage(null)
94
+
95
+ try {
96
+ // Client-side mode: we have a directory handle from the File System Access API
97
+ if (directoryHandle) {
98
+ setProjectRoot(targetUrl, trimmed)
99
+ await triggerClientScan(directoryHandle, scanFeedbackCallbacks)
100
+ onSaved?.()
101
+ return
102
+ }
103
+
104
+ // Server-side mode: validate path and scan on server
105
+ if (!trimmed.startsWith('/')) {
106
+ setValidationState('error')
107
+ setValidationMessage('Path must be absolute (start with /)')
108
+ return
109
+ }
110
+
111
+ const res = await fetch(`${getApiBase()}/api/claude/status`, {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json' },
114
+ body: JSON.stringify({ projectRoot: trimmed }),
115
+ })
116
+
117
+ if (res.ok) {
118
+ setProjectRoot(targetUrl, trimmed)
119
+ await triggerScan(trimmed, scanFeedbackCallbacks)
120
+ onSaved?.()
121
+ } else {
122
+ const data = await res
123
+ .json()
124
+ .catch(() => ({ error: 'Validation failed' }))
125
+ setValidationState('error')
126
+ setValidationMessage(data.error || 'Path validation failed')
127
+ }
128
+ } catch {
129
+ // If the POST endpoint doesn't exist yet, fall back to saving and scanning
130
+ setProjectRoot(targetUrl, trimmed)
131
+ if (directoryHandle) {
132
+ await triggerClientScan(directoryHandle, scanFeedbackCallbacks)
133
+ } else {
134
+ await triggerScan(trimmed, scanFeedbackCallbacks)
135
+ }
136
+ onSaved?.()
137
+ } finally {
138
+ setValidating(false)
139
+ }
140
+ }, [
141
+ inputValue,
142
+ targetUrl,
143
+ setProjectRoot,
144
+ onSaved,
145
+ triggerScan,
146
+ triggerClientScan,
147
+ directoryHandle,
148
+ ])
149
+
150
+ const handleKeyDown = useCallback(
151
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
152
+ if (e.key === 'Enter') {
153
+ handleSave()
154
+ }
155
+ },
156
+ [handleSave],
157
+ )
158
+
159
+ const componentCount = componentFileMap
160
+ ? Object.keys(componentFileMap).length
161
+ : 0
162
+
163
+ return (
164
+ <div className="flex flex-col gap-2">
165
+ <label
166
+ className="text-[11px] font-medium"
167
+ style={{ color: 'var(--text-secondary)' }}
168
+ >
169
+ Project Root
170
+ </label>
171
+
172
+ <div className="flex gap-1.5">
173
+ <input
174
+ type="text"
175
+ value={inputValue}
176
+ onChange={(e) => {
177
+ setInputValue(e.target.value)
178
+ setDirectoryHandle(null)
179
+ setValidationState('idle')
180
+ setValidationMessage(null)
181
+ }}
182
+ onKeyDown={handleKeyDown}
183
+ placeholder={
184
+ hasServerAccess
185
+ ? '/path/to/your/project'
186
+ : 'Click folder icon to browse'
187
+ }
188
+ className="flex-1 min-w-0 px-2 py-1.5 rounded text-xs font-mono outline-none transition-colors"
189
+ style={{
190
+ background: 'var(--bg-tertiary)',
191
+ color: 'var(--text-primary)',
192
+ border: `1px solid ${
193
+ validationState === 'error'
194
+ ? 'var(--error)'
195
+ : validationState === 'success'
196
+ ? 'var(--success)'
197
+ : 'var(--border)'
198
+ }`,
199
+ }}
200
+ readOnly={!hasServerAccess && !!directoryHandle}
201
+ />
202
+ <button
203
+ onClick={handlePickFolder}
204
+ disabled={picking}
205
+ title="Browse for folder"
206
+ className="flex items-center justify-center w-8 h-8 rounded transition-colors disabled:opacity-50 flex-shrink-0"
207
+ style={{
208
+ background: 'var(--bg-tertiary)',
209
+ border: '1px solid var(--border)',
210
+ color: 'var(--text-secondary)',
211
+ }}
212
+ >
213
+ <svg
214
+ width="14"
215
+ height="14"
216
+ viewBox="0 0 16 16"
217
+ fill="none"
218
+ xmlns="http://www.w3.org/2000/svg"
219
+ >
220
+ <path
221
+ d="M1.5 3C1.5 2.17157 2.17157 1.5 3 1.5H6.17157C6.57019 1.5 6.95262 1.65804 7.23431 1.93934L8.06066 2.76569C8.34196 3.04698 8.72439 3.20503 9.12301 3.20503H13C13.8284 3.20503 14.5 3.8766 14.5 4.70503V12.5C14.5 13.3284 13.8284 14 13 14H3C2.17157 14 1.5 13.3284 1.5 12.5V3Z"
222
+ stroke="currentColor"
223
+ strokeWidth="1.3"
224
+ strokeLinecap="round"
225
+ strokeLinejoin="round"
226
+ />
227
+ </svg>
228
+ </button>
229
+ <button
230
+ onClick={handleSave}
231
+ disabled={validating || !inputValue.trim()}
232
+ className="px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 flex-shrink-0"
233
+ style={{
234
+ background: 'var(--accent)',
235
+ color: '#fff',
236
+ }}
237
+ >
238
+ {validating ? '...' : 'Save'}
239
+ </button>
240
+ </div>
241
+
242
+ {validationMessage && (
243
+ <div
244
+ className="text-[11px]"
245
+ style={{
246
+ color:
247
+ validationState === 'success'
248
+ ? componentCount > 0
249
+ ? 'var(--success)'
250
+ : 'var(--warning)'
251
+ : 'var(--error)',
252
+ }}
253
+ >
254
+ {validationState === 'success' ? '\u2713' : '\u2717'}{' '}
255
+ {validationMessage}
256
+ </div>
257
+ )}
258
+
259
+ {scanStatus === 'scanning' && (
260
+ <div className="text-[11px]" style={{ color: 'var(--text-muted)' }}>
261
+ Scanning project for components...
262
+ </div>
263
+ )}
264
+
265
+ {projectRoot &&
266
+ validationState !== 'success' &&
267
+ scanStatus !== 'scanning' && (
268
+ <div className="text-[11px]" style={{ color: 'var(--text-muted)' }}>
269
+ Current: {projectRoot}
270
+ {scannedProjectName && ` (${scannedProjectName})`}
271
+ </div>
272
+ )}
273
+ </div>
274
+ )
275
+ }
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { ApplyConfirmModal } from './ApplyConfirmModal'
6
+
7
+ interface ResultsSummaryProps {
8
+ summary: string
9
+ onApplyAll: () => void
10
+ }
11
+
12
+ export function ResultsSummary({ summary, onApplyAll }: ResultsSummaryProps) {
13
+ const parsedDiffs = useEditorStore((s) => s.parsedDiffs)
14
+ const claudeStatus = useEditorStore((s) => s.claudeStatus)
15
+ const [copied, setCopied] = useState(false)
16
+ const [showConfirmModal, setShowConfirmModal] = useState(false)
17
+
18
+ const handleCopyDiffs = useCallback(async () => {
19
+ if (parsedDiffs.length === 0) return
20
+
21
+ const text = parsedDiffs
22
+ .map((diff) => {
23
+ const header = `--- ${diff.filePath}\n+++ ${diff.filePath}`
24
+ const hunks = diff.hunks
25
+ .map((hunk) => {
26
+ const hunkHeader = hunk.header
27
+ const lines = hunk.lines
28
+ .map((line) => {
29
+ const prefix =
30
+ line.type === 'addition'
31
+ ? '+'
32
+ : line.type === 'removal'
33
+ ? '-'
34
+ : ' '
35
+ return `${prefix}${line.content}`
36
+ })
37
+ .join('\n')
38
+ return `${hunkHeader}\n${lines}`
39
+ })
40
+ .join('\n')
41
+ return `${header}\n${hunks}`
42
+ })
43
+ .join('\n\n')
44
+
45
+ try {
46
+ await navigator.clipboard.writeText(text)
47
+ setCopied(true)
48
+ setTimeout(() => setCopied(false), 2000)
49
+ } catch {
50
+ // Fallback for clipboard API
51
+ const textarea = document.createElement('textarea')
52
+ textarea.value = text
53
+ textarea.style.position = 'fixed'
54
+ textarea.style.opacity = '0'
55
+ document.body.appendChild(textarea)
56
+ textarea.select()
57
+ document.execCommand('copy')
58
+ document.body.removeChild(textarea)
59
+ setCopied(true)
60
+ setTimeout(() => setCopied(false), 2000)
61
+ }
62
+ }, [parsedDiffs])
63
+
64
+ const isApplying = claudeStatus === 'applying'
65
+
66
+ return (
67
+ <div
68
+ className="flex flex-col gap-3 p-3"
69
+ style={{ borderTop: '1px solid var(--border)' }}
70
+ >
71
+ {/* Summary text */}
72
+ {summary && (
73
+ <div
74
+ className="text-xs leading-relaxed"
75
+ style={{ color: 'var(--text-secondary)' }}
76
+ >
77
+ {summary}
78
+ </div>
79
+ )}
80
+
81
+ {/* Action buttons */}
82
+ <div className="flex flex-col gap-2">
83
+ <button
84
+ onClick={() => setShowConfirmModal(true)}
85
+ disabled={isApplying || parsedDiffs.length === 0}
86
+ className="w-full py-1.5 px-3 rounded text-xs font-medium transition-colors disabled:opacity-50"
87
+ style={{
88
+ background: 'var(--accent)',
89
+ color: '#fff',
90
+ }}
91
+ >
92
+ {isApplying ? 'Applying...' : 'Apply All'}
93
+ </button>
94
+
95
+ <button
96
+ onClick={handleCopyDiffs}
97
+ disabled={parsedDiffs.length === 0}
98
+ className="w-full py-1.5 px-3 rounded text-xs font-medium transition-colors disabled:opacity-50"
99
+ style={{
100
+ background: copied ? 'var(--success)' : 'var(--bg-hover)',
101
+ color: copied ? '#fff' : 'var(--text-secondary)',
102
+ }}
103
+ >
104
+ {copied ? 'Copied!' : 'Copy All Diffs'}
105
+ </button>
106
+ </div>
107
+
108
+ {showConfirmModal && (
109
+ <ApplyConfirmModal
110
+ onConfirm={() => {
111
+ setShowConfirmModal(false)
112
+ onApplyAll()
113
+ }}
114
+ onCancel={() => setShowConfirmModal(false)}
115
+ />
116
+ )}
117
+ </div>
118
+ )
119
+ }