@antigenic-oss/paint 0.2.0 → 0.2.1
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/README.md +32 -17
- package/bin/bridge-server.js +36 -0
- package/bin/paint.js +542 -102
- package/bin/terminal-server.js +105 -0
- package/package.json +7 -8
- package/public/dev-editor-inspector.js +92 -104
- package/src/app/api/claude/apply/route.ts +2 -2
- package/src/app/api/project/scan/route.ts +1 -1
- package/src/app/api/proxy/[[...path]]/route.ts +4 -4
- package/src/app/docs/DocsClient.tsx +1 -1
- package/src/app/docs/page.tsx +0 -1
- package/src/app/page.tsx +1 -1
- package/src/bridge/api-handlers.ts +1 -1
- package/src/bridge/proxy-handler.ts +4 -4
- package/src/bridge/server.ts +135 -39
- package/src/components/ConnectModal.tsx +1 -2
- package/src/components/PreviewFrame.tsx +2 -2
- package/src/components/ResponsiveToolbar.tsx +1 -2
- package/src/components/common/ColorPicker.tsx +7 -9
- package/src/components/common/UnitInput.tsx +1 -1
- package/src/components/common/VariableColorPicker.tsx +0 -1
- package/src/components/left-panel/ComponentsPanel.tsx +3 -3
- package/src/components/left-panel/LayerNode.tsx +1 -1
- package/src/components/left-panel/icons.tsx +1 -1
- package/src/components/left-panel/terminal/TerminalPanel.tsx +2 -2
- package/src/components/right-panel/ElementLogBox.tsx +1 -3
- package/src/components/right-panel/changes/ChangesPanel.tsx +12 -12
- package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +2 -2
- package/src/components/right-panel/claude/DiffViewer.tsx +1 -1
- package/src/components/right-panel/claude/ProjectRootSelector.tsx +7 -7
- package/src/components/right-panel/claude/SetupFlow.tsx +1 -1
- package/src/components/right-panel/console/ConsolePanel.tsx +4 -4
- package/src/components/right-panel/design/BackgroundSection.tsx +2 -2
- package/src/components/right-panel/design/GradientEditor.tsx +6 -6
- package/src/components/right-panel/design/LayoutSection.tsx +4 -4
- package/src/components/right-panel/design/PositionSection.tsx +2 -2
- package/src/components/right-panel/design/SVGSection.tsx +2 -3
- package/src/components/right-panel/design/ShadowBlurSection.tsx +5 -5
- package/src/components/right-panel/design/TextSection.tsx +5 -5
- package/src/components/right-panel/design/icons.tsx +1 -1
- package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +2 -2
- package/src/components/right-panel/design/inputs/CompactInput.tsx +2 -2
- package/src/components/right-panel/design/inputs/DraggableLabel.tsx +2 -1
- package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +1 -1
- package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +3 -3
- package/src/components/right-panel/design/inputs/SectionHeader.tsx +2 -1
- package/src/hooks/useDOMTree.ts +0 -1
- package/src/hooks/usePostMessage.ts +4 -3
- package/src/hooks/useTargetUrl.ts +1 -1
- package/src/inspector/DOMTraverser.ts +2 -2
- package/src/inspector/HoverHighlighter.ts +6 -6
- package/src/inspector/SelectionHighlighter.ts +4 -4
- package/src/lib/classifyElement.ts +1 -2
- package/src/lib/claude-bin.ts +1 -1
- package/src/lib/clientProjectScanner.ts +13 -13
- package/src/lib/cssVariableUtils.ts +1 -1
- package/src/lib/folderPicker.ts +4 -1
- package/src/lib/projectScanner.ts +15 -15
- package/src/lib/tailwindClassParser.ts +1 -1
- package/src/lib/textShadowUtils.ts +1 -1
- package/src/lib/utils.ts +4 -4
- package/src/proxy.ts +1 -1
- package/src/store/treeSlice.ts +2 -2
- package/src/server/terminal-server.ts +0 -104
- package/tsconfig.server.json +0 -12
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
1
|
+
import { type NextRequest, NextResponse } from 'next/server'
|
|
2
2
|
import { PROXY_HEADER } from '@/lib/constants'
|
|
3
3
|
|
|
4
4
|
const INSPECTOR_SCRIPT = `
|
|
@@ -311,7 +311,7 @@ function getInspectorCode(): string {
|
|
|
311
311
|
// Show first meaningful class
|
|
312
312
|
var cls = el.className;
|
|
313
313
|
if (cls && typeof cls === 'string') {
|
|
314
|
-
var first = cls.trim().split(
|
|
314
|
+
var first = cls.trim().split(/s+/)[0];
|
|
315
315
|
if (first) return tag + '.' + first;
|
|
316
316
|
}
|
|
317
317
|
return tag;
|
|
@@ -1787,7 +1787,7 @@ async function handleProxy(request: NextRequest, params: { path?: string[] }) {
|
|
|
1787
1787
|
// need to make requests through the proxy
|
|
1788
1788
|
// 5. Suppresses HMR-related errors as a safety net
|
|
1789
1789
|
// 6. Detects infinite reload loops as a safety net
|
|
1790
|
-
const targetPagePath =
|
|
1790
|
+
const targetPagePath = `/${path || ''}`
|
|
1791
1791
|
const safePagePath = JSON.stringify(targetPagePath)
|
|
1792
1792
|
const safeTargetUrl = JSON.stringify(targetUrl)
|
|
1793
1793
|
const safeEncodedTarget = JSON.stringify(encodedTarget)
|
|
@@ -2243,7 +2243,7 @@ async function handleProxy(request: NextRequest, params: { path?: string[] }) {
|
|
|
2243
2243
|
|
|
2244
2244
|
// Inject inspector script before </body> (case-insensitive)
|
|
2245
2245
|
if (/<\/body>/i.test(html)) {
|
|
2246
|
-
html = html.replace(/<\/body>/i, () => INSPECTOR_SCRIPT
|
|
2246
|
+
html = html.replace(/<\/body>/i, () => `${INSPECTOR_SCRIPT}</body>`)
|
|
2247
2247
|
} else {
|
|
2248
2248
|
html += INSPECTOR_SCRIPT
|
|
2249
2249
|
}
|
|
@@ -33,7 +33,7 @@ export function Sidebar() {
|
|
|
33
33
|
document.getElementById(item.id),
|
|
34
34
|
).filter(Boolean) as HTMLElement[]
|
|
35
35
|
|
|
36
|
-
sections.forEach((el) => observerRef.current
|
|
36
|
+
sections.forEach((el) => observerRef.current?.observe(el))
|
|
37
37
|
|
|
38
38
|
return () => observerRef.current?.disconnect()
|
|
39
39
|
}, [])
|
package/src/app/docs/page.tsx
CHANGED
package/src/app/page.tsx
CHANGED
|
@@ -770,7 +770,7 @@ async function handleClaudeApply(
|
|
|
770
770
|
)
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
-
const combinedOutput = result.stdout
|
|
773
|
+
const combinedOutput = `${result.stdout}\n${result.stderr}`
|
|
774
774
|
const filesModified = extractModifiedFiles(combinedOutput)
|
|
775
775
|
const fileCount = filesModified.length
|
|
776
776
|
const summary =
|
|
@@ -137,13 +137,13 @@ function injectIntoHtml(html: string, inspectorScript: string | null): string {
|
|
|
137
137
|
|
|
138
138
|
// Inject navigation blocker in <head>, inspector before </body>
|
|
139
139
|
if (result.includes('</head>')) {
|
|
140
|
-
result = result.replace('</head>', navBlocker
|
|
140
|
+
result = result.replace('</head>', `${navBlocker}</head>`)
|
|
141
141
|
} else {
|
|
142
142
|
result = navBlocker + result
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
if (result.includes('</body>')) {
|
|
146
|
-
result = result.replace('</body>', inspectorTag
|
|
146
|
+
result = result.replace('</body>', `${inspectorTag}</body>`)
|
|
147
147
|
} else {
|
|
148
148
|
result = result + inspectorTag
|
|
149
149
|
}
|
|
@@ -208,7 +208,7 @@ export async function handleProxy(
|
|
|
208
208
|
|
|
209
209
|
// Build the target fetch URL
|
|
210
210
|
const targetOrigin = new URL(targetUrl).origin
|
|
211
|
-
const
|
|
211
|
+
const _fetchUrl = `${targetOrigin}${pathname}${url.search ? url.search.replace(new RegExp(`[?&]${PROXY_HEADER}=[^&]*`), '') : ''}`
|
|
212
212
|
|
|
213
213
|
// Strip the proxy header from the search params
|
|
214
214
|
const cleanSearch = url.search
|
|
@@ -305,7 +305,7 @@ export async function handleProxy(
|
|
|
305
305
|
// Image response — always revalidate so updated assets on the target
|
|
306
306
|
// are reflected immediately instead of being served from browser cache.
|
|
307
307
|
if (
|
|
308
|
-
(contentType
|
|
308
|
+
(contentType?.includes('image/')) ||
|
|
309
309
|
pathname.match(/\.(png|jpe?g|gif|svg|ico|webp|avif)(\?|$)/i)
|
|
310
310
|
) {
|
|
311
311
|
responseHeaders.set(
|
package/src/bridge/server.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pAInt Bridge Server
|
|
2
|
+
* pAInt Bridge Server (Node runtime)
|
|
3
3
|
*
|
|
4
|
-
* A lightweight
|
|
4
|
+
* A lightweight HTTP server that runs on the user's machine.
|
|
5
5
|
* When pAInt is deployed on Vercel, it connects to this bridge
|
|
6
6
|
* to proxy localhost pages, scan project directories, and run Claude CLI.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* bun run src/bridge/server.ts
|
|
10
|
-
* # or
|
|
11
|
-
* bun run bridge
|
|
12
7
|
*/
|
|
13
8
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
10
|
+
import {
|
|
11
|
+
createServer,
|
|
12
|
+
type IncomingMessage,
|
|
13
|
+
type ServerResponse,
|
|
14
|
+
} from 'node:http'
|
|
15
|
+
import { dirname, join } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
17
|
import { handleAPI } from './api-handlers'
|
|
18
|
+
import { handleProxy } from './proxy-handler'
|
|
18
19
|
|
|
19
20
|
const BRIDGE_PORT = Number(process.env.BRIDGE_PORT) || 4002
|
|
21
|
+
const BRIDGE_HOST = process.env.BRIDGE_HOST || '127.0.0.1'
|
|
20
22
|
|
|
21
23
|
const ALLOWED_ORIGIN_PATTERNS = [
|
|
22
24
|
'https://dev-editor-flow.vercel.app',
|
|
@@ -51,63 +53,157 @@ function corsHeaders(requestOrigin: string | null): Record<string, string> {
|
|
|
51
53
|
|
|
52
54
|
// Cache the inspector script at startup
|
|
53
55
|
let inspectorScript: string | null = null
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
'../../public/dev-editor-inspector.js',
|
|
57
|
-
)
|
|
56
|
+
const thisDir = dirname(fileURLToPath(import.meta.url))
|
|
57
|
+
const inspectorPath = join(thisDir, '../../public/dev-editor-inspector.js')
|
|
58
58
|
if (existsSync(inspectorPath)) {
|
|
59
59
|
inspectorScript = readFileSync(inspectorPath, 'utf-8')
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
function toRequest(req: IncomingMessage): Request {
|
|
63
|
+
const protocol = 'http'
|
|
64
|
+
const host = req.headers.host || `${BRIDGE_HOST}:${BRIDGE_PORT}`
|
|
65
|
+
const url = new URL(req.url || '/', `${protocol}://${host}`)
|
|
66
|
+
|
|
67
|
+
const headers = new Headers()
|
|
68
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
69
|
+
if (typeof value === 'undefined') continue
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
for (const v of value) headers.append(key, v)
|
|
72
|
+
} else {
|
|
73
|
+
headers.set(key, value)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const method = req.method || 'GET'
|
|
78
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
79
|
+
return new Request(url, { method, headers })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return new Request(url, {
|
|
83
|
+
method,
|
|
84
|
+
headers,
|
|
85
|
+
body: req as unknown as BodyInit,
|
|
86
|
+
duplex: 'half',
|
|
87
|
+
} as RequestInit)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function writeResponse(
|
|
91
|
+
res: ServerResponse,
|
|
92
|
+
response: Response,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
res.statusCode = response.status
|
|
95
|
+
response.headers.forEach((value, key) => {
|
|
96
|
+
res.setHeader(key, value)
|
|
97
|
+
})
|
|
64
98
|
|
|
65
|
-
|
|
99
|
+
// Preserve multiple Set-Cookie headers when available.
|
|
100
|
+
const getSetCookie = (
|
|
101
|
+
response.headers as Headers & { getSetCookie?: () => string[] }
|
|
102
|
+
).getSetCookie
|
|
103
|
+
if (typeof getSetCookie === 'function') {
|
|
104
|
+
const cookies = getSetCookie.call(response.headers)
|
|
105
|
+
if (cookies.length > 0) {
|
|
106
|
+
res.setHeader('set-cookie', cookies)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!response.body) {
|
|
111
|
+
res.end()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const reader = response.body.getReader()
|
|
116
|
+
while (true) {
|
|
117
|
+
const { done, value } = await reader.read()
|
|
118
|
+
if (done) break
|
|
119
|
+
if (!value || value.length === 0) continue
|
|
120
|
+
if (!res.write(Buffer.from(value))) {
|
|
121
|
+
await new Promise<void>((resolve) => {
|
|
122
|
+
res.once('drain', resolve)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
res.end()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const server = createServer(async (incomingReq, outgoingRes) => {
|
|
130
|
+
try {
|
|
131
|
+
const req = toRequest(incomingReq)
|
|
66
132
|
const url = new URL(req.url)
|
|
67
133
|
const origin = req.headers.get('origin')
|
|
68
134
|
const cors = corsHeaders(origin)
|
|
69
135
|
|
|
70
136
|
// CORS preflight
|
|
71
137
|
if (req.method === 'OPTIONS') {
|
|
72
|
-
|
|
138
|
+
await writeResponse(
|
|
139
|
+
outgoingRes,
|
|
140
|
+
new Response(null, { status: 204, headers: cors }),
|
|
141
|
+
)
|
|
142
|
+
return
|
|
73
143
|
}
|
|
74
144
|
|
|
75
145
|
// Health check (used for auto-discovery from Vercel editor)
|
|
76
146
|
if (url.pathname === '/health') {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
147
|
+
await writeResponse(
|
|
148
|
+
outgoingRes,
|
|
149
|
+
Response.json(
|
|
150
|
+
{ status: 'ok', version: '1.0.0', bridge: true },
|
|
151
|
+
{ headers: cors },
|
|
152
|
+
),
|
|
80
153
|
)
|
|
154
|
+
return
|
|
81
155
|
}
|
|
82
156
|
|
|
83
157
|
// Serve the inspector script
|
|
84
158
|
if (url.pathname === '/dev-editor-inspector.js') {
|
|
85
159
|
if (!inspectorScript) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
160
|
+
await writeResponse(
|
|
161
|
+
outgoingRes,
|
|
162
|
+
new Response('Inspector script not found', {
|
|
163
|
+
status: 404,
|
|
164
|
+
headers: cors,
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
return
|
|
90
168
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
169
|
+
await writeResponse(
|
|
170
|
+
outgoingRes,
|
|
171
|
+
new Response(inspectorScript, {
|
|
172
|
+
headers: {
|
|
173
|
+
...cors,
|
|
174
|
+
'Content-Type': 'application/javascript',
|
|
175
|
+
'Cache-Control': 'public, max-age=3600',
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
return
|
|
98
180
|
}
|
|
99
181
|
|
|
100
182
|
// API routes
|
|
101
183
|
if (url.pathname.startsWith('/api/')) {
|
|
102
|
-
|
|
184
|
+
await writeResponse(outgoingRes, await handleAPI(req, url, cors))
|
|
185
|
+
return
|
|
103
186
|
}
|
|
104
187
|
|
|
105
188
|
// Everything else: proxy to target localhost
|
|
106
|
-
|
|
107
|
-
|
|
189
|
+
await writeResponse(
|
|
190
|
+
outgoingRes,
|
|
191
|
+
await handleProxy(req, url, cors, inspectorScript),
|
|
192
|
+
)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const message =
|
|
195
|
+
error instanceof Error ? error.message : 'Unknown bridge error'
|
|
196
|
+
outgoingRes.statusCode = 500
|
|
197
|
+
outgoingRes.setHeader('Content-Type', 'application/json')
|
|
198
|
+
outgoingRes.end(JSON.stringify({ error: message }))
|
|
199
|
+
}
|
|
108
200
|
})
|
|
109
201
|
|
|
110
|
-
|
|
111
|
-
console.log(
|
|
112
|
-
|
|
113
|
-
)
|
|
202
|
+
server.listen(BRIDGE_PORT, BRIDGE_HOST, () => {
|
|
203
|
+
console.log(
|
|
204
|
+
`\n pAInt Bridge running on http://${BRIDGE_HOST}:${BRIDGE_PORT}`,
|
|
205
|
+
)
|
|
206
|
+
console.log(
|
|
207
|
+
` Connect from: https://dev-editor-flow.vercel.app?bridge=${BRIDGE_HOST}:${BRIDGE_PORT}\n`,
|
|
208
|
+
)
|
|
209
|
+
})
|
|
@@ -141,7 +141,7 @@ export function ConnectModal() {
|
|
|
141
141
|
setFolderPath(saved)
|
|
142
142
|
setFolderError(null)
|
|
143
143
|
}
|
|
144
|
-
}, [
|
|
144
|
+
}, [currentUrl, portRoots, connectionStatus])
|
|
145
145
|
|
|
146
146
|
const handleBrowse = async () => {
|
|
147
147
|
setIsBrowsing(true)
|
|
@@ -368,7 +368,6 @@ export function ConnectModal() {
|
|
|
368
368
|
color: 'var(--text-primary)',
|
|
369
369
|
border: '1px solid var(--border)',
|
|
370
370
|
}}
|
|
371
|
-
autoFocus
|
|
372
371
|
/>
|
|
373
372
|
) : (
|
|
374
373
|
<select
|
|
@@ -72,7 +72,7 @@ export function PreviewFrame() {
|
|
|
72
72
|
const setPreviewWidth = useEditorStore((s) => s.setPreviewWidth)
|
|
73
73
|
const currentPagePath = useEditorStore((s) => s.currentPagePath)
|
|
74
74
|
const setConnectionStatus = useEditorStore((s) => s.setConnectionStatus)
|
|
75
|
-
const
|
|
75
|
+
const _viewMode = useEditorStore((s) => s.viewMode)
|
|
76
76
|
const { iframeRef, sendToInspector } = usePostMessage()
|
|
77
77
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
78
78
|
const lastSrcRef = useRef<string | null>(null)
|
|
@@ -139,7 +139,7 @@ export function PreviewFrame() {
|
|
|
139
139
|
// Selection mode is managed by TopBar via sendToInspector.
|
|
140
140
|
// When exiting preview, TopBar re-enables selection and the proxy
|
|
141
141
|
// iframe is still loaded — no reload needed.
|
|
142
|
-
}, [
|
|
142
|
+
}, [targetUrl, connectionStatus])
|
|
143
143
|
|
|
144
144
|
// Drag resize logic — symmetric from center
|
|
145
145
|
const dragStateRef = useRef<{
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
PREVIEW_WIDTH_MAX,
|
|
10
10
|
BREAKPOINT_CATEGORY_MAP,
|
|
11
11
|
} from '@/lib/constants'
|
|
12
|
-
import type { DevicePreset } from '@/lib/constants'
|
|
13
12
|
|
|
14
13
|
export function ResponsiveToolbar() {
|
|
15
14
|
const previewWidth = useEditorStore((s) => s.previewWidth)
|
|
@@ -57,7 +56,7 @@ export function ResponsiveToolbar() {
|
|
|
57
56
|
|
|
58
57
|
const handleInputCommit = useCallback(() => {
|
|
59
58
|
const parsed = parseInt(inputValue, 10)
|
|
60
|
-
if (!isNaN(parsed)) {
|
|
59
|
+
if (!Number.isNaN(parsed)) {
|
|
61
60
|
applyWidth(parsed)
|
|
62
61
|
} else {
|
|
63
62
|
setInputValue(String(previewWidth))
|
|
@@ -76,7 +76,7 @@ function rgbToHsv({ r, g, b }: RGB): HSV {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function rgbToHex({ r, g, b }: RGB): string {
|
|
79
|
-
return
|
|
79
|
+
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function hexToRgb(hex: string): RGB | null {
|
|
@@ -93,7 +93,7 @@ function hexToRgb(hex: string): RGB | null {
|
|
|
93
93
|
} else {
|
|
94
94
|
return null
|
|
95
95
|
}
|
|
96
|
-
if (isNaN(r) || isNaN(g) || isNaN(b)) return null
|
|
96
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null
|
|
97
97
|
return { r, g, b }
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -254,7 +254,7 @@ function ScrubInput({
|
|
|
254
254
|
const commit = useCallback(
|
|
255
255
|
(raw: string) => {
|
|
256
256
|
const n = parseInt(raw, 10)
|
|
257
|
-
if (!isNaN(n)) onChange(Math.max(min, Math.min(max, n)))
|
|
257
|
+
if (!Number.isNaN(n)) onChange(Math.max(min, Math.min(max, n)))
|
|
258
258
|
setEditing(false)
|
|
259
259
|
},
|
|
260
260
|
[onChange, min, max],
|
|
@@ -264,7 +264,6 @@ function ScrubInput({
|
|
|
264
264
|
<div className="flex flex-col items-center gap-0.5">
|
|
265
265
|
{editing ? (
|
|
266
266
|
<input
|
|
267
|
-
autoFocus
|
|
268
267
|
type="text"
|
|
269
268
|
inputMode="numeric"
|
|
270
269
|
value={editVal}
|
|
@@ -429,7 +428,7 @@ export function ColorPicker({
|
|
|
429
428
|
// Commit pending hex input on close
|
|
430
429
|
if (prevOpenRef.current) {
|
|
431
430
|
const currentHex = rgbToHex(hsvToRgb(hsv))
|
|
432
|
-
const pendingHex =
|
|
431
|
+
const pendingHex = `#${hexInput}`
|
|
433
432
|
if (pendingHex !== currentHex) {
|
|
434
433
|
const rgb = hexToRgb(pendingHex)
|
|
435
434
|
if (rgb) {
|
|
@@ -503,7 +502,7 @@ export function ColorPicker({
|
|
|
503
502
|
|
|
504
503
|
// Hex commit
|
|
505
504
|
const commitHex = useCallback(() => {
|
|
506
|
-
const rgb = hexToRgb(
|
|
505
|
+
const rgb = hexToRgb(`#${hexInput}`)
|
|
507
506
|
if (rgb) {
|
|
508
507
|
const next = rgbToHsv(rgb)
|
|
509
508
|
setHsv(next)
|
|
@@ -617,7 +616,7 @@ export function ColorPicker({
|
|
|
617
616
|
// Emit immediately when a valid 6-char hex is entered
|
|
618
617
|
const clean = v.replace(/[^0-9a-fA-F]/g, '')
|
|
619
618
|
if (clean.length === 6) {
|
|
620
|
-
const rgb = hexToRgb(
|
|
619
|
+
const rgb = hexToRgb(`#${clean}`)
|
|
621
620
|
if (rgb) {
|
|
622
621
|
const next = rgbToHsv(rgb)
|
|
623
622
|
setHsv(next)
|
|
@@ -626,7 +625,7 @@ export function ColorPicker({
|
|
|
626
625
|
}
|
|
627
626
|
}}
|
|
628
627
|
onBlur={() => {
|
|
629
|
-
const rgb = hexToRgb(
|
|
628
|
+
const rgb = hexToRgb(`#${hexInput}`)
|
|
630
629
|
if (rgb) {
|
|
631
630
|
const next = rgbToHsv(rgb)
|
|
632
631
|
setHsv(next)
|
|
@@ -863,7 +862,6 @@ export function ColorPicker({
|
|
|
863
862
|
color: 'var(--text-primary)',
|
|
864
863
|
outline: 'none',
|
|
865
864
|
}}
|
|
866
|
-
autoFocus
|
|
867
865
|
/>
|
|
868
866
|
|
|
869
867
|
{/* Scrollable list */}
|
|
@@ -33,8 +33,8 @@ function buildComponentTree(
|
|
|
33
33
|
|
|
34
34
|
for (const [path, candidate] of nodeMap) {
|
|
35
35
|
if (
|
|
36
|
-
component.selectorPath.startsWith(path
|
|
37
|
-
component.selectorPath.startsWith(path
|
|
36
|
+
component.selectorPath.startsWith(`${path} `) ||
|
|
37
|
+
component.selectorPath.startsWith(`${path} > `)
|
|
38
38
|
) {
|
|
39
39
|
if (
|
|
40
40
|
!parentNode ||
|
|
@@ -172,7 +172,7 @@ export default function ComponentsPanel() {
|
|
|
172
172
|
(c) =>
|
|
173
173
|
c.name.toLowerCase().includes(query) ||
|
|
174
174
|
c.tagName.toLowerCase().includes(query) ||
|
|
175
|
-
(c.className
|
|
175
|
+
(c.className?.toLowerCase().includes(query)),
|
|
176
176
|
)
|
|
177
177
|
}, [detectedComponents, componentSearchQuery])
|
|
178
178
|
|
|
@@ -405,7 +405,7 @@ const DRAG_DATA_TYPE = 'application/x-dev-editor-layer-move'
|
|
|
405
405
|
type DropPosition = 'before' | 'inside' | 'after'
|
|
406
406
|
|
|
407
407
|
function isDescendant(parentId: string, childId: string): boolean {
|
|
408
|
-
return childId.startsWith(parentId
|
|
408
|
+
return childId.startsWith(`${parentId} > `)
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
function getDropPosition(
|
|
@@ -30,7 +30,7 @@ export function TerminalPanel() {
|
|
|
30
30
|
fitAddon.fit()
|
|
31
31
|
const dims = fitAddon.proposeDimensions()
|
|
32
32
|
if (dims) {
|
|
33
|
-
ws.send(
|
|
33
|
+
ws.send(`\x01${JSON.stringify({ cols: dims.cols, rows: dims.rows })}`)
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -54,7 +54,7 @@ export function TerminalPanel() {
|
|
|
54
54
|
|
|
55
55
|
term.onResize((size: { cols: number; rows: number }) => {
|
|
56
56
|
if (ws.readyState === WebSocket.OPEN) {
|
|
57
|
-
ws.send(
|
|
57
|
+
ws.send(`\x01${JSON.stringify({ cols: size.cols, rows: size.rows })}`)
|
|
58
58
|
}
|
|
59
59
|
})
|
|
60
60
|
},
|
|
@@ -4,9 +4,7 @@ import { useMemo, useState, useCallback, useRef } from 'react'
|
|
|
4
4
|
import { useEditorStore } from '@/store'
|
|
5
5
|
import {
|
|
6
6
|
buildInstructionsFooter,
|
|
7
|
-
BREAKPOINTS,
|
|
8
7
|
getBreakpointDeviceInfo,
|
|
9
|
-
getBreakpointRange,
|
|
10
8
|
} from '@/lib/constants'
|
|
11
9
|
import { EditablePre } from '@/components/common/EditablePre'
|
|
12
10
|
import type { Breakpoint } from '@/types/changelog'
|
|
@@ -67,7 +65,7 @@ function buildElementLogText(opts: {
|
|
|
67
65
|
const attrParts: string[] = []
|
|
68
66
|
if (opts.elementId) attrParts.push(`id="${opts.elementId}"`)
|
|
69
67
|
if (opts.className) attrParts.push(`class="${opts.className}"`)
|
|
70
|
-
const tag = `<${opts.tagName}${attrParts.length ?
|
|
68
|
+
const tag = `<${opts.tagName}${attrParts.length ? ` ${attrParts.join(' ')}` : ''}>`
|
|
71
69
|
|
|
72
70
|
const isMobileApp =
|
|
73
71
|
opts.framework === 'flutter' || opts.framework === 'react-native'
|
|
@@ -21,21 +21,21 @@ import type { FileMap, ClaudeScanResponse } from '@/types/claude'
|
|
|
21
21
|
|
|
22
22
|
type BreakpointGroupKey = 'all' | 'desktop-only' | 'tablet-only' | 'mobile-only'
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const _GROUP_ORDER: BreakpointGroupKey[] = [
|
|
25
25
|
'all',
|
|
26
26
|
'desktop-only',
|
|
27
27
|
'tablet-only',
|
|
28
28
|
'mobile-only',
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const _GROUP_META: Record<BreakpointGroupKey, { label: string }> = {
|
|
32
32
|
all: { label: 'All' },
|
|
33
33
|
'desktop-only': { label: 'Desktop Only' },
|
|
34
34
|
'tablet-only': { label: 'Tablet Only' },
|
|
35
35
|
'mobile-only': { label: 'Mobile Only' },
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function
|
|
38
|
+
function _getGroupKey(change: StyleChange): BreakpointGroupKey {
|
|
39
39
|
const scope = change.changeScope ?? 'all'
|
|
40
40
|
if (scope === 'all') return 'all'
|
|
41
41
|
return `${change.breakpoint}-only` as BreakpointGroupKey
|
|
@@ -141,8 +141,8 @@ function CheckIcon({ size = 14 }: { size?: number }) {
|
|
|
141
141
|
function buildSingleElementLog(
|
|
142
142
|
snapshot: ElementSnapshot,
|
|
143
143
|
changes: StyleChange[],
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
_fileMap?: FileMap | null,
|
|
145
|
+
_projectRoot?: string | null,
|
|
146
146
|
framework?: string | null,
|
|
147
147
|
cssStrategy?: string[] | null,
|
|
148
148
|
): string {
|
|
@@ -152,7 +152,7 @@ function buildSingleElementLog(
|
|
|
152
152
|
const attrParts: string[] = []
|
|
153
153
|
if (snapshot.elementId) attrParts.push(`id="${snapshot.elementId}"`)
|
|
154
154
|
if (snapshot.className) attrParts.push(`class="${snapshot.className}"`)
|
|
155
|
-
const tag = `<${snapshot.tagName}${attrParts.length ?
|
|
155
|
+
const tag = `<${snapshot.tagName}${attrParts.length ? ` ${attrParts.join(' ')}` : ''}>`
|
|
156
156
|
|
|
157
157
|
const changeBp = (changes[0]?.breakpoint || 'mobile') as Breakpoint
|
|
158
158
|
const { deviceName, range } = getBreakpointDeviceInfo(changeBp)
|
|
@@ -230,10 +230,10 @@ function buildSingleElementLog(
|
|
|
230
230
|
function buildElementSection(
|
|
231
231
|
snapshot: ElementSnapshot,
|
|
232
232
|
changes: StyleChange[],
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
_fileMap?: FileMap | null,
|
|
234
|
+
_projectRoot?: string | null,
|
|
235
235
|
framework?: string | null,
|
|
236
|
-
|
|
236
|
+
_cssStrategy?: string[] | null,
|
|
237
237
|
): string {
|
|
238
238
|
const lines: string[] = []
|
|
239
239
|
const isMobileApp = framework === 'flutter' || framework === 'react-native'
|
|
@@ -241,7 +241,7 @@ function buildElementSection(
|
|
|
241
241
|
const attrParts: string[] = []
|
|
242
242
|
if (snapshot.elementId) attrParts.push(`id="${snapshot.elementId}"`)
|
|
243
243
|
if (snapshot.className) attrParts.push(`class="${snapshot.className}"`)
|
|
244
|
-
const tag = `<${snapshot.tagName}${attrParts.length ?
|
|
244
|
+
const tag = `<${snapshot.tagName}${attrParts.length ? ` ${attrParts.join(' ')}` : ''}>`
|
|
245
245
|
|
|
246
246
|
const changeBp = (changes[0]?.breakpoint || 'mobile') as Breakpoint
|
|
247
247
|
const { deviceName: elDevice, range: elRange } =
|
|
@@ -655,7 +655,7 @@ interface BreakpointGroupData {
|
|
|
655
655
|
allChanges: StyleChange[]
|
|
656
656
|
}
|
|
657
657
|
|
|
658
|
-
function
|
|
658
|
+
function _BreakpointGroupAccordion({
|
|
659
659
|
group,
|
|
660
660
|
targetUrl,
|
|
661
661
|
pagePath,
|
|
@@ -1220,7 +1220,7 @@ export function ChangesPanel() {
|
|
|
1220
1220
|
onStderr: (line: string) => {
|
|
1221
1221
|
const w = useEditorStore.getState().writeToTerminal
|
|
1222
1222
|
const formatted = formatStderrLine(line)
|
|
1223
|
-
if (formatted) w?.(formatted
|
|
1223
|
+
if (formatted) w?.(`${formatted}\r\n`)
|
|
1224
1224
|
},
|
|
1225
1225
|
onResult: (data: ClaudeScanResponse) => {
|
|
1226
1226
|
setAiScanResult(data)
|
|
@@ -60,7 +60,7 @@ export function ClaudeIntegrationPanel() {
|
|
|
60
60
|
// Reset setupComplete when targetUrl changes so setup re-shows if needed
|
|
61
61
|
useEffect(() => {
|
|
62
62
|
setSetupComplete(false)
|
|
63
|
-
}, [
|
|
63
|
+
}, [])
|
|
64
64
|
|
|
65
65
|
// Check if setup is needed
|
|
66
66
|
const needsSetup =
|
|
@@ -99,7 +99,7 @@ export function ClaudeIntegrationPanel() {
|
|
|
99
99
|
onStderr: (line) => {
|
|
100
100
|
const w = useEditorStore.getState().writeToTerminal
|
|
101
101
|
const formatted = formatStderrLine(line)
|
|
102
|
-
if (formatted) w?.(formatted
|
|
102
|
+
if (formatted) w?.(`${formatted}\r\n`)
|
|
103
103
|
},
|
|
104
104
|
onResult: (data) => {
|
|
105
105
|
setSessionId(data.sessionId)
|
|
@@ -8,7 +8,7 @@ export function DiffViewer() {
|
|
|
8
8
|
const parsedDiffs = useEditorStore((s) => s.parsedDiffs)
|
|
9
9
|
|
|
10
10
|
const summary = useMemo(() => {
|
|
11
|
-
|
|
11
|
+
const totalFiles = parsedDiffs.length
|
|
12
12
|
let totalAdded = 0
|
|
13
13
|
let totalRemoved = 0
|
|
14
14
|
for (const diff of parsedDiffs) {
|