@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,315 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useEffect } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { ProjectRootSelector } from './ProjectRootSelector'
6
+ import { isEditorOnLocalhost } from '@/hooks/usePostMessage'
7
+ import { getApiBase } from '@/lib/apiBase'
8
+ import type { ClaudeStatusResponse } from '@/types/claude'
9
+
10
+ interface SetupFlowProps {
11
+ targetUrl: string
12
+ onComplete: () => void
13
+ }
14
+
15
+ export function SetupFlow({ targetUrl, onComplete }: SetupFlowProps) {
16
+ const cliAvailable = useEditorStore((s) => s.cliAvailable)
17
+ const setCliAvailable = useEditorStore((s) => s.setCliAvailable)
18
+ const portRoots = useEditorStore((s) => s.portRoots)
19
+ const projectRoot = portRoots[targetUrl] ?? null
20
+
21
+ const bridgeStatus = useEditorStore((s) => s.bridgeStatus)
22
+ const isLocal = typeof window !== 'undefined' && isEditorOnLocalhost()
23
+ const hasBridge = bridgeStatus === 'connected'
24
+ const hasServerAccess = isLocal || hasBridge
25
+
26
+ const [checking, setChecking] = useState(false)
27
+ const [cliVersion, setCliVersion] = useState<string | null>(null)
28
+ const [checkError, setCheckError] = useState<string | null>(null)
29
+
30
+ const checkCli = useCallback(async () => {
31
+ setChecking(true)
32
+ setCheckError(null)
33
+ try {
34
+ const res = await fetch(`${getApiBase()}/api/claude/status`)
35
+ const data: ClaudeStatusResponse = await res.json()
36
+ setCliAvailable(data.available)
37
+ if (data.available && data.version) {
38
+ setCliVersion(data.version)
39
+ } else {
40
+ setCheckError(data.error || 'Claude CLI not available')
41
+ }
42
+ } catch {
43
+ setCliAvailable(false)
44
+ setCheckError('Failed to check CLI status')
45
+ } finally {
46
+ setChecking(false)
47
+ }
48
+ }, [setCliAvailable])
49
+
50
+ // Auto-check CLI on mount (when running locally or bridge is connected)
51
+ useEffect(() => {
52
+ if (hasServerAccess && cliAvailable === null) {
53
+ checkCli()
54
+ }
55
+ }, [hasServerAccess, cliAvailable, checkCli])
56
+
57
+ const handleProjectRootSaved = useCallback(() => {
58
+ onComplete()
59
+ }, [onComplete])
60
+
61
+ // ─── Remote mode without bridge (Vercel) ───
62
+ // Skip CLI check, show project root selector with File System Access API
63
+ if (!hasServerAccess) {
64
+ return (
65
+ <div className="flex flex-col gap-4 p-4">
66
+ <div
67
+ className="text-xs font-medium"
68
+ style={{ color: 'var(--text-primary)' }}
69
+ >
70
+ Claude Code
71
+ </div>
72
+
73
+ {/* Remote mode notice */}
74
+ <div
75
+ className="flex flex-col gap-1.5 px-3 py-2.5 rounded"
76
+ style={{
77
+ background: 'var(--bg-tertiary)',
78
+ border: '1px solid var(--border)',
79
+ }}
80
+ >
81
+ <div
82
+ className="text-[11px] font-medium"
83
+ style={{ color: 'var(--text-secondary)' }}
84
+ >
85
+ Running remotely
86
+ </div>
87
+ <p
88
+ className="text-[10px] leading-relaxed"
89
+ style={{ color: 'var(--text-muted)' }}
90
+ >
91
+ Start the bridge server locally for full CLI access:{' '}
92
+ <code
93
+ className="font-mono px-1 rounded"
94
+ style={{ background: 'var(--bg-primary)' }}
95
+ >
96
+ bun run bridge
97
+ </code>
98
+ </p>
99
+ <p
100
+ className="text-[10px] leading-relaxed"
101
+ style={{ color: 'var(--text-muted)' }}
102
+ >
103
+ Or select a project folder below to improve changelog accuracy.
104
+ </p>
105
+ </div>
106
+
107
+ {/* Project root setup (uses File System Access API on remote) */}
108
+ <div className="flex flex-col gap-2">
109
+ <div
110
+ className="text-[11px] font-medium"
111
+ style={{ color: 'var(--text-primary)' }}
112
+ >
113
+ Project folder{' '}
114
+ <span style={{ color: 'var(--text-muted)', fontWeight: 'normal' }}>
115
+ (optional)
116
+ </span>
117
+ </div>
118
+ <p className="text-[10px]" style={{ color: 'var(--text-muted)' }}>
119
+ Select your project folder to detect framework, components, and CSS
120
+ strategy for smarter changelog export.
121
+ </p>
122
+ <ProjectRootSelector
123
+ targetUrl={targetUrl}
124
+ onSaved={handleProjectRootSaved}
125
+ />
126
+ </div>
127
+ </div>
128
+ )
129
+ }
130
+
131
+ // ─── Local mode ───
132
+
133
+ // Still checking CLI on first load
134
+ if (checking && cliAvailable === null) {
135
+ return (
136
+ <div className="flex flex-col items-center justify-center py-12 gap-3">
137
+ <div
138
+ className="w-5 h-5 border-2 rounded-full animate-spin"
139
+ style={{
140
+ borderColor: 'var(--border)',
141
+ borderTopColor: 'var(--accent)',
142
+ }}
143
+ />
144
+ <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
145
+ Detecting Claude Code CLI...
146
+ </span>
147
+ </div>
148
+ )
149
+ }
150
+
151
+ // CLI not found — show concise troubleshooting (not installation wizard)
152
+ if (cliAvailable === false) {
153
+ return (
154
+ <div className="flex flex-col gap-4 p-4">
155
+ <div
156
+ className="text-xs font-medium"
157
+ style={{ color: 'var(--text-primary)' }}
158
+ >
159
+ Claude Code
160
+ </div>
161
+
162
+ <div
163
+ className="flex flex-col gap-2 px-3 py-2.5 rounded"
164
+ style={{
165
+ background: 'rgba(244, 71, 71, 0.08)',
166
+ border: '1px solid var(--error)',
167
+ }}
168
+ >
169
+ <div
170
+ className="text-[11px] font-medium"
171
+ style={{ color: 'var(--error)' }}
172
+ >
173
+ CLI not detected
174
+ </div>
175
+ <p
176
+ className="text-[11px] leading-relaxed"
177
+ style={{ color: 'var(--text-secondary)' }}
178
+ >
179
+ The Claude Code CLI couldn&apos;t be found on this machine. If
180
+ it&apos;s already installed, the server process may not have access
181
+ to your shell PATH.
182
+ </p>
183
+ {checkError && (
184
+ <code
185
+ className="block px-2 py-1 rounded text-[10px] font-mono break-all"
186
+ style={{
187
+ background: 'var(--bg-primary)',
188
+ color: 'var(--text-muted)',
189
+ }}
190
+ >
191
+ {checkError}
192
+ </code>
193
+ )}
194
+ </div>
195
+
196
+ <div
197
+ className="flex flex-col gap-2 px-3 py-2.5 rounded"
198
+ style={{
199
+ background: 'var(--bg-tertiary)',
200
+ border: '1px solid var(--border)',
201
+ }}
202
+ >
203
+ <div
204
+ className="text-[11px] font-medium"
205
+ style={{ color: 'var(--text-primary)' }}
206
+ >
207
+ Troubleshooting
208
+ </div>
209
+ <ul
210
+ className="flex flex-col gap-1.5 text-[10px] list-none m-0 p-0"
211
+ style={{ color: 'var(--text-secondary)' }}
212
+ >
213
+ <li className="flex gap-1.5">
214
+ <span style={{ color: 'var(--text-muted)' }}>&bull;</span>
215
+ <span>
216
+ Run{' '}
217
+ <code
218
+ className="font-mono px-1 rounded"
219
+ style={{ background: 'var(--bg-primary)' }}
220
+ >
221
+ which claude
222
+ </code>{' '}
223
+ in your terminal to verify it&apos;s installed
224
+ </span>
225
+ </li>
226
+ <li className="flex gap-1.5">
227
+ <span style={{ color: 'var(--text-muted)' }}>&bull;</span>
228
+ <span>Restart pAInt after installing the CLI</span>
229
+ </li>
230
+ <li className="flex gap-1.5">
231
+ <span style={{ color: 'var(--text-muted)' }}>&bull;</span>
232
+ <span>
233
+ Install with:{' '}
234
+ <code
235
+ className="font-mono px-1 rounded"
236
+ style={{ background: 'var(--bg-primary)' }}
237
+ >
238
+ npm install -g @anthropic-ai/claude-code
239
+ </code>
240
+ </span>
241
+ </li>
242
+ </ul>
243
+ </div>
244
+
245
+ <button
246
+ onClick={checkCli}
247
+ disabled={checking}
248
+ className="w-full py-1.5 px-3 rounded text-xs font-medium transition-colors disabled:opacity-50"
249
+ style={{ background: 'var(--accent)', color: '#fff' }}
250
+ >
251
+ {checking ? 'Checking...' : 'Retry Detection'}
252
+ </button>
253
+ </div>
254
+ )
255
+ }
256
+
257
+ // CLI found — just ask for project root
258
+ return (
259
+ <div className="flex flex-col gap-4 p-4">
260
+ <div
261
+ className="text-xs font-medium"
262
+ style={{ color: 'var(--text-primary)' }}
263
+ >
264
+ Claude Code
265
+ </div>
266
+
267
+ {/* CLI detected badge */}
268
+ <div
269
+ className="flex items-center gap-2 px-3 py-2 rounded"
270
+ style={{
271
+ background: 'rgba(78, 201, 176, 0.08)',
272
+ border: '1px solid var(--success)',
273
+ }}
274
+ >
275
+ <span style={{ color: 'var(--success)' }}>&#10003;</span>
276
+ <span className="text-[11px]" style={{ color: 'var(--success)' }}>
277
+ CLI detected{cliVersion ? ` — ${cliVersion}` : ''}
278
+ </span>
279
+ </div>
280
+
281
+ {/* Project root setup */}
282
+ <div className="flex flex-col gap-2">
283
+ <div
284
+ className="text-[11px] font-medium"
285
+ style={{ color: 'var(--text-primary)' }}
286
+ >
287
+ Set project root for{' '}
288
+ <span className="font-mono" style={{ color: 'var(--accent)' }}>
289
+ {(() => {
290
+ try {
291
+ return new URL(targetUrl).host
292
+ } catch {
293
+ return targetUrl
294
+ }
295
+ })()}
296
+ </span>
297
+ </div>
298
+ <p className="text-[10px]" style={{ color: 'var(--text-muted)' }}>
299
+ Enter the absolute path to the project Claude will analyze. Run{' '}
300
+ <code
301
+ className="font-mono px-1 rounded"
302
+ style={{ background: 'var(--bg-tertiary)' }}
303
+ >
304
+ pwd
305
+ </code>{' '}
306
+ in your project directory to get it.
307
+ </p>
308
+ <ProjectRootSelector
309
+ targetUrl={targetUrl}
310
+ onSaved={handleProjectRootSaved}
311
+ />
312
+ </div>
313
+ </div>
314
+ )
315
+ }
@@ -0,0 +1,209 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect, useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import type { ConsoleLevel } from '@/types/messages'
6
+
7
+ const LEVEL_CONFIG: Record<
8
+ ConsoleLevel,
9
+ { icon: string; bgTint: string; color: string }
10
+ > = {
11
+ error: {
12
+ icon: '\u2718',
13
+ bgTint: 'rgba(248,113,113,0.1)',
14
+ color: 'var(--error)',
15
+ },
16
+ warn: {
17
+ icon: '\u26A0',
18
+ bgTint: 'rgba(251,191,36,0.1)',
19
+ color: 'var(--warning)',
20
+ },
21
+ info: {
22
+ icon: '\u24D8',
23
+ bgTint: 'rgba(74,158,255,0.08)',
24
+ color: 'var(--accent)',
25
+ },
26
+ log: {
27
+ icon: '\u25CB',
28
+ bgTint: 'transparent',
29
+ color: 'var(--text-secondary)',
30
+ },
31
+ }
32
+
33
+ type FilterLevel = 'all' | ConsoleLevel
34
+
35
+ function formatTime(ts: number): string {
36
+ const d = new Date(ts)
37
+ return d.toLocaleTimeString('en-US', {
38
+ hour12: false,
39
+ hour: '2-digit',
40
+ minute: '2-digit',
41
+ second: '2-digit',
42
+ })
43
+ }
44
+
45
+ export function ConsolePanel() {
46
+ const entries = useEditorStore((s) => s.consoleEntries)
47
+ const clearConsole = useEditorStore((s) => s.clearConsole)
48
+ const [filter, setFilter] = useState<FilterLevel>('all')
49
+ const listRef = useRef<HTMLDivElement>(null)
50
+ const userScrolledUp = useRef(false)
51
+
52
+ const filtered =
53
+ filter === 'all' ? entries : entries.filter((e) => e.level === filter)
54
+ const errorCount = entries.filter((e) => e.level === 'error').length
55
+
56
+ // Auto-scroll to bottom unless user scrolled up
57
+ useEffect(() => {
58
+ if (!userScrolledUp.current && listRef.current) {
59
+ listRef.current.scrollTop = listRef.current.scrollHeight
60
+ }
61
+ }, [filtered.length])
62
+
63
+ const handleScroll = useCallback(() => {
64
+ const el = listRef.current
65
+ if (!el) return
66
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 30
67
+ userScrolledUp.current = !atBottom
68
+ }, [])
69
+
70
+ const copyErrors = useCallback(() => {
71
+ const errors = entries.filter((e) => e.level === 'error')
72
+ if (errors.length === 0) return
73
+ const text = errors
74
+ .map((e) => {
75
+ const ts = new Date(e.timestamp).toISOString()
76
+ const loc = e.source
77
+ ? ` (${e.source}${e.line != null ? ':' + e.line : ''}${e.column != null ? ':' + e.column : ''})`
78
+ : ''
79
+ return `[${ts}] ERROR${loc}: ${e.args.join(' ')}`
80
+ })
81
+ .join('\n')
82
+ navigator.clipboard.writeText(text)
83
+ }, [entries])
84
+
85
+ const filters: { id: FilterLevel; label: string }[] = [
86
+ { id: 'all', label: 'All' },
87
+ { id: 'error', label: 'Errors' },
88
+ { id: 'warn', label: 'Warnings' },
89
+ { id: 'info', label: 'Info' },
90
+ { id: 'log', label: 'Logs' },
91
+ ]
92
+
93
+ return (
94
+ <div
95
+ className="flex flex-col h-full"
96
+ style={{ color: 'var(--text-primary)' }}
97
+ >
98
+ {/* Header */}
99
+ <div
100
+ className="flex items-center justify-between px-3 py-1.5 flex-shrink-0"
101
+ style={{ borderBottom: '1px solid var(--border)' }}
102
+ >
103
+ <span className="text-xs" style={{ color: 'var(--text-secondary)' }}>
104
+ {filtered.length} message{filtered.length !== 1 ? 's' : ''}
105
+ </span>
106
+ <div className="flex items-center gap-1.5">
107
+ {errorCount > 0 && (
108
+ <button
109
+ onClick={copyErrors}
110
+ className="px-2 py-0.5 rounded text-[10px] font-medium"
111
+ style={{ background: 'var(--error)', color: '#fff' }}
112
+ >
113
+ Copy Errors ({errorCount})
114
+ </button>
115
+ )}
116
+ <button
117
+ onClick={clearConsole}
118
+ className="px-2 py-0.5 rounded text-[10px] font-medium"
119
+ style={{
120
+ background: 'var(--bg-hover)',
121
+ color: 'var(--text-secondary)',
122
+ }}
123
+ >
124
+ Clear
125
+ </button>
126
+ </div>
127
+ </div>
128
+
129
+ {/* Filter bar */}
130
+ <div
131
+ className="flex items-center gap-1 px-3 py-1.5 flex-shrink-0"
132
+ style={{ borderBottom: '1px solid var(--border)' }}
133
+ >
134
+ {filters.map((f) => (
135
+ <button
136
+ key={f.id}
137
+ onClick={() => setFilter(f.id)}
138
+ className="px-2 py-0.5 rounded-full text-[10px] font-medium transition-colors"
139
+ style={{
140
+ background: filter === f.id ? 'var(--accent)' : 'var(--bg-input)',
141
+ color: filter === f.id ? '#fff' : 'var(--text-secondary)',
142
+ }}
143
+ >
144
+ {f.label}
145
+ </button>
146
+ ))}
147
+ </div>
148
+
149
+ {/* Message list */}
150
+ <div
151
+ ref={listRef}
152
+ onScroll={handleScroll}
153
+ className="flex-1 overflow-y-auto"
154
+ >
155
+ {filtered.length === 0 ? (
156
+ <div className="flex items-center justify-center h-full">
157
+ <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
158
+ No console messages captured
159
+ </span>
160
+ </div>
161
+ ) : (
162
+ filtered.map((entry) => {
163
+ const cfg = LEVEL_CONFIG[entry.level]
164
+ return (
165
+ <div
166
+ key={entry.id}
167
+ className="flex items-start gap-2 px-3 py-1 text-[11px]"
168
+ style={{
169
+ background: cfg.bgTint,
170
+ borderBottom: '1px solid var(--border)',
171
+ fontFamily: 'monospace',
172
+ }}
173
+ >
174
+ <span
175
+ style={{
176
+ color: cfg.color,
177
+ flexShrink: 0,
178
+ width: 14,
179
+ textAlign: 'center',
180
+ }}
181
+ >
182
+ {cfg.icon}
183
+ </span>
184
+ <span
185
+ className="flex-1 break-all"
186
+ style={{ color: 'var(--text-primary)' }}
187
+ >
188
+ {entry.args.join(' ')}
189
+ </span>
190
+ <span className="flex-shrink-0 flex flex-col items-end gap-0.5">
191
+ <span style={{ color: 'var(--text-muted)', fontSize: 10 }}>
192
+ {formatTime(entry.timestamp)}
193
+ </span>
194
+ {entry.source && (
195
+ <span style={{ color: 'var(--text-muted)', fontSize: 9 }}>
196
+ {entry.source.split('/').pop()}
197
+ {entry.line != null ? ':' + entry.line : ''}
198
+ {entry.column != null ? ':' + entry.column : ''}
199
+ </span>
200
+ )}
201
+ </span>
202
+ </div>
203
+ )
204
+ })
205
+ )}
206
+ </div>
207
+ </div>
208
+ )
209
+ }