@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,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
|
+
}
|