@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,285 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import type { ClaudeScanResponse } from '@/types/claude'
5
+
6
+ function ChevronIcon({ open, size = 10 }: { open: boolean; size?: number }) {
7
+ return (
8
+ <span
9
+ className="inline-block transition-transform"
10
+ style={{
11
+ transform: open ? 'rotate(0deg)' : 'rotate(-90deg)',
12
+ fontSize: `${size}px`,
13
+ }}
14
+ >
15
+
16
+ </span>
17
+ )
18
+ }
19
+
20
+ function CopyIcon({ size = 14 }: { size?: number }) {
21
+ return (
22
+ <svg
23
+ width={size}
24
+ height={size}
25
+ viewBox="0 0 24 24"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ strokeWidth={2}
29
+ strokeLinecap="round"
30
+ strokeLinejoin="round"
31
+ >
32
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
33
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
34
+ </svg>
35
+ )
36
+ }
37
+
38
+ function CheckIcon({ size = 14 }: { size?: number }) {
39
+ return (
40
+ <svg
41
+ width={size}
42
+ height={size}
43
+ viewBox="0 0 24 24"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ strokeWidth={2}
47
+ strokeLinecap="round"
48
+ strokeLinejoin="round"
49
+ >
50
+ <polyline points="20 6 9 17 4 12" />
51
+ </svg>
52
+ )
53
+ }
54
+
55
+ function CloseIcon({ size = 14 }: { size?: number }) {
56
+ return (
57
+ <svg
58
+ width={size}
59
+ height={size}
60
+ viewBox="0 0 24 24"
61
+ fill="none"
62
+ stroke="currentColor"
63
+ strokeWidth={2}
64
+ strokeLinecap="round"
65
+ strokeLinejoin="round"
66
+ >
67
+ <line x1="18" y1="6" x2="6" y2="18" />
68
+ <line x1="6" y1="6" x2="18" y2="18" />
69
+ </svg>
70
+ )
71
+ }
72
+
73
+ function SendIcon({ size = 14 }: { size?: number }) {
74
+ return (
75
+ <svg
76
+ width={size}
77
+ height={size}
78
+ viewBox="0 0 24 24"
79
+ fill="none"
80
+ stroke="currentColor"
81
+ strokeWidth={2}
82
+ strokeLinecap="round"
83
+ strokeLinejoin="round"
84
+ >
85
+ <line x1="22" y1="2" x2="11" y2="13" />
86
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
87
+ </svg>
88
+ )
89
+ }
90
+
91
+ function useCopy() {
92
+ const [copied, setCopied] = useState(false)
93
+ const copy = useCallback(async (text: string) => {
94
+ try {
95
+ await navigator.clipboard.writeText(text)
96
+ } catch {
97
+ const textarea = document.createElement('textarea')
98
+ textarea.value = text
99
+ textarea.style.position = 'fixed'
100
+ textarea.style.opacity = '0'
101
+ document.body.appendChild(textarea)
102
+ textarea.select()
103
+ document.execCommand('copy')
104
+ document.body.removeChild(textarea)
105
+ }
106
+ setCopied(true)
107
+ setTimeout(() => setCopied(false), 2000)
108
+ }, [])
109
+ return { copied, copy }
110
+ }
111
+
112
+ export function AiScanResultPanel({
113
+ result,
114
+ onDismiss,
115
+ onSendToClaudeCode,
116
+ }: {
117
+ result: ClaudeScanResponse
118
+ onDismiss: () => void
119
+ onSendToClaudeCode: (prompt: string) => void
120
+ }) {
121
+ const [promptText, setPromptText] = useState(result.smartPrompt)
122
+ const [expandedGroups, setExpandedGroups] = useState<Set<number>>(new Set())
123
+ const { copied, copy } = useCopy()
124
+
125
+ const toggleGroup = useCallback((index: number) => {
126
+ setExpandedGroups((prev) => {
127
+ const next = new Set(prev)
128
+ if (next.has(index)) next.delete(index)
129
+ else next.add(index)
130
+ return next
131
+ })
132
+ }, [])
133
+
134
+ return (
135
+ <div
136
+ className="flex flex-col"
137
+ style={{
138
+ borderTop: '1px solid var(--border)',
139
+ background: 'var(--bg-primary)',
140
+ }}
141
+ >
142
+ {/* Header: Intent + dismiss */}
143
+ <div
144
+ className="flex items-center justify-between px-3 py-2"
145
+ style={{ borderBottom: '1px solid var(--border)' }}
146
+ >
147
+ <div className="flex items-center gap-2 min-w-0">
148
+ <span
149
+ className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium flex-shrink-0"
150
+ style={{
151
+ background: 'rgba(168, 85, 247, 0.12)',
152
+ color: '#a855f7',
153
+ border: '1px solid rgba(168, 85, 247, 0.25)',
154
+ }}
155
+ >
156
+ AI Scan
157
+ </span>
158
+ <span
159
+ className="text-[11px] truncate"
160
+ style={{ color: 'var(--text-secondary)' }}
161
+ >
162
+ {result.intent}
163
+ </span>
164
+ </div>
165
+ <button
166
+ onClick={onDismiss}
167
+ className="flex-shrink-0 p-0.5 rounded transition-colors hover:bg-[var(--bg-hover)]"
168
+ style={{ color: 'var(--text-muted)' }}
169
+ title="Dismiss scan results"
170
+ >
171
+ <CloseIcon size={12} />
172
+ </button>
173
+ </div>
174
+
175
+ {/* Groups */}
176
+ {result.groups.length > 0 && (
177
+ <div style={{ borderBottom: '1px solid var(--border)' }}>
178
+ {result.groups.map((group, i) => (
179
+ <div key={i}>
180
+ <div
181
+ onClick={() => toggleGroup(i)}
182
+ className="flex items-center gap-2 px-3 py-1.5 text-[11px] cursor-pointer hover:bg-[var(--bg-hover)] transition-colors"
183
+ role="button"
184
+ tabIndex={0}
185
+ onKeyDown={(e) => {
186
+ if (e.key === 'Enter' || e.key === ' ') {
187
+ e.preventDefault()
188
+ toggleGroup(i)
189
+ }
190
+ }}
191
+ >
192
+ <ChevronIcon open={expandedGroups.has(i)} />
193
+ <span style={{ color: 'var(--text-primary)' }}>
194
+ {group.label}
195
+ </span>
196
+ <span style={{ color: 'var(--text-muted)' }}>
197
+ {group.changeCount} change{group.changeCount !== 1 ? 's' : ''}
198
+ </span>
199
+ </div>
200
+ {expandedGroups.has(i) && group.suggestedFiles.length > 0 && (
201
+ <div className="pl-7 pr-3 pb-1.5">
202
+ {group.suggestedFiles.map((file) => (
203
+ <div
204
+ key={file}
205
+ className="text-[10px] font-mono truncate"
206
+ style={{ color: 'var(--text-muted)' }}
207
+ >
208
+ {file}
209
+ </div>
210
+ ))}
211
+ </div>
212
+ )}
213
+ </div>
214
+ ))}
215
+ </div>
216
+ )}
217
+
218
+ {/* Warnings */}
219
+ {result.warnings.length > 0 && (
220
+ <div
221
+ className="mx-3 mt-2 px-2 py-1.5 rounded text-[11px]"
222
+ style={{
223
+ background: 'rgba(251, 191, 36, 0.08)',
224
+ border: '1px solid rgba(251, 191, 36, 0.25)',
225
+ color: '#fbbf24',
226
+ }}
227
+ >
228
+ {result.warnings.map((warning, i) => (
229
+ <div key={i} className="flex gap-1.5">
230
+ <span className="flex-shrink-0">!</span>
231
+ <span>{warning}</span>
232
+ </div>
233
+ ))}
234
+ </div>
235
+ )}
236
+
237
+ {/* Editable prompt */}
238
+ <div className="px-3 py-2">
239
+ <textarea
240
+ value={promptText}
241
+ onChange={(e) => setPromptText(e.target.value)}
242
+ className="w-full rounded text-[11px] font-mono leading-relaxed resize-y p-2"
243
+ style={{
244
+ background: 'var(--bg-tertiary)',
245
+ color: 'var(--text-primary)',
246
+ border: '1px solid var(--border)',
247
+ minHeight: '120px',
248
+ maxHeight: '300px',
249
+ }}
250
+ spellCheck={false}
251
+ />
252
+ </div>
253
+
254
+ {/* Action buttons */}
255
+ <div className="flex gap-2 px-3 pb-3">
256
+ <button
257
+ onClick={() => copy(promptText)}
258
+ className="flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-md text-[12px] font-medium transition-all"
259
+ style={{
260
+ background: copied
261
+ ? 'rgba(74, 222, 128, 0.15)'
262
+ : 'rgba(168, 85, 247, 0.12)',
263
+ color: copied ? 'var(--success)' : '#a855f7',
264
+ border: `1px solid ${copied ? 'rgba(74, 222, 128, 0.3)' : 'rgba(168, 85, 247, 0.25)'}`,
265
+ }}
266
+ >
267
+ {copied ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
268
+ {copied ? 'Copied' : 'Copy Prompt'}
269
+ </button>
270
+ <button
271
+ onClick={() => onSendToClaudeCode(promptText)}
272
+ className="flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-md text-[12px] font-medium transition-all"
273
+ style={{
274
+ background: 'rgba(74, 158, 255, 0.12)',
275
+ color: 'var(--accent)',
276
+ border: '1px solid rgba(74, 158, 255, 0.25)',
277
+ }}
278
+ >
279
+ <SendIcon size={12} />
280
+ Send to Claude Code
281
+ </button>
282
+ </div>
283
+ </div>
284
+ )
285
+ }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import type { StyleChange } from '@/types/changelog'
4
+ import { camelToKebab } from '@/lib/utils'
5
+
6
+ interface StyleChangeEntryProps {
7
+ change: StyleChange
8
+ /** When provided, display this value instead of change.newValue (CSS tab source of truth). */
9
+ displayValue?: string
10
+ onUndo: (id: string, selectorPath: string, property: string) => void
11
+ }
12
+
13
+ export function ChangeEntry({
14
+ change,
15
+ displayValue,
16
+ onUndo,
17
+ }: StyleChangeEntryProps) {
18
+ const shownValue = displayValue ?? change.newValue
19
+ return (
20
+ <div className="flex items-center gap-2 py-1.5 px-2 rounded text-xs group hover:bg-[var(--bg-hover)] transition-colors">
21
+ <div className="flex-1 min-w-0">
22
+ <div className="flex items-center gap-1">
23
+ <span style={{ color: 'var(--accent)' }}>
24
+ {camelToKebab(change.property)}
25
+ </span>
26
+ </div>
27
+ <div className="flex items-center gap-1 mt-0.5">
28
+ <span
29
+ className="truncate max-w-[80px] line-through"
30
+ style={{ color: 'var(--text-muted)' }}
31
+ >
32
+ {change.originalValue || '(none)'}
33
+ </span>
34
+ <span style={{ color: 'var(--text-muted)' }}>→</span>
35
+ <span
36
+ className="truncate max-w-[80px]"
37
+ style={{ color: 'var(--success)' }}
38
+ >
39
+ {shownValue}
40
+ </span>
41
+ </div>
42
+ </div>
43
+ <button
44
+ onClick={() =>
45
+ onUndo(change.id, change.elementSelector, change.property)
46
+ }
47
+ className="opacity-0 group-hover:opacity-100 px-1.5 py-0.5 text-[10px] rounded transition-opacity"
48
+ style={{
49
+ background: 'var(--bg-tertiary)',
50
+ color: 'var(--text-secondary)',
51
+ border: '1px solid var(--border)',
52
+ }}
53
+ title="Undo this change"
54
+ >
55
+ Undo
56
+ </button>
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,105 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { useChangeTracker } from '@/hooks/useChangeTracker'
6
+ import { formatChangelog } from '@/lib/utils'
7
+ import { BREAKPOINTS } from '@/lib/constants'
8
+
9
+ export function ChangelogActions() {
10
+ const [copied, setCopied] = useState(false)
11
+ const [showConfirm, setShowConfirm] = useState(false)
12
+
13
+ const targetUrl = useEditorStore((s) => s.targetUrl)
14
+ const activeBreakpoint = useEditorStore((s) => s.activeBreakpoint)
15
+ const currentPagePath = useEditorStore((s) => s.currentPagePath)
16
+ const styleChanges = useEditorStore((s) => s.styleChanges)
17
+ const { revertAll } = useChangeTracker()
18
+
19
+ const handleCopyChangelog = useCallback(async () => {
20
+ if (!targetUrl) return
21
+
22
+ const changelog = formatChangelog({
23
+ targetUrl,
24
+ pagePath: currentPagePath,
25
+ breakpoint: activeBreakpoint,
26
+ breakpointWidth: BREAKPOINTS[activeBreakpoint].width,
27
+ styleChanges,
28
+ })
29
+
30
+ try {
31
+ await navigator.clipboard.writeText(changelog)
32
+ setCopied(true)
33
+ setTimeout(() => setCopied(false), 2000)
34
+ } catch {
35
+ const textarea = document.createElement('textarea')
36
+ textarea.value = changelog
37
+ textarea.style.position = 'fixed'
38
+ textarea.style.opacity = '0'
39
+ document.body.appendChild(textarea)
40
+ textarea.select()
41
+ document.execCommand('copy')
42
+ document.body.removeChild(textarea)
43
+ setCopied(true)
44
+ setTimeout(() => setCopied(false), 2000)
45
+ }
46
+ }, [targetUrl, activeBreakpoint, styleChanges, currentPagePath])
47
+
48
+ const handleClearAll = useCallback(() => {
49
+ revertAll()
50
+ setShowConfirm(false)
51
+ }, [revertAll])
52
+
53
+ if (styleChanges.length === 0) return null
54
+
55
+ return (
56
+ <div
57
+ className="flex flex-col gap-2 p-3"
58
+ style={{ borderTop: '1px solid var(--border)' }}
59
+ >
60
+ <button
61
+ onClick={handleCopyChangelog}
62
+ className="w-full py-1.5 px-3 rounded text-xs font-medium transition-colors"
63
+ style={{
64
+ background: copied ? 'var(--success)' : 'var(--accent)',
65
+ color: '#fff',
66
+ }}
67
+ >
68
+ {copied ? 'Copied!' : 'Copy Changelog'}
69
+ </button>
70
+
71
+ {showConfirm ? (
72
+ <div className="flex gap-2">
73
+ <button
74
+ onClick={handleClearAll}
75
+ className="flex-1 py-1.5 px-3 rounded text-xs font-medium transition-colors"
76
+ style={{ background: 'var(--error)', color: '#fff' }}
77
+ >
78
+ Confirm Clear
79
+ </button>
80
+ <button
81
+ onClick={() => setShowConfirm(false)}
82
+ className="flex-1 py-1.5 px-3 rounded text-xs font-medium transition-colors"
83
+ style={{
84
+ background: 'var(--bg-hover)',
85
+ color: 'var(--text-secondary)',
86
+ }}
87
+ >
88
+ Cancel
89
+ </button>
90
+ </div>
91
+ ) : (
92
+ <button
93
+ onClick={() => setShowConfirm(true)}
94
+ className="w-full py-1.5 px-3 rounded text-xs font-medium transition-colors"
95
+ style={{
96
+ background: 'var(--bg-hover)',
97
+ color: 'var(--text-secondary)',
98
+ }}
99
+ >
100
+ Clear All Changes
101
+ </button>
102
+ )}
103
+ </div>
104
+ )
105
+ }