@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,217 @@
1
+ 'use client'
2
+
3
+ import { useRef, useEffect, useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { ScanOverlay } from './ScanOverlay'
6
+ import '@xterm/xterm/css/xterm.css'
7
+
8
+ export function TerminalPanel() {
9
+ const containerRef = useRef<HTMLDivElement>(null)
10
+ const termRef = useRef<import('@xterm/xterm').Terminal | null>(null)
11
+ const fitRef = useRef<import('@xterm/addon-fit').FitAddon | null>(null)
12
+ const wsRef = useRef<WebSocket | null>(null)
13
+ const initRef = useRef(false)
14
+ const terminalStatus = useEditorStore((s) => s.terminalStatus)
15
+ const setTerminalStatus = useEditorStore((s) => s.setTerminalStatus)
16
+ const port = useEditorStore((s) => s.terminalServerPort)
17
+ const registerTerminalWriter = useEditorStore((s) => s.registerTerminalWriter)
18
+
19
+ const connectWebSocket = useCallback(
20
+ (
21
+ term: import('@xterm/xterm').Terminal,
22
+ fitAddon: import('@xterm/addon-fit').FitAddon,
23
+ ) => {
24
+ setTerminalStatus('connecting')
25
+ const ws = new WebSocket(`ws://localhost:${port}/ws`)
26
+ wsRef.current = ws
27
+
28
+ ws.onopen = () => {
29
+ setTerminalStatus('connected')
30
+ fitAddon.fit()
31
+ const dims = fitAddon.proposeDimensions()
32
+ if (dims) {
33
+ ws.send('\x01' + JSON.stringify({ cols: dims.cols, rows: dims.rows }))
34
+ }
35
+ }
36
+
37
+ ws.onmessage = (event) => {
38
+ term.write(event.data)
39
+ }
40
+
41
+ ws.onclose = () => {
42
+ setTerminalStatus('disconnected')
43
+ }
44
+
45
+ ws.onerror = () => {
46
+ setTerminalStatus('error')
47
+ }
48
+
49
+ term.onData((data: string) => {
50
+ if (ws.readyState === WebSocket.OPEN) {
51
+ ws.send(data)
52
+ }
53
+ })
54
+
55
+ term.onResize((size: { cols: number; rows: number }) => {
56
+ if (ws.readyState === WebSocket.OPEN) {
57
+ ws.send('\x01' + JSON.stringify({ cols: size.cols, rows: size.rows }))
58
+ }
59
+ })
60
+ },
61
+ [port, setTerminalStatus],
62
+ )
63
+
64
+ useEffect(() => {
65
+ if (initRef.current) return
66
+ initRef.current = true
67
+ let mounted = true
68
+
69
+ async function init() {
70
+ const [{ Terminal }, { FitAddon }] = await Promise.all([
71
+ import('@xterm/xterm'),
72
+ import('@xterm/addon-fit'),
73
+ ])
74
+
75
+ if (!mounted || !containerRef.current) return
76
+
77
+ const fitAddon = new FitAddon()
78
+ fitRef.current = fitAddon
79
+
80
+ const term = new Terminal({
81
+ theme: {
82
+ background: '#1e1e1e',
83
+ foreground: '#e0e0e0',
84
+ cursor: '#4a9eff',
85
+ selectionBackground: '#4a9eff44',
86
+ black: '#1e1e1e',
87
+ red: '#f87171',
88
+ green: '#4ade80',
89
+ yellow: '#fbbf24',
90
+ blue: '#4a9eff',
91
+ magenta: '#c084fc',
92
+ cyan: '#22d3ee',
93
+ white: '#e0e0e0',
94
+ brightBlack: '#666666',
95
+ brightRed: '#f87171',
96
+ brightGreen: '#4ade80',
97
+ brightYellow: '#fbbf24',
98
+ brightBlue: '#4a9eff',
99
+ brightMagenta: '#c084fc',
100
+ brightCyan: '#22d3ee',
101
+ brightWhite: '#ffffff',
102
+ },
103
+ fontSize: 12,
104
+ fontFamily: 'Menlo, Monaco, "Courier New", monospace',
105
+ cursorBlink: true,
106
+ scrollback: 1000,
107
+ })
108
+
109
+ term.loadAddon(fitAddon)
110
+ term.open(containerRef.current)
111
+ fitAddon.fit()
112
+
113
+ termRef.current = term
114
+ registerTerminalWriter((data: string) => term.write(data))
115
+ connectWebSocket(term, fitAddon)
116
+ }
117
+
118
+ init()
119
+
120
+ return () => {
121
+ mounted = false
122
+ registerTerminalWriter(null)
123
+ wsRef.current?.close()
124
+ termRef.current?.dispose()
125
+ }
126
+ }, [connectWebSocket, registerTerminalWriter])
127
+
128
+ // Refit terminal when container resizes
129
+ useEffect(() => {
130
+ const el = containerRef.current
131
+ if (!el) return
132
+
133
+ const observer = new ResizeObserver(() => {
134
+ if (fitRef.current && termRef.current) {
135
+ fitRef.current.fit()
136
+ }
137
+ })
138
+
139
+ observer.observe(el)
140
+ return () => observer.disconnect()
141
+ }, [])
142
+
143
+ const reconnect = useCallback(() => {
144
+ if (termRef.current && fitRef.current) {
145
+ wsRef.current?.close()
146
+ termRef.current.clear()
147
+ connectWebSocket(termRef.current, fitRef.current)
148
+ }
149
+ }, [connectWebSocket])
150
+
151
+ return (
152
+ <div className="relative flex flex-col h-full">
153
+ {/* Header */}
154
+ <div
155
+ className="flex items-center justify-between px-3 py-1 flex-shrink-0"
156
+ style={{ borderBottom: '1px solid var(--border)' }}
157
+ >
158
+ <div className="flex items-center gap-1.5">
159
+ <span
160
+ className="w-1.5 h-1.5 rounded-full"
161
+ style={{
162
+ background:
163
+ terminalStatus === 'connected'
164
+ ? 'var(--success)'
165
+ : terminalStatus === 'error'
166
+ ? 'var(--error)'
167
+ : 'var(--text-muted)',
168
+ }}
169
+ />
170
+ <span
171
+ className="text-[11px]"
172
+ style={{ color: 'var(--text-secondary)' }}
173
+ >
174
+ Terminal
175
+ </span>
176
+ </div>
177
+ {(terminalStatus === 'disconnected' || terminalStatus === 'error') && (
178
+ <button
179
+ onClick={reconnect}
180
+ className="px-2 py-0.5 rounded text-[10px] font-medium"
181
+ style={{
182
+ background: 'var(--bg-hover)',
183
+ color: 'var(--text-secondary)',
184
+ }}
185
+ >
186
+ Reconnect
187
+ </button>
188
+ )}
189
+ </div>
190
+
191
+ {/* Terminal container */}
192
+ <div
193
+ ref={containerRef}
194
+ className="flex-1 overflow-hidden"
195
+ style={{ padding: '4px 0 0 4px' }}
196
+ />
197
+
198
+ {/* Error state overlay */}
199
+ {terminalStatus === 'error' && (
200
+ <div
201
+ className="absolute inset-0 flex flex-col items-center justify-center gap-2 pointer-events-none"
202
+ style={{ background: 'rgba(30,30,30,0.85)' }}
203
+ >
204
+ <span className="text-xs" style={{ color: 'var(--error)' }}>
205
+ Cannot connect to terminal server
206
+ </span>
207
+ <span className="text-[10px]" style={{ color: 'var(--text-muted)' }}>
208
+ Run: bun run terminal
209
+ </span>
210
+ </div>
211
+ )}
212
+
213
+ {/* AI Scan animation overlay */}
214
+ <ScanOverlay />
215
+ </div>
216
+ )
217
+ }
@@ -0,0 +1,248 @@
1
+ 'use client'
2
+
3
+ import { useMemo, useState, useCallback, useRef } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import {
6
+ buildInstructionsFooter,
7
+ BREAKPOINTS,
8
+ getBreakpointDeviceInfo,
9
+ getBreakpointRange,
10
+ } from '@/lib/constants'
11
+ import { EditablePre } from '@/components/common/EditablePre'
12
+ import type { Breakpoint } from '@/types/changelog'
13
+
14
+ function CopyIcon({ size = 14 }: { size?: number }) {
15
+ return (
16
+ <svg
17
+ width={size}
18
+ height={size}
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ strokeWidth={2}
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ >
26
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
27
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
28
+ </svg>
29
+ )
30
+ }
31
+
32
+ function CheckIcon({ size = 14 }: { size?: number }) {
33
+ return (
34
+ <svg
35
+ width={size}
36
+ height={size}
37
+ viewBox="0 0 24 24"
38
+ fill="none"
39
+ stroke="currentColor"
40
+ strokeWidth={2}
41
+ strokeLinecap="round"
42
+ strokeLinejoin="round"
43
+ >
44
+ <polyline points="20 6 9 17 4 12" />
45
+ </svg>
46
+ )
47
+ }
48
+
49
+ function buildElementLogText(opts: {
50
+ tagName: string | null
51
+ className: string | null
52
+ elementId: string | null
53
+ selectorPath: string | null
54
+ attributes: Record<string, string>
55
+ innerText: string | null
56
+ computedStyles: Record<string, string>
57
+ pagePath: string
58
+ changeScope: 'all' | 'breakpoint-only'
59
+ activeBreakpoint: Breakpoint
60
+ changeCount: number
61
+ framework?: string | null
62
+ cssStrategy?: string[] | null
63
+ }): string {
64
+ const lines: string[] = []
65
+ if (!opts.tagName) return ''
66
+
67
+ const attrParts: string[] = []
68
+ if (opts.elementId) attrParts.push(`id="${opts.elementId}"`)
69
+ if (opts.className) attrParts.push(`class="${opts.className}"`)
70
+ const tag = `<${opts.tagName}${attrParts.length ? ' ' + attrParts.join(' ') : ''}>`
71
+
72
+ const isMobileApp =
73
+ opts.framework === 'flutter' || opts.framework === 'react-native'
74
+ const { deviceName, range } = getBreakpointDeviceInfo(opts.activeBreakpoint)
75
+
76
+ lines.push('PAGE NAME')
77
+ lines.push(opts.pagePath || '/')
78
+ lines.push('')
79
+
80
+ lines.push('ELEMENT')
81
+ lines.push(tag)
82
+ lines.push('')
83
+
84
+ if (!isMobileApp) {
85
+ lines.push('DEVICE')
86
+ lines.push(`Device Name: ${deviceName}`)
87
+ lines.push(`Breakpoint: ${range}`)
88
+ lines.push('')
89
+
90
+ lines.push('APPLIES TO')
91
+ lines.push(
92
+ opts.changeScope === 'all'
93
+ ? 'All breakpoints'
94
+ : `${deviceName} (${range})`,
95
+ )
96
+ lines.push('')
97
+ }
98
+
99
+ const attrEntries = Object.entries(opts.attributes)
100
+ if (attrEntries.length > 0) {
101
+ lines.push('ATTRIBUTES')
102
+ for (const [key, value] of attrEntries) {
103
+ lines.push(` ${key}: ${value}`)
104
+ }
105
+ lines.push('')
106
+ }
107
+
108
+ if (opts.innerText) {
109
+ lines.push('INNER TEXT')
110
+ lines.push(opts.innerText)
111
+ lines.push('')
112
+ }
113
+
114
+ lines.push(
115
+ buildInstructionsFooter(opts.changeCount, 1, {
116
+ framework: opts.framework,
117
+ cssStrategy: opts.cssStrategy,
118
+ }),
119
+ )
120
+
121
+ return lines.join('\n').trim()
122
+ }
123
+
124
+ export function ElementLogBox() {
125
+ const [copied, setCopied] = useState(false)
126
+ const editedTextRef = useRef<string | null>(null)
127
+
128
+ const tagName = useEditorStore((s) => s.tagName)
129
+ const className = useEditorStore((s) => s.className)
130
+ const elementId = useEditorStore((s) => s.elementId)
131
+ const selectorPath = useEditorStore((s) => s.selectorPath)
132
+ const attributes = useEditorStore((s) => s.attributes)
133
+ const innerText = useEditorStore((s) => s.innerText)
134
+ const computedStyles = useEditorStore((s) => s.computedStyles)
135
+ const currentPagePath = useEditorStore((s) => s.currentPagePath)
136
+ const changeScope = useEditorStore((s) => s.changeScope)
137
+ const activeBreakpoint = useEditorStore((s) => s.activeBreakpoint)
138
+ const styleChanges = useEditorStore((s) => s.styleChanges)
139
+ const targetUrl = useEditorStore((s) => s.targetUrl)
140
+ const getProjectScanForUrl = useEditorStore((s) => s.getProjectScanForUrl)
141
+
142
+ const projectScan = useMemo(() => {
143
+ return getProjectScanForUrl(targetUrl)
144
+ }, [targetUrl, getProjectScanForUrl])
145
+
146
+ const framework = projectScan?.framework ?? null
147
+ const cssStrategy = projectScan?.cssStrategy ?? null
148
+
149
+ const changeCount = useMemo(() => {
150
+ if (!selectorPath) return 0
151
+ return styleChanges.filter((c) => c.elementSelector === selectorPath).length
152
+ }, [styleChanges, selectorPath])
153
+
154
+ const logText = useMemo(
155
+ () =>
156
+ buildElementLogText({
157
+ tagName,
158
+ className,
159
+ elementId,
160
+ selectorPath,
161
+ attributes,
162
+ innerText,
163
+ computedStyles,
164
+ pagePath: currentPagePath,
165
+ changeScope,
166
+ activeBreakpoint,
167
+ changeCount,
168
+ framework,
169
+ cssStrategy,
170
+ }),
171
+ [
172
+ tagName,
173
+ className,
174
+ elementId,
175
+ selectorPath,
176
+ attributes,
177
+ innerText,
178
+ computedStyles,
179
+ currentPagePath,
180
+ changeScope,
181
+ activeBreakpoint,
182
+ changeCount,
183
+ framework,
184
+ cssStrategy,
185
+ ],
186
+ )
187
+
188
+ const handleTextChange = useCallback(
189
+ (edited: string) => {
190
+ editedTextRef.current = edited === logText ? null : edited
191
+ },
192
+ [logText],
193
+ )
194
+
195
+ const handleCopy = useCallback(async () => {
196
+ const textToCopy = editedTextRef.current ?? logText
197
+ if (!textToCopy) return
198
+ try {
199
+ await navigator.clipboard.writeText(textToCopy)
200
+ } catch {
201
+ const textarea = document.createElement('textarea')
202
+ textarea.value = textToCopy
203
+ textarea.style.position = 'fixed'
204
+ textarea.style.opacity = '0'
205
+ document.body.appendChild(textarea)
206
+ textarea.select()
207
+ document.execCommand('copy')
208
+ document.body.removeChild(textarea)
209
+ }
210
+ setCopied(true)
211
+ setTimeout(() => setCopied(false), 2000)
212
+ }, [logText])
213
+
214
+ if (!tagName) return null
215
+
216
+ return (
217
+ <div>
218
+ <div className="flex items-center justify-between px-3 py-1.5">
219
+ <span
220
+ className="text-xs font-medium"
221
+ style={{ color: 'var(--text-secondary)' }}
222
+ >
223
+ Element Info
224
+ </span>
225
+ <button
226
+ onClick={handleCopy}
227
+ className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] transition-colors"
228
+ style={{
229
+ color: copied ? 'var(--success)' : 'var(--text-muted)',
230
+ background: 'transparent',
231
+ }}
232
+ title="Copy to clipboard"
233
+ >
234
+ {copied ? <CheckIcon size={11} /> : <CopyIcon size={11} />}
235
+ {copied ? 'Copied' : 'Copy'}
236
+ </button>
237
+ </div>
238
+ <div className="px-3 pb-3">
239
+ <EditablePre
240
+ text={logText}
241
+ onTextChange={handleTextChange}
242
+ className="text-[11px] font-mono whitespace-pre-wrap break-words leading-relaxed"
243
+ style={{ color: 'var(--text-muted)' }}
244
+ />
245
+ </div>
246
+ </div>
247
+ )
248
+ }
@@ -0,0 +1,83 @@
1
+ 'use client'
2
+
3
+ import { useEditorStore } from '@/store'
4
+
5
+ export function PanelTabs() {
6
+ const activeRightTab = useEditorStore((s) => s.activeRightTab)
7
+ const setActiveRightTab = useEditorStore((s) => s.setActiveRightTab)
8
+ const activeBreakpoint = useEditorStore((s) => s.activeBreakpoint)
9
+ const changeCount = useEditorStore(
10
+ (s) =>
11
+ s.styleChanges.filter((c) => c.breakpoint === activeBreakpoint).length,
12
+ )
13
+ const consoleErrorCount = useEditorStore((s) => s.consoleErrorCount)
14
+
15
+ const varCount = useEditorStore(
16
+ (s) => Object.keys(s.cssVariableDefinitions).length,
17
+ )
18
+
19
+ const tabs: Array<{
20
+ id: 'design' | 'variables' | 'changes' | 'claude' | 'console'
21
+ label: string
22
+ }> = [
23
+ { id: 'design', label: 'Design' },
24
+ { id: 'variables', label: 'Variables' },
25
+ { id: 'changes', label: 'Changes' },
26
+ // { id: 'claude', label: 'Claude' },
27
+ { id: 'console', label: 'Console' },
28
+ ]
29
+
30
+ return (
31
+ <div
32
+ className="flex items-center h-8 flex-shrink-0"
33
+ style={{ borderBottom: '1px solid var(--border)' }}
34
+ >
35
+ {tabs.map((tab) => (
36
+ <button
37
+ key={tab.id}
38
+ onClick={() => setActiveRightTab(tab.id)}
39
+ className="flex items-center gap-1.5 px-3 h-full text-xs font-medium transition-colors relative"
40
+ style={{
41
+ color:
42
+ activeRightTab === tab.id
43
+ ? 'var(--text-primary)'
44
+ : 'var(--text-muted)',
45
+ background: 'transparent',
46
+ }}
47
+ >
48
+ {tab.label}
49
+ {tab.id === 'variables' && varCount > 0 && (
50
+ <span
51
+ className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full text-[10px] font-medium"
52
+ style={{ background: 'var(--text-muted)', color: '#fff' }}
53
+ >
54
+ {varCount}
55
+ </span>
56
+ )}
57
+ {tab.id === 'changes' && changeCount > 0 && (
58
+ <span
59
+ className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full text-[10px] font-medium"
60
+ style={{ background: 'var(--accent)', color: '#fff' }}
61
+ >
62
+ {changeCount}
63
+ </span>
64
+ )}
65
+ {tab.id === 'console' && consoleErrorCount > 0 && (
66
+ <span
67
+ className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full text-[10px] font-medium"
68
+ style={{ background: 'var(--error)', color: '#fff' }}
69
+ >
70
+ {consoleErrorCount}
71
+ </span>
72
+ )}
73
+ {activeRightTab === tab.id && (
74
+ <div
75
+ className="absolute bottom-0 left-0 right-0 h-0.5"
76
+ style={{ background: 'var(--accent)' }}
77
+ />
78
+ )}
79
+ </button>
80
+ ))}
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,41 @@
1
+ 'use client'
2
+
3
+ import { ResizablePanel } from '@/components/common/ResizablePanel'
4
+ import { PanelTabs } from './PanelTabs'
5
+ import { DesignPanel } from './design/DesignPanel'
6
+ import { ChangesPanel } from './changes/ChangesPanel'
7
+ import { ClaudeIntegrationPanel } from './claude/ClaudeIntegrationPanel'
8
+ import { ConsolePanel } from './console/ConsolePanel'
9
+ import { VariablesPanel } from './variables/VariablesPanel'
10
+ import { useEditorStore } from '@/store'
11
+ import { PANEL_DEFAULTS } from '@/lib/constants'
12
+
13
+ interface RightPanelProps {
14
+ width: number
15
+ }
16
+
17
+ export function RightPanel({ width }: RightPanelProps) {
18
+ const setRightPanelWidth = useEditorStore((s) => s.setRightPanelWidth)
19
+ const activeRightTab = useEditorStore((s) => s.activeRightTab)
20
+
21
+ return (
22
+ <ResizablePanel
23
+ width={width}
24
+ minWidth={PANEL_DEFAULTS.rightMin}
25
+ maxWidth={PANEL_DEFAULTS.rightMax}
26
+ onResize={setRightPanelWidth}
27
+ side="right"
28
+ >
29
+ <div className="flex flex-col h-full">
30
+ <PanelTabs />
31
+ <div className="flex-1 overflow-y-auto">
32
+ {activeRightTab === 'design' && <DesignPanel />}
33
+ {activeRightTab === 'variables' && <VariablesPanel />}
34
+ {activeRightTab === 'changes' && <ChangesPanel />}
35
+ {activeRightTab === 'claude' && <ClaudeIntegrationPanel />}
36
+ {activeRightTab === 'console' && <ConsolePanel />}
37
+ </div>
38
+ </div>
39
+ </ResizablePanel>
40
+ )
41
+ }