@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
@@ -138,13 +138,13 @@ export function ProjectRootSelector({
138
138
  setValidating(false)
139
139
  }
140
140
  }, [
141
- inputValue,
142
- targetUrl,
143
- setProjectRoot,
144
- onSaved,
145
- triggerScan,
146
- triggerClientScan,
147
- directoryHandle,
141
+ inputValue,
142
+ targetUrl,
143
+ setProjectRoot,
144
+ onSaved,
145
+ triggerScan,
146
+ triggerClientScan,
147
+ directoryHandle, scanFeedbackCallbacks
148
148
  ])
149
149
 
150
150
  const handleKeyDown = useCallback(
@@ -16,7 +16,7 @@ export function SetupFlow({ targetUrl, onComplete }: SetupFlowProps) {
16
16
  const cliAvailable = useEditorStore((s) => s.cliAvailable)
17
17
  const setCliAvailable = useEditorStore((s) => s.setCliAvailable)
18
18
  const portRoots = useEditorStore((s) => s.portRoots)
19
- const projectRoot = portRoots[targetUrl] ?? null
19
+ const _projectRoot = portRoots[targetUrl] ?? null
20
20
 
21
21
  const bridgeStatus = useEditorStore((s) => s.bridgeStatus)
22
22
  const isLocal = typeof window !== 'undefined' && isEditorOnLocalhost()
@@ -58,7 +58,7 @@ export function ConsolePanel() {
58
58
  if (!userScrolledUp.current && listRef.current) {
59
59
  listRef.current.scrollTop = listRef.current.scrollHeight
60
60
  }
61
- }, [filtered.length])
61
+ }, [])
62
62
 
63
63
  const handleScroll = useCallback(() => {
64
64
  const el = listRef.current
@@ -74,7 +74,7 @@ export function ConsolePanel() {
74
74
  .map((e) => {
75
75
  const ts = new Date(e.timestamp).toISOString()
76
76
  const loc = e.source
77
- ? ` (${e.source}${e.line != null ? ':' + e.line : ''}${e.column != null ? ':' + e.column : ''})`
77
+ ? ` (${e.source}${e.line != null ? `:${e.line}` : ''}${e.column != null ? `:${e.column}` : ''})`
78
78
  : ''
79
79
  return `[${ts}] ERROR${loc}: ${e.args.join(' ')}`
80
80
  })
@@ -194,8 +194,8 @@ export function ConsolePanel() {
194
194
  {entry.source && (
195
195
  <span style={{ color: 'var(--text-muted)', fontSize: 9 }}>
196
196
  {entry.source.split('/').pop()}
197
- {entry.line != null ? ':' + entry.line : ''}
198
- {entry.column != null ? ':' + entry.column : ''}
197
+ {entry.line != null ? `:${entry.line}` : ''}
198
+ {entry.column != null ? `:${entry.column}` : ''}
199
199
  </span>
200
200
  )}
201
201
  </span>
@@ -250,7 +250,7 @@ function ClipDropdown({
250
250
  export function BackgroundSection() {
251
251
  const computedStyles = useEditorStore((state) => state.computedStyles)
252
252
  const cssVariableUsages = useEditorStore((state) => state.cssVariableUsages)
253
- const selectorPath = useEditorStore((state) => state.selectorPath)
253
+ const _selectorPath = useEditorStore((state) => state.selectorPath)
254
254
  const { applyChange, resetProperty } = useChangeTracker()
255
255
 
256
256
  const hasChanges = useEditorStore((s) => {
@@ -310,7 +310,7 @@ export function BackgroundSection() {
310
310
  if (parsedGradient) {
311
311
  setGradientData(parsedGradient)
312
312
  }
313
- }, [selectorPath, bgImage, parsedGradient])
313
+ }, [bgImage, parsedGradient])
314
314
 
315
315
  // --- Layer preview swatch ---
316
316
  const layerSwatchBg = useMemo(() => {
@@ -42,7 +42,7 @@ function parseStopColor(color: string): { hex: string; opacity: number } {
42
42
  b = +rgbaMatch[3]
43
43
  const a = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1
44
44
  const hex =
45
- '#' + [r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')
45
+ `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`
46
46
  return { hex, opacity: Math.round(a * 100) }
47
47
  }
48
48
  if (color.startsWith('#')) {
@@ -453,7 +453,7 @@ export function GradientEditor({
453
453
  const newStops = [...value.stops, newStop].sort(
454
454
  (a, b) => a.position - b.position,
455
455
  )
456
- const newIndex = newStops.findIndex((s) => s === newStop)
456
+ const newIndex = newStops.indexOf(newStop)
457
457
  setSelectedStop(newIndex)
458
458
  onChange({ ...value, stops: newStops })
459
459
  },
@@ -483,7 +483,7 @@ export function GradientEditor({
483
483
  (newHex: string) => {
484
484
  const clean = newHex.replace(/[^0-9a-fA-F]/g, '').slice(0, 6)
485
485
  if (clean.length === 6 || clean.length === 3) {
486
- const hex = '#' + clean
486
+ const hex = `#${clean}`
487
487
  const newColor = buildStopColor(hex, stopOpacity)
488
488
  updateStop(selectedStop, { color: newColor })
489
489
  }
@@ -511,14 +511,14 @@ export function GradientEditor({
511
511
 
512
512
  const commitAngle = useCallback(() => {
513
513
  const n = parseInt(angleInput, 10)
514
- if (!isNaN(n)) updateAngle(((n % 360) + 360) % 360)
514
+ if (!Number.isNaN(n)) updateAngle(((n % 360) + 360) % 360)
515
515
  else setAngleInput(String(value.angle))
516
516
  }, [angleInput, value.angle, updateAngle])
517
517
 
518
518
  const [hexInput, setHexInput] = useState(stopHex.replace('#', ''))
519
519
  useEffect(() => setHexInput(stopHex.replace('#', '')), [stopHex])
520
520
 
521
- const commitHex = useCallback(() => {
521
+ const _commitHex = useCallback(() => {
522
522
  updateStopHex(hexInput)
523
523
  }, [hexInput, updateStopHex])
524
524
 
@@ -527,7 +527,7 @@ export function GradientEditor({
527
527
 
528
528
  const commitOpacity = useCallback(() => {
529
529
  const n = parseInt(opacityInput, 10)
530
- if (!isNaN(n)) updateStopOpacity(n)
530
+ if (!Number.isNaN(n)) updateStopOpacity(n)
531
531
  else setOpacityInput(String(stopOpacity))
532
532
  }, [opacityInput, stopOpacity, updateStopOpacity])
533
533
 
@@ -132,7 +132,7 @@ function toGridTemplate(count: number): string {
132
132
  return `repeat(${count}, 1fr)`
133
133
  }
134
134
 
135
- function parseGapValues(gap: string): { row: string; col: string } {
135
+ function _parseGapValues(gap: string): { row: string; col: string } {
136
136
  if (!gap || gap === 'normal') return { row: '0px', col: '0px' }
137
137
  const parts = gap.trim().split(/\s+/)
138
138
  if (parts.length >= 2) return { row: parts[0], col: parts[1] }
@@ -1137,7 +1137,7 @@ function NumberStepper({
1137
1137
  value={value}
1138
1138
  onChange={(e) => {
1139
1139
  const n = parseInt(e.target.value, 10)
1140
- if (!isNaN(n) && n >= min && n <= max) onChange(n)
1140
+ if (!Number.isNaN(n) && n >= min && n <= max) onChange(n)
1141
1141
  }}
1142
1142
  className="w-8 h-full text-center text-[11px] bg-transparent border-none outline-none"
1143
1143
  style={{ color: 'var(--text-primary)' }}
@@ -1199,7 +1199,7 @@ function GapInput({
1199
1199
  const commit = useCallback(
1200
1200
  (num: string, u: string) => {
1201
1201
  const n = parseFloat(num)
1202
- if (!isNaN(n)) onChange(property, formatCSSValue(Math.max(0, n), u))
1202
+ if (!Number.isNaN(n)) onChange(property, formatCSSValue(Math.max(0, n), u))
1203
1203
  },
1204
1204
  [onChange, property],
1205
1205
  )
@@ -1246,7 +1246,7 @@ function GapInput({
1246
1246
  const next = units[(idx + 1) % units.length]
1247
1247
  setUnit(next)
1248
1248
  const n = parseFloat(localVal || '0')
1249
- if (!isNaN(n)) onChange(property, formatCSSValue(Math.max(0, n), next))
1249
+ if (!Number.isNaN(n)) onChange(property, formatCSSValue(Math.max(0, n), next))
1250
1250
  }, [units, unit, localVal, onChange, property])
1251
1251
 
1252
1252
  return (
@@ -259,7 +259,7 @@ function OffsetInput({
259
259
  onChange(property, 'auto')
260
260
  } else {
261
261
  const n = parseFloat(num)
262
- if (!isNaN(n)) onChange(property, formatCSSValue(n, 'px'))
262
+ if (!Number.isNaN(n)) onChange(property, formatCSSValue(n, 'px'))
263
263
  }
264
264
  },
265
265
  [onChange, property],
@@ -565,7 +565,7 @@ function ZIndexInput({
565
565
  setIsAutoMode(true)
566
566
  } else {
567
567
  const n = parseInt(v, 10)
568
- if (!isNaN(n)) {
568
+ if (!Number.isNaN(n)) {
569
569
  onChange('zIndex', String(n))
570
570
  setIsAutoMode(false)
571
571
  }
@@ -69,7 +69,6 @@ function SaveAsVariableRow({
69
69
  return (
70
70
  <div className="flex items-center gap-1 pl-1">
71
71
  <input
72
- autoFocus
73
72
  value={varName}
74
73
  onChange={(e) => setVarName(e.target.value)}
75
74
  onKeyDown={handleKeyDown}
@@ -329,7 +328,7 @@ export function SVGSection() {
329
328
  value={fillDisplay}
330
329
  property="fill"
331
330
  onChange={handleColorChange}
332
- varExpression={cssVariableUsages['fill']}
331
+ varExpression={cssVariableUsages.fill}
333
332
  />
334
333
  <SaveAsVariableRow
335
334
  property="fill"
@@ -346,7 +345,7 @@ export function SVGSection() {
346
345
  value={strokeDisplay}
347
346
  property="stroke"
348
347
  onChange={handleColorChange}
349
- varExpression={cssVariableUsages['stroke']}
348
+ varExpression={cssVariableUsages.stroke}
350
349
  />
351
350
  <SaveAsVariableRow
352
351
  property="stroke"
@@ -78,7 +78,7 @@ export function ShadowBlurSection() {
78
78
 
79
79
  const handleFilterBlurChange = (_property: string, value: string) => {
80
80
  const num = parseFloat(value)
81
- if (!isNaN(num) && num > 0) {
81
+ if (!Number.isNaN(num) && num > 0) {
82
82
  applyChange('filter', `blur(${num}px)`)
83
83
  } else {
84
84
  applyChange('filter', 'none')
@@ -150,7 +150,7 @@ export function ShadowBlurSection() {
150
150
  property={`shadow-${i}-x`}
151
151
  onChange={(_p, v) => {
152
152
  const num = parseFloat(v)
153
- if (!isNaN(num)) updateShadow(i, { x: num })
153
+ if (!Number.isNaN(num)) updateShadow(i, { x: num })
154
154
  }}
155
155
  units={['px']}
156
156
  />
@@ -160,7 +160,7 @@ export function ShadowBlurSection() {
160
160
  property={`shadow-${i}-y`}
161
161
  onChange={(_p, v) => {
162
162
  const num = parseFloat(v)
163
- if (!isNaN(num)) updateShadow(i, { y: num })
163
+ if (!Number.isNaN(num)) updateShadow(i, { y: num })
164
164
  }}
165
165
  units={['px']}
166
166
  />
@@ -170,7 +170,7 @@ export function ShadowBlurSection() {
170
170
  property={`shadow-${i}-blur`}
171
171
  onChange={(_p, v) => {
172
172
  const num = parseFloat(v)
173
- if (!isNaN(num)) updateShadow(i, { blur: Math.max(0, num) })
173
+ if (!Number.isNaN(num)) updateShadow(i, { blur: Math.max(0, num) })
174
174
  }}
175
175
  units={['px']}
176
176
  min={0}
@@ -181,7 +181,7 @@ export function ShadowBlurSection() {
181
181
  property={`shadow-${i}-spread`}
182
182
  onChange={(_p, v) => {
183
183
  const num = parseFloat(v)
184
- if (!isNaN(num)) updateShadow(i, { spread: num })
184
+ if (!Number.isNaN(num)) updateShadow(i, { spread: num })
185
185
  }}
186
186
  units={['px']}
187
187
  />
@@ -100,7 +100,7 @@ const selectStyle = {
100
100
  color: 'var(--text-primary)',
101
101
  } as const
102
102
 
103
- const labelStyle = {
103
+ const _labelStyle = {
104
104
  color: 'var(--text-muted)',
105
105
  } as const
106
106
 
@@ -391,7 +391,7 @@ export function TextSection() {
391
391
  value={color}
392
392
  property="color"
393
393
  onChange={handleChange}
394
- varExpression={cssVariableUsages['color']}
394
+ varExpression={cssVariableUsages.color}
395
395
  />
396
396
  </div>
397
397
  </div>
@@ -668,7 +668,7 @@ export function TextSection() {
668
668
  property={`textShadow-${i}-x`}
669
669
  onChange={(_p, v) => {
670
670
  const num = parseFloat(v)
671
- if (!isNaN(num)) updateShadow(i, { x: num })
671
+ if (!Number.isNaN(num)) updateShadow(i, { x: num })
672
672
  }}
673
673
  units={['px']}
674
674
  />
@@ -678,7 +678,7 @@ export function TextSection() {
678
678
  property={`textShadow-${i}-y`}
679
679
  onChange={(_p, v) => {
680
680
  const num = parseFloat(v)
681
- if (!isNaN(num)) updateShadow(i, { y: num })
681
+ if (!Number.isNaN(num)) updateShadow(i, { y: num })
682
682
  }}
683
683
  units={['px']}
684
684
  />
@@ -688,7 +688,7 @@ export function TextSection() {
688
688
  property={`textShadow-${i}-blur`}
689
689
  onChange={(_p, v) => {
690
690
  const num = parseFloat(v)
691
- if (!isNaN(num)) updateShadow(i, { blur: Math.max(0, num) })
691
+ if (!Number.isNaN(num)) updateShadow(i, { blur: Math.max(0, num) })
692
692
  }}
693
693
  units={['px']}
694
694
  min={0}
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import type React from 'react'
2
2
 
3
3
  export function FlexRowIcon(props: React.SVGProps<SVGSVGElement>) {
4
4
  return (
@@ -118,7 +118,7 @@ function ScrubValue({
118
118
  const commitEdit = useCallback(() => {
119
119
  setIsEditing(false)
120
120
  const n = parseFloat(editValue)
121
- if (!isNaN(n)) {
121
+ if (!Number.isNaN(n)) {
122
122
  onChange(property, formatCSSValue(Math.max(0, n), unit))
123
123
  }
124
124
  }, [editValue, onChange, property, unit])
@@ -209,7 +209,7 @@ export function BoxModelPreview({
209
209
 
210
210
  // Dim-display helper for the content dimension text
211
211
  const dimText =
212
- (contentW ? `${contentW}` : '–') + ' × ' + (contentH ? `${contentH}` : '–')
212
+ `${contentW ? `${contentW}` : '–'} × ${contentH ? `${contentH}` : '–'}`
213
213
 
214
214
  return (
215
215
  <div className="pt-1.5" style={{ borderTop: '1px solid var(--border)' }}>
@@ -154,7 +154,7 @@ export function CompactInput({
154
154
  onChange(property, 'auto')
155
155
  } else {
156
156
  const n = parseFloat(num)
157
- if (!isNaN(n)) {
157
+ if (!Number.isNaN(n)) {
158
158
  const clamped = clampValue(n)
159
159
  onChange(property, formatCSSValue(clamped, u))
160
160
  }
@@ -173,7 +173,7 @@ export function CompactInput({
173
173
  onChange(property, 'auto')
174
174
  } else {
175
175
  const num = parseFloat(localValue || '0')
176
- if (!isNaN(num)) {
176
+ if (!Number.isNaN(num)) {
177
177
  const clamped = clampValue(num)
178
178
  setLocalValue(String(clamped))
179
179
  onChange(property, formatCSSValue(clamped, nextUnit))
@@ -54,6 +54,7 @@ export function DraggableLabel({
54
54
  value === 'auto' ||
55
55
  value === 'none' ||
56
56
  value === 'normal' ||
57
+ Number.
57
58
  isNaN(parsed.number)
58
59
  )
59
60
  return
@@ -98,7 +99,7 @@ export function DraggableLabel({
98
99
  value !== 'auto' &&
99
100
  value !== 'none' &&
100
101
  value !== 'normal' &&
101
- !isNaN(parsed.number)
102
+ !Number.isNaN(parsed.number)
102
103
 
103
104
  return (
104
105
  <span
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import React from 'react'
3
+ import type React from 'react'
4
4
 
5
5
  interface IconToggleOption {
6
6
  value: string
@@ -14,7 +14,7 @@ interface LinkedInputPairProps {
14
14
  units?: string[]
15
15
  }
16
16
 
17
- function areAllEqual(values: {
17
+ function _areAllEqual(values: {
18
18
  top: string
19
19
  right: string
20
20
  bottom: string
@@ -68,12 +68,12 @@ export function LinkedInputPair({
68
68
  }
69
69
  }, [values])
70
70
 
71
- const handleLinkedHChange = (property: string, value: string) => {
71
+ const handleLinkedHChange = (_property: string, value: string) => {
72
72
  onChange(properties.left, value)
73
73
  onChange(properties.right, value)
74
74
  }
75
75
 
76
- const handleLinkedVChange = (property: string, value: string) => {
76
+ const handleLinkedVChange = (_property: string, value: string) => {
77
77
  onChange(properties.top, value)
78
78
  onChange(properties.bottom, value)
79
79
  }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import React, { useState } from 'react'
3
+ import type React from 'react'
4
+ import { useState } from 'react'
4
5
 
5
6
  interface SectionHeaderProps {
6
7
  title: string
@@ -1,6 +1,5 @@
1
1
  'use client'
2
2
 
3
- import { useEffect } from 'react'
4
3
  import { useEditorStore } from '@/store'
5
4
 
6
5
  /**
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import React, { useEffect, useCallback } from 'react'
3
+ import type React from 'react'
4
+ import { useEffect, useCallback } from 'react'
4
5
  import { useEditorStore } from '@/store'
5
6
  import type {
6
7
  InspectorToEditorMessage,
@@ -114,7 +115,7 @@ function handleMessage(event: MessageEvent) {
114
115
  sendViaIframe({ type: 'REQUEST_DOM_TREE' })
115
116
  sendViaIframe({ type: 'REQUEST_PAGE_LINKS' })
116
117
  sendViaIframe({ type: 'REQUEST_CSS_VARIABLES' })
117
- setTimeout(function () {
118
+ setTimeout(() => {
118
119
  sendViaIframe({ type: 'REQUEST_COMPONENTS', payload: {} })
119
120
  }, 500)
120
121
 
@@ -265,7 +266,7 @@ function handleMessage(event: MessageEvent) {
265
266
  // Debounced component rescan on DOM changes (2s to avoid
266
267
  // excessive scanning during rapid DOM mutations)
267
268
  if (componentRescanTimer) clearTimeout(componentRescanTimer)
268
- componentRescanTimer = setTimeout(function () {
269
+ componentRescanTimer = setTimeout(() => {
269
270
  componentRescanTimer = null
270
271
  sendViaIframe({ type: 'REQUEST_COMPONENTS', payload: {} })
271
272
  }, 2000)
@@ -60,7 +60,7 @@ export function useTargetUrl() {
60
60
  )
61
61
  return
62
62
 
63
- const delay = RECONNECT_BASE_DELAY_MS * Math.pow(2, retryCountRef.current)
63
+ const delay = RECONNECT_BASE_DELAY_MS * 2 ** retryCountRef.current
64
64
  retryTimeoutRef.current = setTimeout(() => {
65
65
  retryCountRef.current++
66
66
  setConnectionStatus('connecting')
@@ -21,14 +21,14 @@ export function generateSelectorPath(element: Element): string {
21
21
  if (current.className && typeof current.className === 'string') {
22
22
  const classes = current.className.trim().split(/\s+/).filter(Boolean)
23
23
  if (classes.length > 0) {
24
- selector += '.' + classes.join('.')
24
+ selector += `.${classes.join('.')}`
25
25
  }
26
26
  }
27
27
 
28
28
  const parent = current.parentElement
29
29
  if (parent) {
30
30
  const siblings = Array.from(parent.children).filter(
31
- (child) => child.tagName === current!.tagName,
31
+ (child) => child.tagName === current?.tagName,
32
32
  )
33
33
  if (siblings.length > 1) {
34
34
  const index = siblings.indexOf(current) + 1
@@ -16,11 +16,11 @@ export function createHoverHighlighter() {
16
16
 
17
17
  function getElementLabel(el: Element): string {
18
18
  const tag = el.tagName.toLowerCase()
19
- if (el.id) return tag + '#' + el.id
19
+ if (el.id) return `${tag}#${el.id}`
20
20
  const cls = el.className
21
21
  if (cls && typeof cls === 'string') {
22
22
  const first = cls.trim().split(/\s+/)[0]
23
- if (first) return tag + '.' + first
23
+ if (first) return `${tag}.${first}`
24
24
  }
25
25
  return tag
26
26
  }
@@ -28,10 +28,10 @@ export function createHoverHighlighter() {
28
28
  return {
29
29
  show(el: Element, rect: DOMRect) {
30
30
  overlay.style.display = 'block'
31
- overlay.style.top = rect.top + 'px'
32
- overlay.style.left = rect.left + 'px'
33
- overlay.style.width = rect.width + 'px'
34
- overlay.style.height = rect.height + 'px'
31
+ overlay.style.top = `${rect.top}px`
32
+ overlay.style.left = `${rect.left}px`
33
+ overlay.style.width = `${rect.width}px`
34
+ overlay.style.height = `${rect.height}px`
35
35
  label.textContent = getElementLabel(el)
36
36
  // Flip label below if near top
37
37
  if (rect.top < 20) {
@@ -12,10 +12,10 @@ export function createSelectionHighlighter() {
12
12
  return {
13
13
  show(rect: DOMRect) {
14
14
  overlay.style.display = 'block'
15
- overlay.style.top = rect.top + 'px'
16
- overlay.style.left = rect.left + 'px'
17
- overlay.style.width = rect.width + 'px'
18
- overlay.style.height = rect.height + 'px'
15
+ overlay.style.top = `${rect.top}px`
16
+ overlay.style.left = `${rect.left}px`
17
+ overlay.style.width = `${rect.width}px`
18
+ overlay.style.height = `${rect.height}px`
19
19
  },
20
20
  hide() {
21
21
  overlay.style.display = 'none'
@@ -8,7 +8,6 @@
8
8
 
9
9
  import type {
10
10
  FileMap,
11
- ComponentEntry,
12
11
  RouteEntry,
13
12
  SourceInfo,
14
13
  } from '@/types/claude'
@@ -328,7 +327,7 @@ export function inferSourcePath(opts: {
328
327
  const abs = opts.sourceInfo.fileName
329
328
  if (opts.projectRoot) {
330
329
  const root = opts.projectRoot.replace(/\/$/, '')
331
- if (abs.startsWith(root + '/')) {
330
+ if (abs.startsWith(`${root}/`)) {
332
331
  return abs.substring(root.length + 1)
333
332
  }
334
333
  }
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync } from 'node:fs'
2
- import { spawn, execFileSync } from 'node:child_process'
2
+ import { spawn, } from 'node:child_process'
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
@@ -97,7 +97,7 @@ function filePathToUrlPattern(relativePath: string): string {
97
97
  const parts = relativePath.split('/')
98
98
  const routeParts = parts.slice(1, -1)
99
99
  const filtered = routeParts.filter((p) => !p.startsWith('('))
100
- return '/' + filtered.join('/')
100
+ return `/${filtered.join('/')}`
101
101
  }
102
102
 
103
103
  interface Collectors {
@@ -201,14 +201,14 @@ function detectFramework(
201
201
  devDeps: Record<string, string>,
202
202
  ): string | null {
203
203
  const all = { ...deps, ...devDeps }
204
- if (all['next']) return 'Next.js'
205
- if (all['@remix-run/react'] || all['remix']) return 'Remix'
206
- if (all['gatsby']) return 'Gatsby'
207
- if (all['astro']) return 'Astro'
204
+ if (all.next) return 'Next.js'
205
+ if (all['@remix-run/react'] || all.remix) return 'Remix'
206
+ if (all.gatsby) return 'Gatsby'
207
+ if (all.astro) return 'Astro'
208
208
  if (all['@angular/core']) return 'Angular'
209
- if (all['vue']) return 'Vue'
210
- if (all['svelte']) return 'Svelte'
211
- if (all['react']) return 'React'
209
+ if (all.vue) return 'Vue'
210
+ if (all.svelte) return 'Svelte'
211
+ if (all.react) return 'React'
212
212
  return null
213
213
  }
214
214
 
@@ -220,13 +220,13 @@ function detectCssStrategy(
220
220
  ): string[] {
221
221
  const all = { ...deps, ...devDeps }
222
222
  const strategies: string[] = []
223
- if (all['tailwindcss']) strategies.push('Tailwind')
223
+ if (all.tailwindcss) strategies.push('Tailwind')
224
224
  if (hasCssModules) strategies.push('CSS Modules')
225
225
  if (all['styled-components']) strategies.push('styled-components')
226
226
  if (all['@emotion/react'] || all['@emotion/styled'])
227
227
  strategies.push('Emotion')
228
- if (all['sass'] || all['node-sass']) strategies.push('Sass')
229
- if (all['less']) strategies.push('Less')
228
+ if (all.sass || all['node-sass']) strategies.push('Sass')
229
+ if (all.less) strategies.push('Less')
230
230
  if (all['@vanilla-extract/css']) strategies.push('Vanilla Extract')
231
231
  if (strategies.length === 0 && cssFiles.length > 0) strategies.push('CSS')
232
232
  return strategies
@@ -270,7 +270,7 @@ export async function scanProjectClient(
270
270
 
271
271
  for (const dir of candidateDirs) {
272
272
  const alreadyCovered = scannedRoots.some((root) =>
273
- dir.startsWith(root + '/'),
273
+ dir.startsWith(`${root}/`),
274
274
  )
275
275
  if (alreadyCovered) continue
276
276
 
@@ -312,7 +312,7 @@ export async function scanProjectClient(
312
312
  entry.kind === 'directory' &&
313
313
  ASSET_DIR_NAMES.has(name.toLowerCase())
314
314
  ) {
315
- collectors.assetDirs.add('public/' + name)
315
+ collectors.assetDirs.add(`public/${name}`)
316
316
  }
317
317
  if (entry.kind === 'file') {
318
318
  collectors.assetDirs.add('public')
@@ -37,7 +37,7 @@ export function groupVariablesIntoFamilies(
37
37
  if (!prefixMap.has(prefix)) {
38
38
  prefixMap.set(prefix, [])
39
39
  }
40
- prefixMap.get(prefix)!.push({
40
+ prefixMap.get(prefix)?.push({
41
41
  name,
42
42
  suffix,
43
43
  value: def.value,
@@ -57,7 +57,10 @@ export async function pickFolder(): Promise<FolderPickResult> {
57
57
 
58
58
  async function pickFolderClient(): Promise<FolderPickResult> {
59
59
  try {
60
- const handle = await window.showDirectoryPicker!({ mode: 'read' })
60
+ if (!window.showDirectoryPicker) {
61
+ return { type: 'error', message: 'Folder picker is not supported' }
62
+ }
63
+ const handle = await window.showDirectoryPicker({ mode: 'read' })
61
64
  return { type: 'handle', handle, name: handle.name }
62
65
  } catch (err) {
63
66
  if (err instanceof DOMException && err.name === 'AbortError') {