@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,204 @@
1
+ import { useCallback } from 'react'
2
+ import { useEditorStore } from '@/store'
3
+ import { scanProjectClient } from '@/lib/clientProjectScanner'
4
+ import { getApiBase } from '@/lib/apiBase'
5
+ import type { RouteEntry } from '@/types/claude'
6
+
7
+ export interface ScanCallbacks {
8
+ onSuccess?: (count: number, projectName: string) => void
9
+ onError?: (message: string) => void
10
+ }
11
+
12
+ export interface ScanResult {
13
+ success: boolean
14
+ count: number
15
+ projectName: string
16
+ pageCount: number
17
+ cssFileCount: number
18
+ assetDirCount: number
19
+ framework: string | null
20
+ cssStrategy: string[]
21
+ error?: string
22
+ }
23
+
24
+ const EMPTY_RESULT: ScanResult = {
25
+ success: false,
26
+ count: 0,
27
+ projectName: '',
28
+ pageCount: 0,
29
+ cssFileCount: 0,
30
+ assetDirCount: 0,
31
+ framework: null,
32
+ cssStrategy: [],
33
+ }
34
+
35
+ /**
36
+ * Shared hook that triggers a project scan and updates the store.
37
+ * Captures components, pages/routes, CSS files, assets, and framework info.
38
+ *
39
+ * - `triggerScan(rootPath)` — server-side scan via /api/project-scan (localhost only)
40
+ * - `triggerClientScan(handle)` — client-side scan via File System Access API (works on Vercel)
41
+ */
42
+ export function useProjectScan() {
43
+ const setScanStatus = useEditorStore((s) => s.setScanStatus)
44
+ const setScanError = useEditorStore((s) => s.setScanError)
45
+ const setComponentFileMap = useEditorStore((s) => s.setComponentFileMap)
46
+ const setScannedProjectName = useEditorStore((s) => s.setScannedProjectName)
47
+ const setProjectScan = useEditorStore((s) => s.setProjectScan)
48
+ const pendingTargetUrl = useEditorStore((s) => s.pendingTargetUrl)
49
+ const targetUrl = useEditorStore((s) => s.targetUrl)
50
+
51
+ /** Populate store from scan data and return a ScanResult. */
52
+ const finalizeScan = useCallback(
53
+ (
54
+ data: {
55
+ componentFileMap: Record<string, string>
56
+ projectName: string
57
+ routes: RouteEntry[]
58
+ cssFiles: string[]
59
+ assetDirs: string[]
60
+ framework: string | null
61
+ cssStrategy: string[]
62
+ srcDirs: string[]
63
+ },
64
+ callbacks?: ScanCallbacks,
65
+ ): ScanResult => {
66
+ setComponentFileMap(data.componentFileMap)
67
+ setScannedProjectName(data.projectName)
68
+ setScanStatus('complete')
69
+
70
+ const count = Object.keys(data.componentFileMap || {}).length
71
+ const pageCount = data.routes.filter((r) => r.type === 'page').length
72
+
73
+ const url = pendingTargetUrl || targetUrl
74
+ if (url) {
75
+ setProjectScan(url, {
76
+ framework: data.framework,
77
+ cssStrategy: data.cssStrategy,
78
+ cssFiles: data.cssFiles,
79
+ srcDirs: data.srcDirs,
80
+ packageName: data.projectName || null,
81
+ assetDirs: data.assetDirs,
82
+ fileMap: {
83
+ routes: data.routes,
84
+ components: Object.entries(data.componentFileMap || {}).map(
85
+ ([name, filePath]) => ({
86
+ name,
87
+ filePath: filePath as string,
88
+ nameLower: name.toLowerCase(),
89
+ category: 'component' as const,
90
+ }),
91
+ ),
92
+ },
93
+ })
94
+ }
95
+
96
+ callbacks?.onSuccess?.(count, data.projectName)
97
+ return {
98
+ success: true,
99
+ count,
100
+ projectName: data.projectName,
101
+ pageCount,
102
+ cssFileCount: data.cssFiles.length,
103
+ assetDirCount: data.assetDirs.length,
104
+ framework: data.framework,
105
+ cssStrategy: data.cssStrategy,
106
+ }
107
+ },
108
+ [
109
+ setComponentFileMap,
110
+ setScannedProjectName,
111
+ setScanStatus,
112
+ setProjectScan,
113
+ pendingTargetUrl,
114
+ targetUrl,
115
+ ],
116
+ )
117
+
118
+ /** Server-side scan (localhost only). */
119
+ const triggerScan = useCallback(
120
+ async (
121
+ rootPath: string,
122
+ callbacks?: ScanCallbacks,
123
+ ): Promise<ScanResult> => {
124
+ setScanStatus('scanning')
125
+ setScanError(null)
126
+ try {
127
+ const res = await fetch(`${getApiBase()}/api/project-scan`, {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: JSON.stringify({ projectRoot: rootPath }),
131
+ })
132
+ if (res.ok) {
133
+ const data = await res.json()
134
+ return finalizeScan(
135
+ {
136
+ componentFileMap: data.componentFileMap || {},
137
+ projectName: data.projectName || 'unknown',
138
+ routes: data.routes || [],
139
+ cssFiles: data.cssFiles || [],
140
+ assetDirs: data.assetDirs || [],
141
+ framework: data.framework || null,
142
+ cssStrategy: data.cssStrategy || [],
143
+ srcDirs: data.srcDirs || [],
144
+ },
145
+ callbacks,
146
+ )
147
+ } else {
148
+ const errData = await res
149
+ .json()
150
+ .catch(() => ({ error: 'Scan failed' }))
151
+ const message = errData.error || 'Scan failed'
152
+ setScanStatus('error')
153
+ setScanError(message)
154
+ callbacks?.onError?.(message)
155
+ return { ...EMPTY_RESULT, error: message }
156
+ }
157
+ } catch {
158
+ const message = 'Network error during scan'
159
+ setScanStatus('error')
160
+ setScanError(message)
161
+ callbacks?.onError?.(message)
162
+ return { ...EMPTY_RESULT, error: message }
163
+ }
164
+ },
165
+ [setScanStatus, setScanError, finalizeScan],
166
+ )
167
+
168
+ /** Client-side scan using File System Access API (works on Vercel). */
169
+ const triggerClientScan = useCallback(
170
+ async (
171
+ handle: FileSystemDirectoryHandle,
172
+ callbacks?: ScanCallbacks,
173
+ ): Promise<ScanResult> => {
174
+ setScanStatus('scanning')
175
+ setScanError(null)
176
+ try {
177
+ const data = await scanProjectClient(handle)
178
+ return finalizeScan(
179
+ {
180
+ componentFileMap: data.componentFileMap,
181
+ projectName: data.projectName,
182
+ routes: data.routes,
183
+ cssFiles: data.cssFiles,
184
+ assetDirs: data.assetDirs,
185
+ framework: data.framework,
186
+ cssStrategy: data.cssStrategy,
187
+ srcDirs: data.srcDirs,
188
+ },
189
+ callbacks,
190
+ )
191
+ } catch (err) {
192
+ const message =
193
+ err instanceof Error ? err.message : 'Client scan failed'
194
+ setScanStatus('error')
195
+ setScanError(message)
196
+ callbacks?.onError?.(message)
197
+ return { ...EMPTY_RESULT, error: message }
198
+ }
199
+ },
200
+ [setScanStatus, setScanError, finalizeScan],
201
+ )
202
+
203
+ return { triggerScan, triggerClientScan }
204
+ }
@@ -0,0 +1,20 @@
1
+ import { useCallback } from 'react'
2
+ import { useEditorStore } from '@/store'
3
+
4
+ export function useResizable(side: 'left' | 'right') {
5
+ const width = useEditorStore((s) =>
6
+ side === 'left' ? s.leftPanelWidth : s.rightPanelWidth,
7
+ )
8
+ const setWidth = useEditorStore((s) =>
9
+ side === 'left' ? s.setLeftPanelWidth : s.setRightPanelWidth,
10
+ )
11
+
12
+ const handleResize = useCallback(
13
+ (newWidth: number) => {
14
+ setWidth(newWidth)
15
+ },
16
+ [setWidth],
17
+ )
18
+
19
+ return { width, handleResize }
20
+ }
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+
3
+ import { useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { usePostMessage } from './usePostMessage'
6
+
7
+ /**
8
+ * Hook that manages element selection with bidirectional sync.
9
+ * Click tree node → sends SELECT_ELEMENT to inspector.
10
+ * Inspector click → updates store (handled by usePostMessage).
11
+ */
12
+ export function useSelectedElement() {
13
+ const selectorPath = useEditorStore((s) => s.selectorPath)
14
+ const tagName = useEditorStore((s) => s.tagName)
15
+ const className = useEditorStore((s) => s.className)
16
+ const elementId = useEditorStore((s) => s.elementId)
17
+ const attributes = useEditorStore((s) => s.attributes)
18
+ const innerText = useEditorStore((s) => s.innerText)
19
+ const computedStyles = useEditorStore((s) => s.computedStyles)
20
+ const boundingRect = useEditorStore((s) => s.boundingRect)
21
+ const clearSelection = useEditorStore((s) => s.clearSelection)
22
+ const { sendToInspector } = usePostMessage()
23
+
24
+ const selectFromTree = useCallback(
25
+ (selectorPath: string) => {
26
+ sendToInspector({
27
+ type: 'SELECT_ELEMENT',
28
+ payload: { selectorPath },
29
+ })
30
+ },
31
+ [sendToInspector],
32
+ )
33
+
34
+ const deselect = useCallback(() => {
35
+ clearSelection()
36
+ }, [clearSelection])
37
+
38
+ return {
39
+ selectorPath,
40
+ tagName,
41
+ className,
42
+ elementId,
43
+ attributes,
44
+ innerText,
45
+ computedStyles,
46
+ boundingRect,
47
+ selectFromTree,
48
+ deselect,
49
+ isSelected: selectorPath !== null,
50
+ }
51
+ }
@@ -0,0 +1,81 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { usePostMessage } from './usePostMessage'
6
+ import {
7
+ HEARTBEAT_INTERVAL_MS,
8
+ RECONNECT_MAX_RETRIES,
9
+ RECONNECT_BASE_DELAY_MS,
10
+ } from '@/lib/constants'
11
+
12
+ export function useTargetUrl() {
13
+ const connectionStatus = useEditorStore((s) => s.connectionStatus)
14
+ const targetUrl = useEditorStore((s) => s.targetUrl)
15
+ const setConnectionStatus = useEditorStore((s) => s.setConnectionStatus)
16
+ const { sendHeartbeat } = usePostMessage()
17
+
18
+ const heartbeatIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
19
+ null,
20
+ )
21
+ const retryCountRef = useRef(0)
22
+ const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
23
+
24
+ const startHeartbeat = useCallback(() => {
25
+ if (heartbeatIntervalRef.current)
26
+ clearInterval(heartbeatIntervalRef.current)
27
+
28
+ heartbeatIntervalRef.current = setInterval(async () => {
29
+ const alive = await sendHeartbeat()
30
+ if (!alive) {
31
+ setConnectionStatus('disconnected')
32
+ }
33
+ }, HEARTBEAT_INTERVAL_MS)
34
+ }, [sendHeartbeat, setConnectionStatus])
35
+
36
+ const stopHeartbeat = useCallback(() => {
37
+ if (heartbeatIntervalRef.current) {
38
+ clearInterval(heartbeatIntervalRef.current)
39
+ heartbeatIntervalRef.current = null
40
+ }
41
+ }, [])
42
+
43
+ // Start heartbeat when connected
44
+ useEffect(() => {
45
+ if (connectionStatus === 'connected') {
46
+ retryCountRef.current = 0
47
+ startHeartbeat()
48
+ } else {
49
+ stopHeartbeat()
50
+ }
51
+ return stopHeartbeat
52
+ }, [connectionStatus, startHeartbeat, stopHeartbeat])
53
+
54
+ // Auto-reconnect on disconnect (confirming/scanning have null targetUrl so they won't trigger this)
55
+ useEffect(() => {
56
+ if (
57
+ connectionStatus !== 'disconnected' ||
58
+ !targetUrl ||
59
+ retryCountRef.current >= RECONNECT_MAX_RETRIES
60
+ )
61
+ return
62
+
63
+ const delay = RECONNECT_BASE_DELAY_MS * Math.pow(2, retryCountRef.current)
64
+ retryTimeoutRef.current = setTimeout(() => {
65
+ retryCountRef.current++
66
+ setConnectionStatus('connecting')
67
+ }, delay)
68
+
69
+ return () => {
70
+ if (retryTimeoutRef.current) {
71
+ clearTimeout(retryTimeoutRef.current)
72
+ }
73
+ }
74
+ }, [connectionStatus, targetUrl, setConnectionStatus])
75
+
76
+ return {
77
+ isConnected: connectionStatus === 'connected',
78
+ isConnecting: connectionStatus === 'connecting',
79
+ isDisconnected: connectionStatus === 'disconnected',
80
+ }
81
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * DOM tree serialization for the inspector.
3
+ * Converts live DOM into TreeNode structure for the editor's left panel.
4
+ */
5
+
6
+ import type { TreeNode } from '@/types/tree'
7
+
8
+ export function generateSelectorPath(element: Element): string {
9
+ const parts: string[] = []
10
+ let current: Element | null = element
11
+
12
+ while (current && current !== document.documentElement) {
13
+ let selector = current.tagName.toLowerCase()
14
+
15
+ if (current.id) {
16
+ selector += `#${current.id}`
17
+ parts.unshift(selector)
18
+ break
19
+ }
20
+
21
+ if (current.className && typeof current.className === 'string') {
22
+ const classes = current.className.trim().split(/\s+/).filter(Boolean)
23
+ if (classes.length > 0) {
24
+ selector += '.' + classes.join('.')
25
+ }
26
+ }
27
+
28
+ const parent = current.parentElement
29
+ if (parent) {
30
+ const siblings = Array.from(parent.children).filter(
31
+ (child) => child.tagName === current!.tagName,
32
+ )
33
+ if (siblings.length > 1) {
34
+ const index = siblings.indexOf(current) + 1
35
+ selector += `:nth-of-type(${index})`
36
+ }
37
+ }
38
+
39
+ parts.unshift(selector)
40
+ current = current.parentElement
41
+ }
42
+
43
+ return parts.join(' > ')
44
+ }
45
+
46
+ const SKIP_TAGS = new Set(['script', 'style', 'link', 'noscript'])
47
+
48
+ export function serializeTree(element: Element): TreeNode | null {
49
+ if (!element || element.nodeType !== 1) return null
50
+
51
+ const tagName = element.tagName.toLowerCase()
52
+ if (SKIP_TAGS.has(tagName)) return null
53
+
54
+ const children: TreeNode[] = []
55
+ for (let i = 0; i < element.children.length; i++) {
56
+ const child = serializeTree(element.children[i])
57
+ if (child) children.push(child)
58
+ }
59
+
60
+ return {
61
+ id: generateSelectorPath(element),
62
+ tagName,
63
+ className:
64
+ element.className && typeof element.className === 'string'
65
+ ? element.className
66
+ : null,
67
+ elementId: element.id || null,
68
+ children,
69
+ imgSrc: tagName === 'img' ? element.getAttribute('src') || null : null,
70
+ }
71
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Click handler in capture phase for element selection.
3
+ * Source reference — runtime code is inlined in the proxy route.
4
+ */
5
+
6
+ export type SelectCallback = (element: Element) => void
7
+
8
+ export function createElementSelector(onSelect: SelectCallback) {
9
+ const handler = (e: MouseEvent) => {
10
+ e.preventDefault()
11
+ e.stopPropagation()
12
+ const el = document.elementFromPoint(e.clientX, e.clientY)
13
+ if (el) onSelect(el)
14
+ }
15
+
16
+ document.addEventListener('click', handler, true)
17
+
18
+ return {
19
+ destroy() {
20
+ document.removeEventListener('click', handler, true)
21
+ },
22
+ }
23
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * visual-editor-style hover highlight — dotted green border + element name label.
3
+ * Source reference — runtime code is inlined in the proxy route.
4
+ */
5
+
6
+ export function createHoverHighlighter() {
7
+ const overlay = document.createElement('div')
8
+ overlay.style.cssText =
9
+ 'position:fixed;pointer-events:none;z-index:999996;border:1px dashed rgba(74,222,128,0.30);display:none;transition:top 0.04s,left 0.04s,width 0.04s,height 0.04s;'
10
+ document.body.appendChild(overlay)
11
+
12
+ const label = document.createElement('div')
13
+ label.style.cssText =
14
+ 'position:absolute;top:-18px;left:-1px;padding:1px 6px;font-size:10px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;line-height:14px;color:#fff;background:rgba(74,222,128,0.70);border-radius:3px 3px 0 0;white-space:nowrap;pointer-events:none;'
15
+ overlay.appendChild(label)
16
+
17
+ function getElementLabel(el: Element): string {
18
+ const tag = el.tagName.toLowerCase()
19
+ if (el.id) return tag + '#' + el.id
20
+ const cls = el.className
21
+ if (cls && typeof cls === 'string') {
22
+ const first = cls.trim().split(/\s+/)[0]
23
+ if (first) return tag + '.' + first
24
+ }
25
+ return tag
26
+ }
27
+
28
+ return {
29
+ show(el: Element, rect: DOMRect) {
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'
35
+ label.textContent = getElementLabel(el)
36
+ // Flip label below if near top
37
+ if (rect.top < 20) {
38
+ label.style.top = 'auto'
39
+ label.style.bottom = '-18px'
40
+ label.style.borderRadius = '0 0 3px 3px'
41
+ } else {
42
+ label.style.top = '-18px'
43
+ label.style.bottom = 'auto'
44
+ label.style.borderRadius = '3px 3px 0 0'
45
+ }
46
+ },
47
+ hide() {
48
+ overlay.style.display = 'none'
49
+ },
50
+ destroy() {
51
+ overlay.remove()
52
+ },
53
+ }
54
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Persistent outline on selected element.
3
+ * Source reference — runtime code is inlined in the proxy route.
4
+ */
5
+
6
+ export function createSelectionHighlighter() {
7
+ const overlay = document.createElement('div')
8
+ overlay.style.cssText =
9
+ 'position:fixed;pointer-events:none;z-index:999997;border:2px solid #4a9eff;display:none;'
10
+ document.body.appendChild(overlay)
11
+
12
+ return {
13
+ show(rect: DOMRect) {
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'
19
+ },
20
+ hide() {
21
+ overlay.style.display = 'none'
22
+ },
23
+ destroy() {
24
+ overlay.remove()
25
+ },
26
+ }
27
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Extract computed styles for the design panel.
3
+ * Source reference — runtime code is inlined in the proxy route.
4
+ */
5
+
6
+ import { ALL_EDITABLE_PROPERTIES } from '@/lib/constants'
7
+
8
+ export function getComputedStylesForElement(
9
+ element: Element,
10
+ ): Record<string, string> {
11
+ const computed = window.getComputedStyle(element)
12
+ const styles: Record<string, string> = {}
13
+
14
+ for (const prop of ALL_EDITABLE_PROPERTIES) {
15
+ styles[prop] = computed.getPropertyValue(prop)
16
+ }
17
+
18
+ return styles
19
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Inspector entry point.
3
+ * This file is the source for the inspector IIFE injected into the iframe.
4
+ * The actual runtime code is inlined in the proxy route for simplicity.
5
+ * This module serves as the canonical source reference.
6
+ *
7
+ * Modules:
8
+ * - DOMTraverser: Serializes DOM to TreeNode
9
+ * - HoverHighlighter: Blue overlay on mousemove
10
+ * - ElementSelector: Click to select with computed styles
11
+ * - SelectionHighlighter: Persistent outline on selected element
12
+ * - StyleExtractor: getComputedStyle reader
13
+ * - messaging: postMessage send/receive bridge
14
+ */
15
+
16
+ export { generateSelectorPath, serializeTree } from './DOMTraverser'
17
+ export { getComputedStylesForElement } from './StyleExtractor'
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Inspector messaging bridge.
3
+ * Handles postMessage send/receive with origin checking.
4
+ * Runtime code is inlined in the proxy; this is the source reference.
5
+ */
6
+
7
+ import type {
8
+ InspectorToEditorMessage,
9
+ EditorToInspectorMessage,
10
+ } from '@/types/messages'
11
+
12
+ const parentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
13
+
14
+ export function sendToEditor(message: InspectorToEditorMessage): void {
15
+ window.parent.postMessage(message, parentOrigin)
16
+ }
17
+
18
+ export type MessageHandler = (message: EditorToInspectorMessage) => void
19
+
20
+ export function listenForEditorMessages(handler: MessageHandler): () => void {
21
+ const listener = (event: MessageEvent) => {
22
+ if (event.origin !== parentOrigin) return
23
+ const data = event.data
24
+ if (!data || !data.type) return
25
+ handler(data as EditorToInspectorMessage)
26
+ }
27
+
28
+ window.addEventListener('message', listener)
29
+ return () => window.removeEventListener('message', listener)
30
+ }
@@ -0,0 +1,15 @@
1
+ import { useEditorStore } from '@/store'
2
+
3
+ /**
4
+ * Returns the base URL for API calls.
5
+ *
6
+ * - When running locally: returns '' (same-origin).
7
+ * - When running remotely with bridge: returns the bridge URL (e.g. 'http://localhost:4002').
8
+ * - When running remotely without bridge: returns '' (calls go to Vercel routes).
9
+ */
10
+ export function getApiBase(): string {
11
+ if (typeof window === 'undefined') return ''
12
+ const h = window.location.hostname
13
+ if (h === 'localhost' || h === '127.0.0.1') return ''
14
+ return useEditorStore.getState().bridgeUrl || ''
15
+ }