@antigenic-oss/paint 0.2.0 → 0.2.2

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 (65) hide show
  1. package/README.md +32 -17
  2. package/bin/bridge-server.js +38 -0
  3. package/bin/paint.js +559 -104
  4. package/bin/terminal-server.js +105 -0
  5. package/package.json +7 -8
  6. package/public/dev-editor-inspector.js +92 -104
  7. package/src/app/api/claude/apply/route.ts +2 -2
  8. package/src/app/api/project/scan/route.ts +1 -1
  9. package/src/app/api/proxy/[[...path]]/route.ts +4 -4
  10. package/src/app/docs/DocsClient.tsx +1 -1
  11. package/src/app/docs/page.tsx +0 -1
  12. package/src/app/page.tsx +1 -1
  13. package/src/bridge/api-handlers.ts +1 -1
  14. package/src/bridge/proxy-handler.ts +4 -4
  15. package/src/bridge/server.ts +135 -39
  16. package/src/components/ConnectModal.tsx +1 -2
  17. package/src/components/PreviewFrame.tsx +2 -2
  18. package/src/components/ResponsiveToolbar.tsx +1 -2
  19. package/src/components/common/ColorPicker.tsx +7 -9
  20. package/src/components/common/UnitInput.tsx +1 -1
  21. package/src/components/common/VariableColorPicker.tsx +0 -1
  22. package/src/components/left-panel/ComponentsPanel.tsx +3 -3
  23. package/src/components/left-panel/LayerNode.tsx +1 -1
  24. package/src/components/left-panel/icons.tsx +1 -1
  25. package/src/components/left-panel/terminal/TerminalPanel.tsx +2 -2
  26. package/src/components/right-panel/ElementLogBox.tsx +1 -3
  27. package/src/components/right-panel/changes/ChangesPanel.tsx +12 -12
  28. package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +2 -2
  29. package/src/components/right-panel/claude/DiffViewer.tsx +1 -1
  30. package/src/components/right-panel/claude/ProjectRootSelector.tsx +7 -7
  31. package/src/components/right-panel/claude/SetupFlow.tsx +1 -1
  32. package/src/components/right-panel/console/ConsolePanel.tsx +4 -4
  33. package/src/components/right-panel/design/BackgroundSection.tsx +2 -2
  34. package/src/components/right-panel/design/GradientEditor.tsx +6 -6
  35. package/src/components/right-panel/design/LayoutSection.tsx +4 -4
  36. package/src/components/right-panel/design/PositionSection.tsx +2 -2
  37. package/src/components/right-panel/design/SVGSection.tsx +2 -3
  38. package/src/components/right-panel/design/ShadowBlurSection.tsx +5 -5
  39. package/src/components/right-panel/design/TextSection.tsx +5 -5
  40. package/src/components/right-panel/design/icons.tsx +1 -1
  41. package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +2 -2
  42. package/src/components/right-panel/design/inputs/CompactInput.tsx +2 -2
  43. package/src/components/right-panel/design/inputs/DraggableLabel.tsx +2 -1
  44. package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +1 -1
  45. package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +3 -3
  46. package/src/components/right-panel/design/inputs/SectionHeader.tsx +2 -1
  47. package/src/hooks/useDOMTree.ts +0 -1
  48. package/src/hooks/usePostMessage.ts +4 -3
  49. package/src/hooks/useTargetUrl.ts +1 -1
  50. package/src/inspector/DOMTraverser.ts +2 -2
  51. package/src/inspector/HoverHighlighter.ts +6 -6
  52. package/src/inspector/SelectionHighlighter.ts +4 -4
  53. package/src/lib/classifyElement.ts +1 -2
  54. package/src/lib/claude-bin.ts +1 -1
  55. package/src/lib/clientProjectScanner.ts +13 -13
  56. package/src/lib/cssVariableUtils.ts +1 -1
  57. package/src/lib/folderPicker.ts +4 -1
  58. package/src/lib/projectScanner.ts +15 -15
  59. package/src/lib/tailwindClassParser.ts +1 -1
  60. package/src/lib/textShadowUtils.ts +1 -1
  61. package/src/lib/utils.ts +4 -4
  62. package/src/proxy.ts +1 -1
  63. package/src/store/treeSlice.ts +2 -2
  64. package/src/server/terminal-server.ts +0 -104
  65. 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(/\s+/)[0];
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 = '/' + (path || '')
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 + '</body>')
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!.observe(el))
36
+ sections.forEach((el) => observerRef.current?.observe(el))
37
37
 
38
38
  return () => observerRef.current?.disconnect()
39
39
  }, [])
@@ -2,7 +2,6 @@ import type { Metadata } from 'next'
2
2
  import Link from 'next/link'
3
3
  import {
4
4
  CodeBlock,
5
- CopyButton,
6
5
  FaqAccordion,
7
6
  FaqSection,
8
7
  FrameworkAccordion,
package/src/app/page.tsx CHANGED
@@ -18,7 +18,7 @@ export default function Home() {
18
18
  setIsRecursiveEmbed(true)
19
19
  try {
20
20
  window.parent.postMessage({ type: 'RECURSIVE_EMBED_DETECTED' }, '*')
21
- } catch (e) {
21
+ } catch (_e) {
22
22
  /* cross-origin */
23
23
  }
24
24
  return
@@ -770,7 +770,7 @@ async function handleClaudeApply(
770
770
  )
771
771
  }
772
772
 
773
- const combinedOutput = result.stdout + '\n' + result.stderr
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 + '</head>')
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 + '</body>')
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 fetchUrl = `${targetOrigin}${pathname}${url.search ? url.search.replace(new RegExp(`[?&]${PROXY_HEADER}=[^&]*`), '') : ''}`
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 && contentType.includes('image/')) ||
308
+ (contentType?.includes('image/')) ||
309
309
  pathname.match(/\.(png|jpe?g|gif|svg|ico|webp|avif)(\?|$)/i)
310
310
  ) {
311
311
  responseHeaders.set(
@@ -1,22 +1,24 @@
1
1
  /**
2
- * pAInt Bridge Server
2
+ * pAInt Bridge Server (Node runtime)
3
3
  *
4
- * A lightweight Bun HTTP server that runs on the user's machine.
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 { readFileSync, existsSync } from 'node:fs'
15
- import { join } from 'node:path'
16
- import { handleProxy } from './proxy-handler'
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 inspectorPath = join(
55
- import.meta.dir,
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
- Bun.serve({
63
- port: BRIDGE_PORT,
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
- async fetch(req) {
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
- return new Response(null, { status: 204, headers: cors })
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
- return Response.json(
78
- { status: 'ok', version: '1.0.0', bridge: true },
79
- { headers: cors },
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
- return new Response('Inspector script not found', {
87
- status: 404,
88
- headers: cors,
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
- return new Response(inspectorScript, {
92
- headers: {
93
- ...cors,
94
- 'Content-Type': 'application/javascript',
95
- 'Cache-Control': 'public, max-age=3600',
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
- return handleAPI(req, url, cors)
184
+ await writeResponse(outgoingRes, await handleAPI(req, url, cors))
185
+ return
103
186
  }
104
187
 
105
188
  // Everything else: proxy to target localhost
106
- return handleProxy(req, url, cors, inspectorScript)
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
- console.log(`\n pAInt Bridge running on http://localhost:${BRIDGE_PORT}`)
111
- console.log(
112
- ` Connect from: https://dev-editor-flow.vercel.app?bridge=localhost:${BRIDGE_PORT}\n`,
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
- }, [selectedPort, urlMode, currentUrl, portRoots, connectionStatus])
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 viewMode = useEditorStore((s) => s.viewMode)
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
- }, [viewMode, targetUrl, connectionStatus, currentPagePath, iframeRef])
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 '#' + [r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')
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 = '#' + hexInput
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('#' + hexInput)
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('#' + clean)
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('#' + hexInput)
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 */}
@@ -32,7 +32,7 @@ export function UnitInput({
32
32
  onChange('auto')
33
33
  } else {
34
34
  const n = parseFloat(num)
35
- if (!isNaN(n)) onChange(formatCSSValue(n, u))
35
+ if (!Number.isNaN(n)) onChange(formatCSSValue(n, u))
36
36
  }
37
37
  },
38
38
  [onChange],
@@ -527,7 +527,6 @@ export function VariableColorPicker({
527
527
  />
528
528
  </svg>
529
529
  <input
530
- autoFocus
531
530
  type="text"
532
531
  placeholder="Search tokens..."
533
532
  value={search}
@@ -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 && c.className.toLowerCase().includes(query)),
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(
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import type React from 'react'
2
2
 
3
3
  export function LayersIcon(props: React.SVGProps<SVGSVGElement>) {
4
4
  return (
@@ -30,7 +30,7 @@ export function TerminalPanel() {
30
30
  fitAddon.fit()
31
31
  const dims = fitAddon.proposeDimensions()
32
32
  if (dims) {
33
- ws.send('\x01' + JSON.stringify({ cols: dims.cols, rows: dims.rows }))
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('\x01' + JSON.stringify({ cols: size.cols, rows: size.rows }))
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 ? ' ' + attrParts.join(' ') : ''}>`
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 GROUP_ORDER: BreakpointGroupKey[] = [
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 GROUP_META: Record<BreakpointGroupKey, { label: string }> = {
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 getGroupKey(change: StyleChange): BreakpointGroupKey {
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
- fileMap?: FileMap | null,
145
- projectRoot?: string | null,
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 ? ' ' + attrParts.join(' ') : ''}>`
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
- fileMap?: FileMap | null,
234
- projectRoot?: string | null,
233
+ _fileMap?: FileMap | null,
234
+ _projectRoot?: string | null,
235
235
  framework?: string | null,
236
- cssStrategy?: string[] | null,
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 ? ' ' + attrParts.join(' ') : ''}>`
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 BreakpointGroupAccordion({
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 + '\r\n')
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
- }, [targetUrl])
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 + '\r\n')
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
- let totalFiles = parsedDiffs.length
11
+ const totalFiles = parsedDiffs.length
12
12
  let totalAdded = 0
13
13
  let totalRemoved = 0
14
14
  for (const diff of parsedDiffs) {