@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,2400 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { PROXY_HEADER } from '@/lib/constants'
3
+
4
+ const INSPECTOR_SCRIPT = `
5
+ <script>
6
+ (function() {
7
+ ${getInspectorCode()}
8
+ })();
9
+ </script>
10
+ `
11
+
12
+ function getInspectorCode(): string {
13
+ // The inspector code will be inlined here during build.
14
+ // For now, load it dynamically via a separate endpoint.
15
+ return `
16
+ // Inspector bootstrap - sends INSPECTOR_READY and sets up message handling
17
+ var DEV_EDITOR_INSPECTOR = (function() {
18
+ var parentOrigin = '*';
19
+ try { parentOrigin = window.parent.location.origin; } catch(e) { parentOrigin = '*'; }
20
+
21
+ function send(message) {
22
+ try { window.parent.postMessage(message, parentOrigin); } catch(e) {}
23
+ }
24
+
25
+ function generateSelectorPath(element) {
26
+ var parts = [];
27
+ var current = element;
28
+ while (current && current !== document.documentElement) {
29
+ var selector = current.tagName.toLowerCase();
30
+ if (current.id) {
31
+ selector += '#' + CSS.escape(current.id);
32
+ parts.unshift(selector);
33
+ break;
34
+ }
35
+ if (current.className && typeof current.className === 'string') {
36
+ var classes = current.className.trim().split(/\\s+/).filter(Boolean);
37
+ if (classes.length > 0) {
38
+ selector += '.' + classes.map(function(c) { return CSS.escape(c); }).join('.');
39
+ }
40
+ }
41
+ var parent = current.parentElement;
42
+ if (parent) {
43
+ var siblings = Array.from(parent.children).filter(function(c) {
44
+ return c.tagName === current.tagName;
45
+ });
46
+ if (siblings.length > 1) {
47
+ var index = siblings.indexOf(current) + 1;
48
+ selector += ':nth-of-type(' + index + ')';
49
+ }
50
+ }
51
+ parts.unshift(selector);
52
+ current = current.parentElement;
53
+ }
54
+ return parts.join(' > ');
55
+ }
56
+
57
+ function serializeTree(element) {
58
+ if (!element || element.nodeType !== 1) return null;
59
+ var tagName = element.tagName.toLowerCase();
60
+ if (tagName === 'script' || tagName === 'style' || tagName === 'link') return null;
61
+ var children = [];
62
+ for (var i = 0; i < element.children.length; i++) {
63
+ var child = serializeTree(element.children[i]);
64
+ if (child) children.push(child);
65
+ }
66
+ return {
67
+ id: generateSelectorPath(element),
68
+ tagName: tagName,
69
+ className: element.className && typeof element.className === 'string' ? element.className : null,
70
+ elementId: element.id || null,
71
+ children: children
72
+ };
73
+ }
74
+
75
+ function toCC(s) {
76
+ if (s.charAt(0) === '-') s = s.substring(1);
77
+ return s.replace(/-([a-z])/g, function(_, c) { return c.toUpperCase(); });
78
+ }
79
+ function toKC(s) {
80
+ var k = s.replace(/[A-Z]/g, function(c) { return '-' + c.toLowerCase(); });
81
+ if (/^(webkit|moz|ms)-/.test(k)) k = '-' + k;
82
+ return k;
83
+ }
84
+
85
+ function getComputedStylesForElement(el) {
86
+ var computed = window.getComputedStyle(el);
87
+ var props = [
88
+ 'width','height','min-width','min-height','max-width','max-height',
89
+ 'overflow','overflow-x','overflow-y','box-sizing',
90
+ 'margin-top','margin-right','margin-bottom','margin-left',
91
+ 'padding-top','padding-right','padding-bottom','padding-left',
92
+ 'font-family','font-size','font-weight','font-style','line-height','letter-spacing',
93
+ 'text-align','text-decoration','text-transform','text-indent','text-overflow','text-shadow','color',
94
+ 'direction','word-break','line-break','white-space','column-count',
95
+ '-webkit-text-stroke-width','-webkit-text-stroke-color',
96
+ 'border-width','border-style','border-color','border-radius',
97
+ 'border-top-width','border-right-width','border-bottom-width','border-left-width',
98
+ 'border-top-left-radius','border-top-right-radius',
99
+ 'border-bottom-right-radius','border-bottom-left-radius',
100
+ 'background-color','background-image',
101
+ 'background-size','background-position','background-repeat','background-attachment','background-clip',
102
+ 'opacity','visibility','cursor','mix-blend-mode','pointer-events',
103
+ 'display','flex-direction','justify-content','align-items',
104
+ 'flex-wrap','gap','column-gap','row-gap',
105
+ 'grid-template-columns','grid-template-rows','grid-auto-flow','justify-items',
106
+ 'vertical-align',
107
+ 'position','top','right','bottom','left','z-index','float','clear',
108
+ 'box-shadow','filter'
109
+ ];
110
+ var styles = {};
111
+ for (var i = 0; i < props.length; i++) {
112
+ styles[toCC(props[i])] = computed.getPropertyValue(props[i]);
113
+ }
114
+ return styles;
115
+ }
116
+
117
+ function scanCSSVariableDefinitions() {
118
+ var definitions = {};
119
+ var scopesSet = {};
120
+ var rootStyles = window.getComputedStyle(document.documentElement);
121
+ var FRAMEWORK_PREFIXES = ['--tw-', '--next-', '--radix-', '--chakra-', '--mantine-', '--mui-', '--framer-', '--sb-'];
122
+
123
+ // Detect Tailwind v4 @theme usage — if present, keep --color-*, --font-*, --spacing-* vars
124
+ var hasTailwindTheme = false;
125
+ for (var tsi = 0; tsi < document.styleSheets.length; tsi++) {
126
+ var tSheet = document.styleSheets[tsi];
127
+ try {
128
+ // Check inline <style> content for @theme directive
129
+ if (tSheet.ownerNode && tSheet.ownerNode.textContent && tSheet.ownerNode.textContent.indexOf('@theme') >= 0) {
130
+ hasTailwindTheme = true;
131
+ break;
132
+ }
133
+ // Check linked stylesheet rules for @layer theme or @theme
134
+ var tRules = tSheet.cssRules || tSheet.rules;
135
+ if (tRules) {
136
+ for (var tri = 0; tri < tRules.length; tri++) {
137
+ var tRule = tRules[tri];
138
+ if (tRule.cssText && tRule.cssText.indexOf('@theme') >= 0) {
139
+ hasTailwindTheme = true;
140
+ break;
141
+ }
142
+ }
143
+ }
144
+ } catch(e) { /* cross-origin stylesheet */ }
145
+ if (hasTailwindTheme) break;
146
+ }
147
+
148
+ function classifyScope(selectorText, parentRule) {
149
+ if (!selectorText) return 'custom';
150
+ var sel = selectorText.trim().toLowerCase();
151
+ // Check if inside a @media (prefers-color-scheme: dark) block
152
+ if (parentRule && parentRule.conditionText) {
153
+ var cond = parentRule.conditionText.toLowerCase();
154
+ if (cond.indexOf('prefers-color-scheme') >= 0 && cond.indexOf('dark') >= 0) {
155
+ return 'media-dark';
156
+ }
157
+ }
158
+ if (sel === ':root' || sel === ':root, :host' || sel === ':host, :root') return 'root';
159
+ if (sel.indexOf('.dark') >= 0 || sel.indexOf('[data-theme="dark"]') >= 0 || sel.indexOf('[data-mode="dark"]') >= 0) return 'dark';
160
+ return 'root';
161
+ }
162
+
163
+ function extractFromRules(rules, parentRule) {
164
+ for (var ri = 0; ri < rules.length; ri++) {
165
+ var rule = rules[ri];
166
+ if (rule.cssRules) {
167
+ extractFromRules(rule.cssRules, rule);
168
+ continue;
169
+ }
170
+ if (!rule.style) continue;
171
+ var ruleSelector = rule.selectorText || '';
172
+ var scope = classifyScope(ruleSelector, parentRule);
173
+ if (ruleSelector) scopesSet[ruleSelector] = true;
174
+ for (var pi = 0; pi < rule.style.length; pi++) {
175
+ var prop = rule.style[pi];
176
+ if (prop.indexOf('--') === 0) {
177
+ var rawVal = rule.style.getPropertyValue(prop).trim();
178
+ var resolved = rootStyles.getPropertyValue(prop).trim();
179
+ definitions[prop] = {
180
+ value: rawVal,
181
+ resolvedValue: resolved || rawVal,
182
+ selector: ruleSelector,
183
+ scope: scope
184
+ };
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ var taggedSheets = [];
191
+ for (var si = 0; si < document.styleSheets.length; si++) {
192
+ var sheet = document.styleSheets[si];
193
+ if (sheet.ownerNode && sheet.ownerNode.hasAttribute && sheet.ownerNode.hasAttribute('data-design-tokens')) {
194
+ taggedSheets.push(sheet);
195
+ }
196
+ }
197
+
198
+ if (taggedSheets.length > 0) {
199
+ for (var ti = 0; ti < taggedSheets.length; ti++) {
200
+ var taggedRules;
201
+ try { taggedRules = taggedSheets[ti].cssRules || taggedSheets[ti].rules; } catch(e) { continue; }
202
+ if (taggedRules) extractFromRules(taggedRules, null);
203
+ }
204
+ return { definitions: definitions, isExplicit: true, scopes: Object.keys(scopesSet) };
205
+ }
206
+
207
+ for (var fi = 0; fi < document.styleSheets.length; fi++) {
208
+ var fallbackSheet = document.styleSheets[fi];
209
+ var fallbackRules;
210
+ try { fallbackRules = fallbackSheet.cssRules || fallbackSheet.rules; } catch(e) { continue; }
211
+ if (fallbackRules) extractFromRules(fallbackRules, null);
212
+ }
213
+
214
+ var metaEl = document.querySelector('meta[name="design-tokens-prefix"]');
215
+ var metaPrefixes = null;
216
+ if (metaEl) {
217
+ var content = metaEl.getAttribute('content');
218
+ if (content) {
219
+ metaPrefixes = content.split(',');
220
+ for (var mpi = 0; mpi < metaPrefixes.length; mpi++) {
221
+ metaPrefixes[mpi] = metaPrefixes[mpi].trim();
222
+ }
223
+ }
224
+ }
225
+
226
+ var filtered = {};
227
+ var keys = Object.keys(definitions);
228
+ for (var ki = 0; ki < keys.length; ki++) {
229
+ var key = keys[ki];
230
+ if (metaPrefixes) {
231
+ var allowed = false;
232
+ for (var api = 0; api < metaPrefixes.length; api++) {
233
+ if (key.indexOf(metaPrefixes[api]) === 0) { allowed = true; break; }
234
+ }
235
+ if (allowed) filtered[key] = definitions[key];
236
+ } else {
237
+ var isFramework = false;
238
+ for (var fpi = 0; fpi < FRAMEWORK_PREFIXES.length; fpi++) {
239
+ if (key.indexOf(FRAMEWORK_PREFIXES[fpi]) === 0) { isFramework = true; break; }
240
+ }
241
+ // If Tailwind v4 @theme detected, keep --color-*, --font-*, --spacing-* even if --tw- internal vars are filtered
242
+ if (isFramework && hasTailwindTheme && key.indexOf('--tw-') !== 0) {
243
+ isFramework = false;
244
+ }
245
+ if (!isFramework) filtered[key] = definitions[key];
246
+ }
247
+ }
248
+
249
+ return { definitions: filtered, isExplicit: false, scopes: Object.keys(scopesSet) };
250
+ }
251
+
252
+ function detectCSSVariablesOnElement(el) {
253
+ var usages = {};
254
+ // Check inline style for var() references
255
+ if (el.style) {
256
+ for (var si = 0; si < el.style.length; si++) {
257
+ var inlineProp = el.style[si];
258
+ var inlineVal = el.style.getPropertyValue(inlineProp);
259
+ if (inlineVal && inlineVal.indexOf('var(') >= 0) {
260
+ usages[inlineProp] = inlineVal.trim();
261
+ }
262
+ }
263
+ }
264
+ // Check stylesheet rules that match this element
265
+ for (var shi = 0; shi < document.styleSheets.length; shi++) {
266
+ var sheet = document.styleSheets[shi];
267
+ var rules;
268
+ try { rules = sheet.cssRules || sheet.rules; } catch(e) { continue; }
269
+ if (!rules) continue;
270
+ for (var ri = 0; ri < rules.length; ri++) {
271
+ var rule = rules[ri];
272
+ if (!rule.selectorText || !rule.style) continue;
273
+ var matches = false;
274
+ try { matches = el.matches(rule.selectorText); } catch(e) { continue; }
275
+ if (!matches) continue;
276
+ for (var pi = 0; pi < rule.style.length; pi++) {
277
+ var prop = rule.style[pi];
278
+ var val = rule.style.getPropertyValue(prop);
279
+ if (val && val.indexOf('var(') >= 0) {
280
+ // Don't overwrite inline style usages (higher specificity)
281
+ if (!usages[prop]) {
282
+ usages[prop] = val.trim();
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+ return usages;
289
+ }
290
+
291
+ // Selection highlight
292
+ var selectionOverlay = document.createElement('div');
293
+ selectionOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:999997;border:2px solid #4a9eff;display:none;';
294
+ document.body.appendChild(selectionOverlay);
295
+
296
+ // Hover highlight — dotted green border + element name label (visual-editor-style)
297
+ var hoverOverlay = document.createElement('div');
298
+ hoverOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:999996;border:1px dashed #4ade80;display:none;transition:top 0.04s,left 0.04s,width 0.04s,height 0.04s;';
299
+ document.body.appendChild(hoverOverlay);
300
+
301
+ var hoverLabel = document.createElement('div');
302
+ hoverLabel.style.cssText = '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:#1D3F23;border-radius:3px 3px 0 0;white-space:nowrap;pointer-events:none;';
303
+ hoverOverlay.appendChild(hoverLabel);
304
+
305
+ var hoveredElement = null;
306
+
307
+ function getElementLabel(el) {
308
+ var tag = el.tagName.toLowerCase();
309
+ // Show id if present
310
+ if (el.id) return tag + '#' + el.id;
311
+ // Show first meaningful class
312
+ var cls = el.className;
313
+ if (cls && typeof cls === 'string') {
314
+ var first = cls.trim().split(/\s+/)[0];
315
+ if (first) return tag + '.' + first;
316
+ }
317
+ return tag;
318
+ }
319
+
320
+ document.addEventListener('mousemove', function(e) {
321
+ if (!selectionModeEnabled) { hoverOverlay.style.display = 'none'; return; }
322
+ var el = document.elementFromPoint(e.clientX, e.clientY);
323
+ if (!el || el === hoverOverlay || el === selectionOverlay || el === hoverLabel) return;
324
+ // Don't show hover on already-selected element
325
+ if (el === selectedElement) { hoverOverlay.style.display = 'none'; hoveredElement = null; return; }
326
+ if (el === hoveredElement) return;
327
+ hoveredElement = el;
328
+ var rect = el.getBoundingClientRect();
329
+ hoverOverlay.style.display = 'block';
330
+ hoverOverlay.style.top = rect.top + 'px';
331
+ hoverOverlay.style.left = rect.left + 'px';
332
+ hoverOverlay.style.width = rect.width + 'px';
333
+ hoverOverlay.style.height = rect.height + 'px';
334
+ hoverLabel.textContent = getElementLabel(el);
335
+ // If element is near top of viewport, show label below instead
336
+ if (rect.top < 20) {
337
+ hoverLabel.style.top = 'auto';
338
+ hoverLabel.style.bottom = '-18px';
339
+ hoverLabel.style.borderRadius = '0 0 3px 3px';
340
+ } else {
341
+ hoverLabel.style.top = '-18px';
342
+ hoverLabel.style.bottom = 'auto';
343
+ hoverLabel.style.borderRadius = '3px 3px 0 0';
344
+ }
345
+ }, true);
346
+
347
+ document.addEventListener('mouseleave', function() {
348
+ hoverOverlay.style.display = 'none';
349
+ hoveredElement = null;
350
+ });
351
+
352
+ function getReactSourceInfo(element) {
353
+ // Find React fiber key on DOM element
354
+ var fiberKey = null;
355
+ var keys = Object.keys(element);
356
+ for (var i = 0; i < keys.length; i++) {
357
+ if (keys[i].indexOf('__reactFiber$') === 0 || keys[i].indexOf('__reactInternalInstance$') === 0) {
358
+ fiberKey = keys[i]; break;
359
+ }
360
+ }
361
+ if (!fiberKey) return null;
362
+
363
+ var fiber = element[fiberKey];
364
+ var source = null;
365
+ var componentName = null;
366
+ var componentChain = [];
367
+ var visited = 0;
368
+
369
+ while (fiber && visited < 50) {
370
+ visited++;
371
+ if (!source && fiber._debugSource) {
372
+ source = {
373
+ fileName: fiber._debugSource.fileName || '',
374
+ lineNumber: fiber._debugSource.lineNumber || 0,
375
+ columnNumber: fiber._debugSource.columnNumber
376
+ };
377
+ }
378
+ // Collect component names (function/class components, forwardRef)
379
+ var type = fiber.type;
380
+ if (type) {
381
+ var name = null;
382
+ if (typeof type === 'function') {
383
+ name = type.displayName || type.name;
384
+ } else if (type.$$typeof && (type.render || type.type)) {
385
+ var inner = type.render || type.type;
386
+ if (typeof inner === 'function') name = inner.displayName || inner.name;
387
+ }
388
+ if (name && name !== 'Anonymous' && name.length > 1) {
389
+ if (!componentName) componentName = name;
390
+ componentChain.push(name);
391
+ }
392
+ }
393
+ if (source && componentChain.length >= 10) break;
394
+ fiber = fiber.return;
395
+ }
396
+ if (!source) return null;
397
+ return {
398
+ fileName: source.fileName,
399
+ lineNumber: source.lineNumber,
400
+ columnNumber: source.columnNumber,
401
+ componentName: componentName,
402
+ componentChain: componentChain
403
+ };
404
+ }
405
+
406
+ var selectedElement = null;
407
+ var selectionModeEnabled = true;
408
+
409
+ // Click selection — when selection mode is on, intercept clicks to select elements.
410
+ // When off, let clicks through so links and buttons work normally.
411
+ document.addEventListener('click', function(e) {
412
+ if (!selectionModeEnabled) return;
413
+
414
+ // If text editing is active, clicking outside commits the edit
415
+ if (textEditingActive) {
416
+ var clickedEl = document.elementFromPoint(e.clientX, e.clientY);
417
+ if (clickedEl !== textEditTarget) {
418
+ e.preventDefault();
419
+ e.stopPropagation();
420
+ commitTextEdit();
421
+ }
422
+ return;
423
+ }
424
+
425
+ e.preventDefault();
426
+ e.stopPropagation();
427
+ var el = document.elementFromPoint(e.clientX, e.clientY);
428
+ if (!el || el === selectionOverlay || el === hoverOverlay || el === hoverLabel) return;
429
+ hoverOverlay.style.display = 'none';
430
+ selectElement(el);
431
+ }, true);
432
+
433
+ function selectElement(el) {
434
+ // Don't select elements when selection mode is disabled (preview mode)
435
+ if (!selectionModeEnabled) return;
436
+ selectedElement = el;
437
+ // Hide hover overlay when selecting
438
+ hoverOverlay.style.display = 'none';
439
+ hoveredElement = null;
440
+ var rect = el.getBoundingClientRect();
441
+ selectionOverlay.style.display = 'block';
442
+ selectionOverlay.style.top = rect.top + 'px';
443
+ selectionOverlay.style.left = rect.left + 'px';
444
+ selectionOverlay.style.width = rect.width + 'px';
445
+ selectionOverlay.style.height = rect.height + 'px';
446
+
447
+ // Collect all attributes
448
+ var attrs = {};
449
+ for (var ai = 0; ai < el.attributes.length; ai++) {
450
+ var attr = el.attributes[ai];
451
+ attrs[attr.name] = attr.value;
452
+ }
453
+
454
+ // Collect truncated inner text
455
+ var text = (el.innerText || '').substring(0, 500) || null;
456
+
457
+ var varUsages = detectCSSVariablesOnElement(el);
458
+ console.log('[pAInt] CSS variable usages for', el.tagName, el.className, varUsages);
459
+
460
+ var sourceInfo = getReactSourceInfo(el);
461
+ if (sourceInfo) {
462
+ console.log('[pAInt] sourceInfo:', sourceInfo.fileName + ':' + sourceInfo.lineNumber, 'component:', sourceInfo.componentName, 'chain:', sourceInfo.componentChain.join(' > '));
463
+ }
464
+
465
+ send({
466
+ type: 'ELEMENT_SELECTED',
467
+ payload: {
468
+ selectorPath: generateSelectorPath(el),
469
+ tagName: el.tagName.toLowerCase(),
470
+ className: el.className && typeof el.className === 'string' ? el.className : null,
471
+ id: el.id || null,
472
+ attributes: attrs,
473
+ innerText: text,
474
+ computedStyles: getComputedStylesForElement(el),
475
+ cssVariableUsages: varUsages,
476
+ sourceInfo: sourceInfo,
477
+ boundingRect: {
478
+ x: rect.x, y: rect.y,
479
+ width: rect.width, height: rect.height,
480
+ top: rect.top, right: rect.right,
481
+ bottom: rect.bottom, left: rect.left
482
+ }
483
+ }
484
+ });
485
+ }
486
+
487
+ function clearSelection() {
488
+ selectedElement = null;
489
+ selectionOverlay.style.display = 'none';
490
+ }
491
+
492
+ // Update overlays on scroll so they follow elements
493
+ function updateOverlays() {
494
+ if (selectedElement && selectionOverlay.style.display !== 'none') {
495
+ var sr = selectedElement.getBoundingClientRect();
496
+ selectionOverlay.style.top = sr.top + 'px';
497
+ selectionOverlay.style.left = sr.left + 'px';
498
+ selectionOverlay.style.width = sr.width + 'px';
499
+ selectionOverlay.style.height = sr.height + 'px';
500
+ }
501
+ if (hoveredElement && hoverOverlay.style.display !== 'none') {
502
+ var hr = hoveredElement.getBoundingClientRect();
503
+ hoverOverlay.style.top = hr.top + 'px';
504
+ hoverOverlay.style.left = hr.left + 'px';
505
+ hoverOverlay.style.width = hr.width + 'px';
506
+ hoverOverlay.style.height = hr.height + 'px';
507
+ }
508
+ }
509
+ window.addEventListener('scroll', updateOverlays, true);
510
+ window.addEventListener('resize', updateOverlays, true);
511
+
512
+ // --- Inline Text Editing ---
513
+ var textEditingActive = false;
514
+ var originalTextContent = null;
515
+ var textEditTarget = null;
516
+ var SKIP_TEXT_EDIT_TAGS = { INPUT: 1, TEXTAREA: 1, SELECT: 1, IMG: 1, VIDEO: 1, IFRAME: 1, SVG: 1, svg: 1, CANVAS: 1 };
517
+
518
+ function commitTextEdit() {
519
+ if (!textEditingActive || !textEditTarget) return;
520
+ var newText = textEditTarget.textContent || '';
521
+ var el = textEditTarget;
522
+ var selectorPath = generateSelectorPath(el);
523
+
524
+ el.contentEditable = 'false';
525
+ el.style.removeProperty('outline');
526
+ el.style.removeProperty('outline-offset');
527
+ el.style.removeProperty('min-width');
528
+ textEditingActive = false;
529
+ textEditTarget = null;
530
+
531
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'id', 'style'] });
532
+
533
+ selectionOverlay.style.display = 'block';
534
+ updateOverlays();
535
+
536
+ if (newText !== originalTextContent) {
537
+ send({
538
+ type: 'TEXT_CHANGED',
539
+ payload: {
540
+ selectorPath: selectorPath,
541
+ originalText: originalTextContent || '',
542
+ newText: newText
543
+ }
544
+ });
545
+ }
546
+ originalTextContent = null;
547
+ }
548
+
549
+ function cancelTextEdit() {
550
+ if (!textEditingActive || !textEditTarget) return;
551
+ textEditTarget.textContent = originalTextContent;
552
+ textEditTarget.contentEditable = 'false';
553
+ textEditTarget.style.removeProperty('outline');
554
+ textEditTarget.style.removeProperty('outline-offset');
555
+ textEditTarget.style.removeProperty('min-width');
556
+ textEditingActive = false;
557
+ textEditTarget = null;
558
+ originalTextContent = null;
559
+
560
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'id', 'style'] });
561
+
562
+ selectionOverlay.style.display = 'block';
563
+ updateOverlays();
564
+ }
565
+
566
+ // Find the best editable text target from a starting element.
567
+ // Walks down through wrappers to find a leaf text node,
568
+ // or returns the element itself if it has direct text content.
569
+ // Handles: <button><svg/>Submit</button>, <a><svg/><span>text</span><svg/></a>, etc.
570
+ function findTextTarget(el) {
571
+ if (!el || SKIP_TEXT_EDIT_TAGS[el.tagName]) return null;
572
+ // Leaf node with text — ideal target
573
+ if (el.children.length === 0) {
574
+ return (el.textContent && el.textContent.trim()) ? el : null;
575
+ }
576
+ // Single child — recurse into it (common: <button><span>text</span></button>)
577
+ if (el.children.length === 1) {
578
+ var child = el.children[0];
579
+ if (SKIP_TEXT_EDIT_TAGS[child.tagName]) {
580
+ return hasDirectTextNodes(el) ? el : null;
581
+ }
582
+ return findTextTarget(child);
583
+ }
584
+ // Multiple children — find the single non-skippable text-bearing child
585
+ // (handles: <a><svg/><span>Book a Call</span><svg/></a>)
586
+ var textChild = null;
587
+ for (var i = 0; i < el.children.length; i++) {
588
+ var ch = el.children[i];
589
+ if (SKIP_TEXT_EDIT_TAGS[ch.tagName]) continue;
590
+ if (ch.textContent && ch.textContent.trim()) {
591
+ if (textChild) { textChild = null; break; } // ambiguous — multiple text children
592
+ textChild = ch;
593
+ }
594
+ }
595
+ if (textChild) return findTextTarget(textChild);
596
+ // Fall back: allow editing if element has direct text nodes
597
+ return hasDirectTextNodes(el) ? el : null;
598
+ }
599
+
600
+ function hasDirectTextNodes(el) {
601
+ for (var i = 0; i < el.childNodes.length; i++) {
602
+ if (el.childNodes[i].nodeType === 3 && el.childNodes[i].textContent.trim()) {
603
+ return true;
604
+ }
605
+ }
606
+ return false;
607
+ }
608
+
609
+ document.addEventListener('dblclick', function(e) {
610
+ if (!selectionModeEnabled) return;
611
+ e.preventDefault();
612
+ e.stopPropagation();
613
+
614
+ var el = document.elementFromPoint(e.clientX, e.clientY);
615
+ // If the selection overlay is in the way, temporarily hide it
616
+ // and re-probe to get the actual element underneath
617
+ if (el === selectionOverlay || el === hoverOverlay) {
618
+ var prevSel = selectionOverlay.style.display;
619
+ var prevHov = hoverOverlay.style.display;
620
+ selectionOverlay.style.display = 'none';
621
+ hoverOverlay.style.display = 'none';
622
+ el = document.elementFromPoint(e.clientX, e.clientY);
623
+ selectionOverlay.style.display = prevSel;
624
+ hoverOverlay.style.display = prevHov;
625
+ }
626
+ if (!el) return;
627
+ if (SKIP_TEXT_EDIT_TAGS[el.tagName]) return;
628
+
629
+ // Find the best editable text target (may walk down into children)
630
+ el = findTextTarget(el);
631
+ if (!el) return;
632
+
633
+ var text = el.textContent;
634
+ if (text === null || text === undefined) return;
635
+
636
+ textEditingActive = true;
637
+ textEditTarget = el;
638
+ originalTextContent = text;
639
+
640
+ observer.disconnect();
641
+
642
+ selectionOverlay.style.display = 'none';
643
+
644
+ el.contentEditable = 'true';
645
+ el.style.setProperty('outline', '2px solid #4a9eff', 'important');
646
+ el.style.setProperty('outline-offset', '2px', 'important');
647
+ el.style.setProperty('min-width', '20px', 'important');
648
+ el.focus();
649
+
650
+ var range = document.createRange();
651
+ range.selectNodeContents(el);
652
+ var sel = window.getSelection();
653
+ sel.removeAllRanges();
654
+ sel.addRange(range);
655
+ }, true);
656
+
657
+ document.addEventListener('keydown', function(e) {
658
+ if (textEditingActive) {
659
+ e.stopPropagation();
660
+ if (e.key === 'Enter' && !e.shiftKey) {
661
+ e.preventDefault();
662
+ commitTextEdit();
663
+ } else if (e.key === 'Escape') {
664
+ e.preventDefault();
665
+ cancelTextEdit();
666
+ }
667
+ return;
668
+ }
669
+
670
+ // Delete selected element (Delete or Backspace when not editing text)
671
+ if ((e.key === 'Delete' || e.key === 'Backspace') && selectionModeEnabled && selectedElement) {
672
+ e.preventDefault();
673
+ e.stopPropagation();
674
+ var delEl = selectedElement;
675
+ var computed = window.getComputedStyle(delEl);
676
+ var origDisplay = computed.getPropertyValue('display');
677
+ var delSelector = generateSelectorPath(delEl);
678
+
679
+ var delAttrs = {};
680
+ for (var dai = 0; dai < delEl.attributes.length; dai++) {
681
+ var da = delEl.attributes[dai];
682
+ delAttrs[da.name] = da.value;
683
+ }
684
+
685
+ // Hide the element
686
+ delEl.style.setProperty('display', 'none', 'important');
687
+ selectionOverlay.style.display = 'none';
688
+ selectedElement = null;
689
+
690
+ send({
691
+ type: 'ELEMENT_DELETED',
692
+ payload: {
693
+ selectorPath: delSelector,
694
+ originalDisplay: origDisplay,
695
+ tagName: delEl.tagName.toLowerCase(),
696
+ className: delEl.className && typeof delEl.className === 'string' ? delEl.className : null,
697
+ elementId: delEl.id || null,
698
+ innerText: (delEl.innerText || '').substring(0, 500) || null,
699
+ attributes: delAttrs,
700
+ computedStyles: getComputedStylesForElement(delEl)
701
+ }
702
+ });
703
+ }
704
+ }, true);
705
+
706
+ // --- Component Detection ---
707
+ var SEMANTIC_COMPONENTS = {
708
+ button: 'Button', nav: 'Navigation', input: 'Input', header: 'Header',
709
+ footer: 'Footer', dialog: 'Dialog', a: 'Link', img: 'Image',
710
+ form: 'Form', select: 'Select', textarea: 'Textarea', table: 'Table',
711
+ aside: 'Sidebar', main: 'Main Content', section: 'Section'
712
+ };
713
+
714
+ var ARIA_ROLE_MAP = {
715
+ button: 'Button', navigation: 'Navigation', tab: 'Tab', tablist: 'Tab List',
716
+ dialog: 'Dialog', alert: 'Alert', menu: 'Menu', menuitem: 'Menu Item',
717
+ search: 'Search'
718
+ };
719
+
720
+ var CLASS_PATTERNS = [
721
+ [/\\bbtn\\b/i, 'Button'], [/\\bcard\\b/i, 'Card'], [/\\bmodal\\b/i, 'Modal'],
722
+ [/\\bdropdown\\b/i, 'Dropdown'], [/\\bbadge\\b/i, 'Badge'],
723
+ [/\\bnav\\b/i, 'Navigation'], [/\\balert\\b/i, 'Alert'], [/\\btabs?\\b/i, 'Tab']
724
+ ];
725
+
726
+ function detectSingleComponent(el) {
727
+ var tag = el.tagName.toLowerCase();
728
+ // Priority 1: data-component attribute
729
+ var dataComp = el.getAttribute('data-component');
730
+ if (dataComp) return { name: dataComp, method: 'data-attribute' };
731
+ // Priority 2: custom element (tag with hyphen)
732
+ if (tag.indexOf('-') >= 0) return { name: tag, method: 'custom-element' };
733
+ // Priority 3: semantic HTML
734
+ if (SEMANTIC_COMPONENTS[tag]) return { name: SEMANTIC_COMPONENTS[tag], method: 'semantic-html' };
735
+ // Priority 4: ARIA role
736
+ var role = el.getAttribute('role');
737
+ if (role && ARIA_ROLE_MAP[role]) return { name: ARIA_ROLE_MAP[role], method: 'aria-role' };
738
+ // Priority 5: class pattern
739
+ var cls = el.className;
740
+ if (cls && typeof cls === 'string') {
741
+ for (var pi = 0; pi < CLASS_PATTERNS.length; pi++) {
742
+ if (CLASS_PATTERNS[pi][0].test(cls)) return { name: CLASS_PATTERNS[pi][1], method: 'class-pattern' };
743
+ }
744
+ }
745
+ return null;
746
+ }
747
+
748
+ function titleWords(str) {
749
+ return str.replace(/\\w\\S*/g, function(w) {
750
+ return w.charAt(0).toUpperCase() + w.substr(1).toLowerCase();
751
+ });
752
+ }
753
+
754
+ function contextualName(el, baseName) {
755
+ var tag = el.tagName.toLowerCase();
756
+ // aria-label: most reliable semantic hint
757
+ var label = (el.getAttribute('aria-label') || '').trim();
758
+ if (label.length > 0 && label.length <= 25) return titleWords(label);
759
+ // title attribute
760
+ var titleAttr = (el.getAttribute('title') || '').trim();
761
+ if (titleAttr.length > 0 && titleAttr.length <= 25) return titleWords(titleAttr);
762
+ // Images: use alt text
763
+ if (tag === 'img') {
764
+ var alt = (el.getAttribute('alt') || '').trim();
765
+ if (alt.length > 0 && alt.length <= 25) return titleWords(alt);
766
+ }
767
+ // Inputs: use placeholder or type
768
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
769
+ var ph = (el.getAttribute('placeholder') || '').trim();
770
+ if (ph.length > 0 && ph.length <= 20) return titleWords(ph);
771
+ var tp = el.getAttribute('type');
772
+ if (tp && tp !== 'text' && tp !== 'hidden') return titleWords(tp) + ' ' + baseName;
773
+ }
774
+ // Buttons/links: use short inner text
775
+ if (tag === 'button' || tag === 'a') {
776
+ var txt = (el.innerText || '').trim();
777
+ if (txt.length > 0 && txt.length <= 20 && txt.indexOf('\\n') === -1) return titleWords(txt);
778
+ }
779
+ // id attribute: convert to readable name
780
+ if (el.id && el.id.length > 1 && el.id.length <= 25) {
781
+ return titleWords(el.id.replace(/[-_]/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2'));
782
+ }
783
+ return baseName;
784
+ }
785
+
786
+ var SIZE_SUFFIXES = ['xs','sm','md','lg','xl','2xl'];
787
+ var COLOR_SUFFIXES = ['primary','secondary','success','danger','warning','info','light','dark'];
788
+ var STATE_SUFFIXES = ['active','disabled','loading','selected','checked'];
789
+
790
+ function detectClassVariants(el) {
791
+ var groups = [];
792
+ var cls = el.className;
793
+ if (!cls || typeof cls !== 'string') return groups;
794
+ var classes = cls.trim().split(/\\s+/).filter(Boolean);
795
+ // Extract base prefixes (e.g., "btn" from "btn-primary")
796
+ var prefixes = {};
797
+ for (var ci = 0; ci < classes.length; ci++) {
798
+ var parts = classes[ci].split('-');
799
+ if (parts.length >= 2) {
800
+ var base = parts[0];
801
+ var suffix = parts.slice(1).join('-');
802
+ if (!prefixes[base]) prefixes[base] = { currentClass: classes[ci], suffix: suffix };
803
+ }
804
+ }
805
+ // Scan stylesheets for matching classes
806
+ for (var base in prefixes) {
807
+ if (!prefixes.hasOwnProperty(base)) continue;
808
+ var sizeOpts = [], colorOpts = [], stateOpts = [];
809
+ var currentClass = prefixes[base].currentClass;
810
+ // Scan all accessible stylesheets
811
+ for (var si = 0; si < document.styleSheets.length; si++) {
812
+ var sheet = document.styleSheets[si];
813
+ var rules;
814
+ try { rules = sheet.cssRules || sheet.rules; } catch(e) { continue; }
815
+ if (!rules) continue;
816
+ for (var ri = 0; ri < rules.length; ri++) {
817
+ var rule = rules[ri];
818
+ if (!rule.selectorText) continue;
819
+ var escapedBase = base.replace(/[-\\/\\\\^*+?.()|\\[\\]]/g, '\\\\' + String.fromCharCode(36) + '&');
820
+ var match = rule.selectorText.match(new RegExp('\\\\.' + escapedBase + '-([\\\\w-]+)'));
821
+ if (!match) continue;
822
+ var foundSuffix = match[1];
823
+ var foundClass = base + '-' + foundSuffix;
824
+ if (foundClass === currentClass) continue; // skip current
825
+ var opt = { label: foundSuffix, className: foundClass, removeClassNames: [currentClass], pseudoState: null, pseudoStyles: null };
826
+ if (SIZE_SUFFIXES.indexOf(foundSuffix) >= 0) { sizeOpts.push(opt); }
827
+ else if (COLOR_SUFFIXES.indexOf(foundSuffix) >= 0) { colorOpts.push(opt); }
828
+ else if (STATE_SUFFIXES.indexOf(foundSuffix) >= 0) { stateOpts.push(opt); }
829
+ }
830
+ }
831
+ // Build groups (need 2+ options including current)
832
+ var currentSuffix = prefixes[base].suffix;
833
+ var currentOpt = { label: currentSuffix, className: currentClass, removeClassNames: [], pseudoState: null, pseudoStyles: null };
834
+ if (sizeOpts.length > 0) {
835
+ sizeOpts.unshift(currentOpt);
836
+ // Deduplicate
837
+ var seen = {};
838
+ sizeOpts = sizeOpts.filter(function(o) { if (seen[o.className]) return false; seen[o.className] = true; return true; });
839
+ if (sizeOpts.length >= 2) {
840
+ for (var soi = 0; soi < sizeOpts.length; soi++) { sizeOpts[soi].removeClassNames = [currentClass]; }
841
+ sizeOpts[0].removeClassNames = [];
842
+ groups.push({ groupName: 'Size', type: 'class', options: sizeOpts, activeIndex: 0 });
843
+ }
844
+ }
845
+ if (colorOpts.length > 0) {
846
+ colorOpts.unshift(currentOpt);
847
+ var seenC = {};
848
+ colorOpts = colorOpts.filter(function(o) { if (seenC[o.className]) return false; seenC[o.className] = true; return true; });
849
+ if (colorOpts.length >= 2) {
850
+ for (var coi = 0; coi < colorOpts.length; coi++) { colorOpts[coi].removeClassNames = [currentClass]; }
851
+ colorOpts[0].removeClassNames = [];
852
+ groups.push({ groupName: 'Color', type: 'class', options: colorOpts, activeIndex: 0 });
853
+ }
854
+ }
855
+ if (stateOpts.length > 0) {
856
+ stateOpts.unshift(currentOpt);
857
+ var seenS = {};
858
+ stateOpts = stateOpts.filter(function(o) { if (seenS[o.className]) return false; seenS[o.className] = true; return true; });
859
+ if (stateOpts.length >= 2) {
860
+ for (var stoi = 0; stoi < stateOpts.length; stoi++) { stateOpts[stoi].removeClassNames = [currentClass]; }
861
+ stateOpts[0].removeClassNames = [];
862
+ groups.push({ groupName: 'State', type: 'class', options: stateOpts, activeIndex: 0 });
863
+ }
864
+ }
865
+ }
866
+ return groups;
867
+ }
868
+
869
+ function detectPseudoVariants(el) {
870
+ try {
871
+ var visualProps = ['color','backgroundColor','borderColor','opacity','transform','boxShadow','textDecoration','outline'];
872
+ var defaultStyles = window.getComputedStyle(el);
873
+ var pseudos = ['hover','focus','active'];
874
+ var options = [{ label: 'default', className: null, removeClassNames: null, pseudoState: null, pseudoStyles: null }];
875
+ for (var pi = 0; pi < pseudos.length; pi++) {
876
+ var pseudo = pseudos[pi];
877
+ var pseudoStyles = window.getComputedStyle(el, ':' + pseudo);
878
+ var diffs = {};
879
+ var hasDiff = false;
880
+ for (var vi = 0; vi < visualProps.length; vi++) {
881
+ var prop = visualProps[vi];
882
+ var defaultVal = defaultStyles.getPropertyValue(prop);
883
+ var pseudoVal = pseudoStyles.getPropertyValue(prop);
884
+ if (defaultVal !== pseudoVal && pseudoVal) {
885
+ diffs[prop] = pseudoVal;
886
+ hasDiff = true;
887
+ }
888
+ }
889
+ if (hasDiff) {
890
+ options.push({ label: pseudo, className: null, removeClassNames: null, pseudoState: pseudo, pseudoStyles: diffs });
891
+ }
892
+ }
893
+ if (options.length >= 2) {
894
+ return [{ groupName: 'Pseudo States', type: 'pseudo', options: options, activeIndex: 0 }];
895
+ }
896
+ } catch(e) {}
897
+ return [];
898
+ }
899
+
900
+ function scanForComponents(rootElement) {
901
+ var allElements = rootElement.querySelectorAll('*');
902
+ var results = [];
903
+ var batchSize = 50;
904
+ var index = 0;
905
+ var scheduleNext = typeof requestIdleCallback === 'function'
906
+ ? function(cb) { requestIdleCallback(cb); }
907
+ : function(cb) { setTimeout(cb, 0); };
908
+
909
+ function processBatch() {
910
+ var end = Math.min(index + batchSize, allElements.length);
911
+ for (var i = index; i < end; i++) {
912
+ var el = allElements[i];
913
+ var detection = detectSingleComponent(el);
914
+ if (detection) {
915
+ var rect = el.getBoundingClientRect();
916
+ var text = (el.innerText || '').substring(0, 50) || null;
917
+ // Count child components
918
+ var childCount = 0;
919
+ var childEls = el.querySelectorAll('*');
920
+ for (var ci = 0; ci < childEls.length; ci++) {
921
+ if (detectSingleComponent(childEls[ci])) childCount++;
922
+ }
923
+ results.push({
924
+ selectorPath: generateSelectorPath(el),
925
+ name: contextualName(el, detection.name),
926
+ tagName: el.tagName.toLowerCase(),
927
+ detectionMethod: detection.method,
928
+ className: el.className && typeof el.className === 'string' ? el.className : null,
929
+ elementId: el.id || null,
930
+ innerText: text,
931
+ boundingRect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
932
+ variants: detectClassVariants(el).concat(detectPseudoVariants(el)),
933
+ childComponentCount: childCount
934
+ });
935
+ }
936
+ }
937
+ index = end;
938
+ if (index < allElements.length) {
939
+ scheduleNext(processBatch);
940
+ } else {
941
+ send({ type: 'COMPONENTS_DETECTED', payload: { components: results } });
942
+ }
943
+ }
944
+ if (allElements.length === 0) {
945
+ send({ type: 'COMPONENTS_DETECTED', payload: { components: [] } });
946
+ } else {
947
+ processBatch();
948
+ }
949
+ }
950
+
951
+ // MutationObserver for DOM changes — throttled to at most once per animation frame
952
+ var mutationPending = false;
953
+ var previousTreeJSON = '';
954
+ var observer = new MutationObserver(function() {
955
+ if (mutationPending) return;
956
+ mutationPending = true;
957
+ requestAnimationFrame(function() {
958
+ mutationPending = false;
959
+ var tree = serializeTree(document.body);
960
+ if (!tree) return;
961
+ // Skip sending if tree hasn't changed (avoids redundant postMessages)
962
+ var treeJSON = JSON.stringify(tree);
963
+ if (treeJSON === previousTreeJSON) return;
964
+ previousTreeJSON = treeJSON;
965
+ var removedSelectors = [];
966
+ if (selectedElement && !document.body.contains(selectedElement)) {
967
+ removedSelectors.push(generateSelectorPath(selectedElement));
968
+ clearSelection();
969
+ }
970
+ send({ type: 'DOM_UPDATED', payload: { tree: tree, removedSelectors: removedSelectors } });
971
+ });
972
+ });
973
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'id', 'style'] });
974
+
975
+ // --- Drag-and-drop from Add Element palette ---
976
+ var VOID_TAGS_DND = {img:1,input:1,br:1,hr:1,area:1,base:1,col:1,embed:1,link:1,meta:1,param:1,source:1,track:1,wbr:1};
977
+ var dropIndicator = document.createElement('div');
978
+ dropIndicator.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483645;border:2px dashed #4a9eff;background:rgba(74,158,255,0.08);display:none;transition:top 0.08s,left 0.08s,width 0.08s,height 0.08s;';
979
+ document.body.appendChild(dropIndicator);
980
+
981
+ document.addEventListener('dragover', function(e) {
982
+ if (!e.dataTransfer || !e.dataTransfer.types.indexOf) return;
983
+ if (e.dataTransfer.types.indexOf('application/x-dev-editor-element') === -1) return;
984
+ e.preventDefault();
985
+ e.dataTransfer.dropEffect = 'copy';
986
+ var dropTarget = document.elementFromPoint(e.clientX, e.clientY);
987
+ if (!dropTarget || dropTarget === dropIndicator) return;
988
+ // Skip inspector overlays
989
+ if (dropTarget.id && dropTarget.id.indexOf('dev-editor') === 0) return;
990
+ var rect = dropTarget.getBoundingClientRect();
991
+ dropIndicator.style.top = rect.top + 'px';
992
+ dropIndicator.style.left = rect.left + 'px';
993
+ dropIndicator.style.width = rect.width + 'px';
994
+ dropIndicator.style.height = rect.height + 'px';
995
+ dropIndicator.style.display = 'block';
996
+ }, true);
997
+
998
+ document.addEventListener('dragleave', function(e) {
999
+ if (e.relatedTarget === null || e.relatedTarget === document.documentElement) {
1000
+ dropIndicator.style.display = 'none';
1001
+ }
1002
+ }, true);
1003
+
1004
+ document.addEventListener('drop', function(e) {
1005
+ dropIndicator.style.display = 'none';
1006
+ if (!e.dataTransfer) return;
1007
+ var raw = e.dataTransfer.getData('application/x-dev-editor-element');
1008
+ if (!raw) return;
1009
+ e.preventDefault();
1010
+ e.stopPropagation();
1011
+ try {
1012
+ var data = JSON.parse(raw);
1013
+ var dropEl = document.elementFromPoint(e.clientX, e.clientY);
1014
+ if (!dropEl) return;
1015
+ // Skip inspector overlays
1016
+ if (dropEl.id && dropEl.id.indexOf('dev-editor') === 0) return;
1017
+ var targetParent = dropEl;
1018
+ var insertMode = 'child';
1019
+ if (VOID_TAGS_DND[dropEl.tagName.toLowerCase()]) {
1020
+ targetParent = dropEl.parentElement || document.body;
1021
+ insertMode = 'after';
1022
+ }
1023
+ var newEl = document.createElement(data.tag);
1024
+ newEl.setAttribute('data-dev-editor-inserted', 'true');
1025
+ if (data.placeholderText) {
1026
+ newEl.textContent = data.placeholderText;
1027
+ }
1028
+ if (data.defaultStyles) {
1029
+ var dndDS = data.defaultStyles;
1030
+ for (var dndDSKey in dndDS) {
1031
+ if (dndDS.hasOwnProperty(dndDSKey)) newEl.style.setProperty(dndDSKey, dndDS[dndDSKey]);
1032
+ }
1033
+ }
1034
+ if (insertMode === 'after' && dropEl.nextSibling) {
1035
+ targetParent.insertBefore(newEl, dropEl.nextSibling);
1036
+ } else {
1037
+ targetParent.appendChild(newEl);
1038
+ }
1039
+ var newSelector = generateSelectorPath(newEl);
1040
+ var newIndex = Array.from(targetParent.children).indexOf(newEl);
1041
+ send({
1042
+ type: 'ELEMENT_INSERTED',
1043
+ payload: {
1044
+ selectorPath: newSelector,
1045
+ parentSelectorPath: generateSelectorPath(targetParent),
1046
+ tagName: data.tag,
1047
+ insertionIndex: newIndex,
1048
+ placeholderText: data.placeholderText || '',
1049
+ defaultStyles: data.defaultStyles || undefined
1050
+ }
1051
+ });
1052
+ } catch(err) {}
1053
+ }, true);
1054
+
1055
+ // Handle messages from editor
1056
+ window.addEventListener('message', function(e) {
1057
+ if (e.origin !== parentOrigin) return;
1058
+ var msg = e.data;
1059
+ if (!msg || !msg.type) return;
1060
+
1061
+ switch (msg.type) {
1062
+ case 'SELECT_ELEMENT': {
1063
+ try {
1064
+ var el = document.querySelector(msg.payload.selectorPath);
1065
+ if (el) {
1066
+ selectElement(el);
1067
+ el.scrollIntoView({ block: 'center', behavior: 'instant' });
1068
+ }
1069
+ } catch(err) {}
1070
+ break;
1071
+ }
1072
+ case 'PREVIEW_CHANGE': {
1073
+ try {
1074
+ var target = document.querySelector(msg.payload.selectorPath);
1075
+ if (target) {
1076
+ var cssProp = toKC(msg.payload.property);
1077
+ requestAnimationFrame(function() {
1078
+ target.style.setProperty(cssProp, msg.payload.value, 'important');
1079
+ });
1080
+ }
1081
+ } catch(err) {}
1082
+ break;
1083
+ }
1084
+ case 'REVERT_CHANGE': {
1085
+ try {
1086
+ var target2 = document.querySelector(msg.payload.selectorPath);
1087
+ if (target2) target2.style.removeProperty(toKC(msg.payload.property));
1088
+ } catch(err) {}
1089
+ break;
1090
+ }
1091
+ case 'REVERT_ALL': {
1092
+ var allElements = document.querySelectorAll('[style]');
1093
+ allElements.forEach(function(el) { el.removeAttribute('style'); });
1094
+ break;
1095
+ }
1096
+ case 'SET_SELECTION_MODE': {
1097
+ selectionModeEnabled = !!msg.payload.enabled;
1098
+ if (!selectionModeEnabled) {
1099
+ selectionOverlay.style.display = 'none';
1100
+ hoverOverlay.style.display = 'none';
1101
+ hoveredElement = null;
1102
+ selectedElement = null;
1103
+ }
1104
+ break;
1105
+ }
1106
+ case 'HIDE_HOVER': {
1107
+ hoverOverlay.style.display = 'none';
1108
+ hoveredElement = null;
1109
+ break;
1110
+ }
1111
+ case 'HIDE_SELECTION_OVERLAY': {
1112
+ selectionOverlay.style.display = 'none';
1113
+ break;
1114
+ }
1115
+ case 'SHOW_SELECTION_OVERLAY': {
1116
+ if (selectionModeEnabled && selectedElement) {
1117
+ selectionOverlay.style.display = 'block';
1118
+ }
1119
+ break;
1120
+ }
1121
+ case 'SET_BREAKPOINT': {
1122
+ // Viewport controller handled externally
1123
+ break;
1124
+ }
1125
+ case 'REQUEST_DOM_TREE': {
1126
+ var tree = serializeTree(document.body);
1127
+ if (tree) send({ type: 'DOM_TREE', payload: { tree: tree } });
1128
+ break;
1129
+ }
1130
+ case 'REQUEST_PAGE_LINKS': {
1131
+ var links = [];
1132
+ var seen = {};
1133
+ var anchors = document.querySelectorAll('a[href]');
1134
+ for (var ai = 0; ai < anchors.length; ai++) {
1135
+ var rawHref = anchors[ai].getAttribute('href') || '';
1136
+ var linkText = (anchors[ai].textContent || '').trim();
1137
+ // Resolve to absolute URL
1138
+ var resolved;
1139
+ try { resolved = new URL(rawHref, window.location.origin); } catch(e) { continue; }
1140
+ // Only same-origin links
1141
+ if (resolved.origin !== window.location.origin) continue;
1142
+ var linkPath = resolved.pathname;
1143
+ // Strip /api/proxy prefix added by proxy rewriting
1144
+ if (linkPath.indexOf('/api/proxy') === 0) {
1145
+ linkPath = linkPath.substring(10) || '/';
1146
+ }
1147
+ // Skip API routes and empty paths
1148
+ if (linkPath.indexOf('/api/') === 0 || linkPath === '') continue;
1149
+ // Skip anchors, mailto, etc.
1150
+ if (!linkPath.startsWith('/')) continue;
1151
+ if (seen[linkPath]) continue;
1152
+ seen[linkPath] = true;
1153
+ links.push({ href: linkPath, text: linkText });
1154
+ }
1155
+ send({ type: 'PAGE_LINKS', payload: { links: links } });
1156
+ break;
1157
+ }
1158
+ case 'REQUEST_CSS_VARIABLES': {
1159
+ var result = scanCSSVariableDefinitions();
1160
+ initialVarCount = Object.keys(result.definitions).length;
1161
+ console.log('[pAInt] CSS variable definitions found:', initialVarCount, result.definitions);
1162
+ send({ type: 'CSS_VARIABLES', payload: { definitions: result.definitions, isExplicit: result.isExplicit, scopes: result.scopes || [] } });
1163
+ break;
1164
+ }
1165
+ case 'REQUEST_COMPONENTS': {
1166
+ try {
1167
+ var compRoot = document.body;
1168
+ if (msg.payload && msg.payload.rootSelectorPath) {
1169
+ var rootEl = document.querySelector(msg.payload.rootSelectorPath);
1170
+ if (rootEl) compRoot = rootEl;
1171
+ }
1172
+ scanForComponents(compRoot);
1173
+ } catch(err) {}
1174
+ break;
1175
+ }
1176
+ case 'APPLY_VARIANT': {
1177
+ try {
1178
+ var avEl = document.querySelector(msg.payload.selectorPath);
1179
+ if (avEl) {
1180
+ if (msg.payload.type === 'class') {
1181
+ if (msg.payload.removeClassNames) {
1182
+ for (var rci = 0; rci < msg.payload.removeClassNames.length; rci++) {
1183
+ avEl.classList.remove(msg.payload.removeClassNames[rci]);
1184
+ }
1185
+ }
1186
+ if (msg.payload.addClassName) {
1187
+ avEl.classList.add(msg.payload.addClassName);
1188
+ }
1189
+ } else if (msg.payload.type === 'pseudo') {
1190
+ if (msg.payload.revertPseudo && msg.payload.pseudoStyles) {
1191
+ var revertKeys = Object.keys(msg.payload.pseudoStyles);
1192
+ for (var rki = 0; rki < revertKeys.length; rki++) {
1193
+ avEl.style.removeProperty(revertKeys[rki]);
1194
+ }
1195
+ }
1196
+ if (msg.payload.pseudoStyles && !msg.payload.revertPseudo) {
1197
+ var psKeys = Object.keys(msg.payload.pseudoStyles);
1198
+ for (var psi = 0; psi < psKeys.length; psi++) {
1199
+ avEl.style.setProperty(psKeys[psi], msg.payload.pseudoStyles[psKeys[psi]], 'important');
1200
+ }
1201
+ }
1202
+ }
1203
+ var avStyles = getComputedStylesForElement(avEl);
1204
+ var avVarUsages = detectCSSVariablesOnElement(avEl);
1205
+ var avRect = avEl.getBoundingClientRect();
1206
+ send({ type: 'VARIANT_APPLIED', payload: {
1207
+ selectorPath: msg.payload.selectorPath,
1208
+ computedStyles: avStyles,
1209
+ cssVariableUsages: avVarUsages,
1210
+ boundingRect: { top: avRect.top, left: avRect.left, width: avRect.width, height: avRect.height }
1211
+ }});
1212
+ }
1213
+ } catch(err) {}
1214
+ break;
1215
+ }
1216
+ case 'REVERT_VARIANT': {
1217
+ try {
1218
+ var rvEl = document.querySelector(msg.payload.selectorPath);
1219
+ if (rvEl) {
1220
+ if (msg.payload.removeClassName) {
1221
+ rvEl.classList.remove(msg.payload.removeClassName);
1222
+ }
1223
+ if (msg.payload.restoreClassName) {
1224
+ rvEl.classList.add(msg.payload.restoreClassName);
1225
+ }
1226
+ if (msg.payload.revertPseudo && msg.payload.pseudoProperties) {
1227
+ for (var rppi = 0; rppi < msg.payload.pseudoProperties.length; rppi++) {
1228
+ rvEl.style.removeProperty(msg.payload.pseudoProperties[rppi]);
1229
+ }
1230
+ }
1231
+ }
1232
+ } catch(err) {}
1233
+ break;
1234
+ }
1235
+ case 'SET_TEXT_CONTENT': {
1236
+ try {
1237
+ var stEl = document.querySelector(msg.payload.selectorPath);
1238
+ if (stEl) stEl.textContent = msg.payload.text;
1239
+ } catch(err) {}
1240
+ break;
1241
+ }
1242
+ case 'REVERT_TEXT_CONTENT': {
1243
+ try {
1244
+ var rtEl = document.querySelector(msg.payload.selectorPath);
1245
+ if (rtEl) rtEl.textContent = msg.payload.originalText;
1246
+ } catch(err) {}
1247
+ break;
1248
+ }
1249
+ case 'REVERT_DELETE': {
1250
+ try {
1251
+ var rdEl = document.querySelector(msg.payload.selectorPath);
1252
+ if (rdEl) {
1253
+ rdEl.style.removeProperty('display');
1254
+ }
1255
+ } catch(err) {}
1256
+ break;
1257
+ }
1258
+ case 'INSERT_ELEMENT': {
1259
+ try {
1260
+ var ieParent = document.querySelector(msg.payload.parentSelectorPath);
1261
+ if (ieParent) {
1262
+ var VOID_TAGS = {img:1,input:1,br:1,hr:1,area:1,base:1,col:1,embed:1,link:1,meta:1,param:1,source:1,track:1,wbr:1};
1263
+ var ieTarget = ieParent;
1264
+ var ieInsertMode = 'child';
1265
+ if (VOID_TAGS[ieParent.tagName.toLowerCase()]) {
1266
+ ieTarget = ieParent.parentElement || document.body;
1267
+ ieInsertMode = 'after';
1268
+ }
1269
+ var ieNew = document.createElement(msg.payload.tagName);
1270
+ ieNew.setAttribute('data-dev-editor-inserted', 'true');
1271
+ if (msg.payload.placeholderText) {
1272
+ ieNew.textContent = msg.payload.placeholderText;
1273
+ }
1274
+ if (msg.payload.defaultStyles) {
1275
+ var ieDS = msg.payload.defaultStyles;
1276
+ for (var ieDSKey in ieDS) {
1277
+ if (ieDS.hasOwnProperty(ieDSKey)) ieNew.style.setProperty(ieDSKey, ieDS[ieDSKey]);
1278
+ }
1279
+ }
1280
+ if (ieInsertMode === 'after' && ieParent.nextSibling) {
1281
+ ieTarget.insertBefore(ieNew, ieParent.nextSibling);
1282
+ } else {
1283
+ ieTarget.appendChild(ieNew);
1284
+ }
1285
+ var ieSelector = generateSelectorPath(ieNew);
1286
+ var ieIndex = Array.from(ieTarget.children).indexOf(ieNew);
1287
+ send({
1288
+ type: 'ELEMENT_INSERTED',
1289
+ payload: {
1290
+ selectorPath: ieSelector,
1291
+ parentSelectorPath: generateSelectorPath(ieTarget),
1292
+ tagName: msg.payload.tagName,
1293
+ insertionIndex: ieIndex,
1294
+ placeholderText: msg.payload.placeholderText || '',
1295
+ defaultStyles: msg.payload.defaultStyles || undefined
1296
+ }
1297
+ });
1298
+ }
1299
+ } catch(err) {}
1300
+ break;
1301
+ }
1302
+ case 'REMOVE_INSERTED_ELEMENT': {
1303
+ try {
1304
+ var rieEl = document.querySelector(msg.payload.selectorPath);
1305
+ if (rieEl && rieEl.parentElement) {
1306
+ rieEl.parentElement.removeChild(rieEl);
1307
+ }
1308
+ } catch(err) {}
1309
+ break;
1310
+ }
1311
+ case 'MOVE_ELEMENT': {
1312
+ try {
1313
+ var meEl = document.querySelector(msg.payload.selectorPath);
1314
+ var meNewParent = document.querySelector(msg.payload.newParentSelectorPath);
1315
+ if (meEl && meNewParent && meEl !== meNewParent) {
1316
+ // Prevent moving an element into its own descendant
1317
+ if (meEl.contains(meNewParent)) break;
1318
+ var meOldParent = meEl.parentElement;
1319
+ var meOldIndex = meOldParent ? Array.from(meOldParent.children).indexOf(meEl) : -1;
1320
+ var meOldParentSelector = meOldParent ? generateSelectorPath(meOldParent) : '';
1321
+ // Remove from current position
1322
+ if (meOldParent) meOldParent.removeChild(meEl);
1323
+ // Insert at new position
1324
+ var meNewIndex = msg.payload.newIndex;
1325
+ var meChildren = meNewParent.children;
1326
+ if (meNewIndex >= 0 && meNewIndex < meChildren.length) {
1327
+ meNewParent.insertBefore(meEl, meChildren[meNewIndex]);
1328
+ } else {
1329
+ meNewParent.appendChild(meEl);
1330
+ }
1331
+ var meNewSelector = generateSelectorPath(meEl);
1332
+ var meActualIndex = Array.from(meNewParent.children).indexOf(meEl);
1333
+ send({
1334
+ type: 'ELEMENT_MOVED',
1335
+ payload: {
1336
+ selectorPath: msg.payload.selectorPath,
1337
+ newSelectorPath: meNewSelector,
1338
+ oldParentSelectorPath: meOldParentSelector,
1339
+ newParentSelectorPath: msg.payload.newParentSelectorPath,
1340
+ oldIndex: meOldIndex,
1341
+ newIndex: meActualIndex
1342
+ }
1343
+ });
1344
+ if (selectedElement === meEl) {
1345
+ updateOverlays();
1346
+ }
1347
+ }
1348
+ } catch(err) {}
1349
+ break;
1350
+ }
1351
+ case 'REVERT_MOVE_ELEMENT': {
1352
+ try {
1353
+ var rmEl = document.querySelector(msg.payload.selectorPath);
1354
+ var rmOldParent = document.querySelector(msg.payload.oldParentSelectorPath);
1355
+ if (rmEl && rmOldParent) {
1356
+ var rmCurrentParent = rmEl.parentElement;
1357
+ if (rmCurrentParent) rmCurrentParent.removeChild(rmEl);
1358
+ var rmOldIndex = msg.payload.oldIndex;
1359
+ var rmChildren = rmOldParent.children;
1360
+ if (rmOldIndex >= 0 && rmOldIndex < rmChildren.length) {
1361
+ rmOldParent.insertBefore(rmEl, rmChildren[rmOldIndex]);
1362
+ } else {
1363
+ rmOldParent.appendChild(rmEl);
1364
+ }
1365
+ if (selectedElement === rmEl) {
1366
+ updateOverlays();
1367
+ }
1368
+ }
1369
+ } catch(err) {}
1370
+ break;
1371
+ }
1372
+ case 'HEARTBEAT': {
1373
+ send({ type: 'HEARTBEAT_RESPONSE' });
1374
+ break;
1375
+ }
1376
+ case 'NAVIGATE_TO': {
1377
+ var navPath = msg.payload.path || '/';
1378
+ var navSep = (navPath.indexOf('?') >= 0) ? '&' : '?';
1379
+ window.location.href = '/api/proxy' + navPath + navSep + pH + '=' + eT;
1380
+ break;
1381
+ }
1382
+ }
1383
+ });
1384
+
1385
+ // --- Animation reveal ---
1386
+ // Animation libraries (Framer Motion, GSAP) set elements to opacity:0 +
1387
+ // translateY via inline styles as initial state, then animate on scroll/mount.
1388
+ // In the proxy context, these animations may not fire. Detect and fix.
1389
+ function revealAnimationHidden() {
1390
+ var els = document.querySelectorAll('[style]');
1391
+ for (var ri = 0; ri < els.length; ri++) {
1392
+ var rel = els[ri];
1393
+ var rst = rel.getAttribute('style') || '';
1394
+ if (!/opacity\\s*:\\s*0\\b/.test(rst)) continue;
1395
+ var rcs = window.getComputedStyle(rel);
1396
+ if (rcs.display === 'none' || rcs.visibility === 'hidden') continue;
1397
+ rel.style.setProperty('transition', 'opacity 0.3s ease, transform 0.3s ease', 'important');
1398
+ rel.style.setProperty('opacity', '1', 'important');
1399
+ if (/translate[YX]\\s*\\(/.test(rst) || /matrix\\s*\\(/.test(rcs.transform)) {
1400
+ rel.style.setProperty('transform', 'none', 'important');
1401
+ }
1402
+ }
1403
+ }
1404
+ var revealAttempts = 0;
1405
+ var revealTimer = setInterval(function() {
1406
+ revealAnimationHidden();
1407
+ revealAttempts++;
1408
+ if (revealAttempts >= 5) clearInterval(revealTimer);
1409
+ }, 800);
1410
+
1411
+ // Signal ready
1412
+ if (document.readyState === 'loading') {
1413
+ document.addEventListener('DOMContentLoaded', function() {
1414
+ send({ type: 'INSPECTOR_READY' });
1415
+ });
1416
+ } else {
1417
+ send({ type: 'INSPECTOR_READY' });
1418
+ }
1419
+
1420
+ // Re-scan CSS variables after all resources (stylesheets, fonts) have loaded.
1421
+ // The initial scan on INSPECTOR_READY may miss variables from stylesheets
1422
+ // that haven't fully loaded yet (async CSS, @import chains, JS-injected styles).
1423
+ var initialVarCount = -1;
1424
+ function rescanCSSVariables() {
1425
+ var result = scanCSSVariableDefinitions();
1426
+ var count = Object.keys(result.definitions).length;
1427
+ if (count > initialVarCount) {
1428
+ initialVarCount = count;
1429
+ send({ type: 'CSS_VARIABLES', payload: { definitions: result.definitions, isExplicit: result.isExplicit, scopes: result.scopes || [] } });
1430
+ }
1431
+ }
1432
+ window.addEventListener('load', function() {
1433
+ setTimeout(rescanCSSVariables, 300);
1434
+ });
1435
+ // Also re-scan after a longer delay for JS-injected styles (React hydration, etc.)
1436
+ setTimeout(rescanCSSVariables, 3000);
1437
+
1438
+ return { selectElement: selectElement, clearSelection: clearSelection };
1439
+ })();
1440
+ `
1441
+ }
1442
+
1443
+ function isLocalhostUrl(url: string): boolean {
1444
+ try {
1445
+ const parsed = new URL(url)
1446
+ return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'
1447
+ } catch {
1448
+ return false
1449
+ }
1450
+ }
1451
+
1452
+ async function handleProxy(request: NextRequest, params: { path?: string[] }) {
1453
+ // Accept target URL from header, query param, or cookie (for dynamic chunk loading)
1454
+ const targetUrl =
1455
+ request.headers.get(PROXY_HEADER) ||
1456
+ request.nextUrl.searchParams.get(PROXY_HEADER) ||
1457
+ request.cookies.get(PROXY_HEADER)?.value
1458
+
1459
+ if (!targetUrl) {
1460
+ return NextResponse.json(
1461
+ { error: 'Missing X-Dev-Editor-Target header or query parameter' },
1462
+ { status: 400 },
1463
+ )
1464
+ }
1465
+
1466
+ if (!isLocalhostUrl(targetUrl)) {
1467
+ return NextResponse.json(
1468
+ { error: 'Target URL must be localhost or 127.0.0.1' },
1469
+ { status: 400 },
1470
+ )
1471
+ }
1472
+
1473
+ const path = (params.path || []).join('/')
1474
+
1475
+ // Short-circuit HMR requests — return empty responses so webpack's
1476
+ // hot-update polling and HMR connections stop at the proxy instead of
1477
+ // hitting the target server (which returns 404s and triggers error loops).
1478
+ if (
1479
+ path.includes('.hot-update.') ||
1480
+ path.includes('webpack-hmr') ||
1481
+ path.includes('__turbopack_hmr') ||
1482
+ path.includes('turbopack-hmr')
1483
+ ) {
1484
+ // For JSON manifests, return empty object (no updates available)
1485
+ if (path.endsWith('.json')) {
1486
+ return new NextResponse('{}', {
1487
+ status: 200,
1488
+ headers: { 'content-type': 'application/json' },
1489
+ })
1490
+ }
1491
+ // For JS hot-update chunks, return empty script
1492
+ if (path.endsWith('.js')) {
1493
+ return new NextResponse('', {
1494
+ status: 200,
1495
+ headers: { 'content-type': 'application/javascript' },
1496
+ })
1497
+ }
1498
+ // Anything else HMR-related: empty 204
1499
+ return new NextResponse(null, { status: 204 })
1500
+ }
1501
+
1502
+ const url = new URL(path || '/', targetUrl)
1503
+
1504
+ // Forward query string (excluding the proxy header param)
1505
+ request.nextUrl.searchParams.forEach((value, key) => {
1506
+ if (key !== PROXY_HEADER) {
1507
+ url.searchParams.set(key, value)
1508
+ }
1509
+ })
1510
+
1511
+ try {
1512
+ const controller = new AbortController()
1513
+ const timeout = setTimeout(() => controller.abort(), 10000)
1514
+
1515
+ const headers = new Headers()
1516
+ request.headers.forEach((value, key) => {
1517
+ if (
1518
+ key !== PROXY_HEADER &&
1519
+ key !== 'host' &&
1520
+ !key.startsWith('x-forwarded') &&
1521
+ key !== 'connection' &&
1522
+ key !== 'accept-encoding'
1523
+ ) {
1524
+ headers.set(key, value)
1525
+ }
1526
+ })
1527
+ // Request uncompressed responses from the target. The proxy streams
1528
+ // non-HTML/CSS bodies directly and strips content-encoding, so
1529
+ // receiving compressed bytes would corrupt JS chunks, fonts, etc.
1530
+ headers.set('accept-encoding', 'identity')
1531
+
1532
+ const response = await fetch(url.toString(), {
1533
+ method: request.method,
1534
+ headers,
1535
+ body:
1536
+ request.method !== 'GET' && request.method !== 'HEAD'
1537
+ ? await request.arrayBuffer()
1538
+ : undefined,
1539
+ signal: controller.signal,
1540
+ redirect: 'manual',
1541
+ })
1542
+
1543
+ clearTimeout(timeout)
1544
+
1545
+ const contentType = response.headers.get('content-type') || ''
1546
+ // Headers to strip from proxied responses:
1547
+ // - content-encoding/transfer-encoding: proxy re-encodes the body
1548
+ // - COEP/COOP/CORP: block inspector script and iframe embedding
1549
+ // - CSP: may restrict inline scripts (inspector) and postMessage
1550
+ // - X-Frame-Options: prevents iframe embedding of proxied pages
1551
+ const STRIP_HEADERS = new Set([
1552
+ 'content-encoding',
1553
+ 'transfer-encoding',
1554
+ 'cross-origin-embedder-policy',
1555
+ 'cross-origin-opener-policy',
1556
+ 'cross-origin-resource-policy',
1557
+ 'content-security-policy',
1558
+ 'content-security-policy-report-only',
1559
+ 'x-frame-options',
1560
+ ])
1561
+
1562
+ // Handle redirects from the target (e.g. auth middleware redirecting to /login).
1563
+ // Rewrite the Location header to go through the proxy so the browser stays
1564
+ // within the iframe and cookies/auth state are preserved.
1565
+ if (response.status >= 300 && response.status < 400) {
1566
+ const location = response.headers.get('location')
1567
+ if (location) {
1568
+ let rewrittenLocation = location
1569
+ const targetOriginForRedirect = new URL(targetUrl).origin
1570
+ try {
1571
+ const locUrl = new URL(location, targetUrl)
1572
+ // Only rewrite same-origin redirects (to the target server)
1573
+ if (locUrl.origin === targetOriginForRedirect) {
1574
+ const sep = locUrl.search ? '&' : '?'
1575
+ rewrittenLocation = `/api/proxy${locUrl.pathname}${locUrl.search}${sep}${PROXY_HEADER}=${encodeURIComponent(targetUrl)}${locUrl.hash}`
1576
+ }
1577
+ } catch {
1578
+ // If URL parsing fails, pass through as-is
1579
+ }
1580
+ const redirectHeaders = new Headers()
1581
+ response.headers.forEach((value, key) => {
1582
+ if (!STRIP_HEADERS.has(key) && key !== 'location') {
1583
+ if (key === 'set-cookie') {
1584
+ redirectHeaders.append(key, value)
1585
+ } else {
1586
+ redirectHeaders.set(key, value)
1587
+ }
1588
+ }
1589
+ })
1590
+ redirectHeaders.set('location', rewrittenLocation)
1591
+ return new NextResponse(null, {
1592
+ status: response.status,
1593
+ headers: redirectHeaders,
1594
+ })
1595
+ }
1596
+ }
1597
+ const responseHeaders = new Headers()
1598
+ response.headers.forEach((value, key) => {
1599
+ if (!STRIP_HEADERS.has(key)) {
1600
+ // Use append for set-cookie to preserve multiple cookies (e.g. Supabase
1601
+ // auth sends separate access-token, refresh-token, etc. cookies).
1602
+ // Using set() would overwrite all but the last cookie.
1603
+ if (key === 'set-cookie') {
1604
+ responseHeaders.append(key, value)
1605
+ } else {
1606
+ responseHeaders.set(key, value)
1607
+ }
1608
+ }
1609
+ })
1610
+
1611
+ // Inject inspector script into HTML responses
1612
+ if (contentType.includes('text/html')) {
1613
+ let html = await response.text()
1614
+
1615
+ // Strip any existing pAInt inspector scripts the target app may
1616
+ // have added manually. The proxy injects its own inspector, so these
1617
+ // duplicates cause multiple overlays and conflicting message handlers.
1618
+ html = html.replace(
1619
+ /<script[^>]*src=["'][^"']*dev-editor-inspector\.js["'][^>]*><\/script>/gi,
1620
+ '',
1621
+ )
1622
+
1623
+ // Rewrite asset URLs to go through proxy, preserving target param
1624
+ const encodedTarget = encodeURIComponent(targetUrl)
1625
+ const targetOrigin = new URL(targetUrl).origin
1626
+ const escapedOrigin = targetOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1627
+
1628
+ // Helper to rewrite a single absolute path, preserving URL fragments
1629
+ function proxyPath(originalPath: string): string {
1630
+ let pathPart = originalPath
1631
+ let fragment = ''
1632
+ const hashIdx = originalPath.indexOf('#')
1633
+ if (hashIdx >= 0) {
1634
+ pathPart = originalPath.substring(0, hashIdx)
1635
+ fragment = originalPath.substring(hashIdx)
1636
+ }
1637
+ const separator = pathPart.includes('?') ? '&' : '?'
1638
+ return `/api/proxy${pathPart}${separator}${PROXY_HEADER}=${encodedTarget}${fragment}`
1639
+ }
1640
+
1641
+ // Rewrite fully-qualified target-origin URLs in attributes FIRST
1642
+ // (e.g., http://localhost:4000/avatars/img.png → /api/proxy/avatars/img.png?...)
1643
+ html = html.replace(
1644
+ new RegExp(
1645
+ `(href|src|action|poster)=(["'])${escapedOrigin}(/[^"']*)`,
1646
+ 'g',
1647
+ ),
1648
+ (_match: string, attr: string, quote: string, pathPart: string) => {
1649
+ // Tag /_next/ paths with _devproxy marker so middleware can identify
1650
+ // them as iframe-originated after history.replaceState changes the referer.
1651
+ // Do NOT add /api/proxy/ prefix — that breaks Turbopack chunk path matching.
1652
+ if (pathPart.startsWith('/_next/')) {
1653
+ const sep = pathPart.includes('?') ? '&' : '?'
1654
+ return `${attr}=${quote}${pathPart}${sep}_devproxy=1`
1655
+ }
1656
+ return `${attr}=${quote}${proxyPath(pathPart)}`
1657
+ },
1658
+ )
1659
+
1660
+ // Rewrite fully-qualified target-origin URLs in srcset
1661
+ html = html.replace(
1662
+ new RegExp(`${escapedOrigin}(/[^\\s,)"']+)`, 'g'),
1663
+ (_match: string, pathPart: string) => {
1664
+ // Only rewrite inside attribute contexts (not in script text)
1665
+ return proxyPath(pathPart)
1666
+ },
1667
+ )
1668
+
1669
+ // Rewrite src, href, action, poster attributes (absolute paths starting with /)
1670
+ // Tag /_next/ paths with ?_devproxy=1 so the middleware can proxy them to
1671
+ // the target server. Without this marker, after history.replaceState changes
1672
+ // the URL, the referer no longer contains /api/proxy and the middleware
1673
+ // skips the request — loading pAInt's own chunks instead of the
1674
+ // target's, which prevents React from hydrating.
1675
+ // Do NOT add /api/proxy/ prefix — that breaks Turbopack chunk path matching.
1676
+ // Turbopack's getPathFromScript() strips query strings, so the marker is safe.
1677
+ html = html.replace(
1678
+ /(href|src|action|poster)=(["'])(\/[^"']*)/g,
1679
+ (_match: string, attr: string, quote: string, originalPath: string) => {
1680
+ // Skip already-rewritten paths
1681
+ if (originalPath.startsWith('/api/proxy')) return _match
1682
+ // Tag /_next/ paths with _devproxy marker for middleware identification
1683
+ if (originalPath.startsWith('/_next/')) {
1684
+ if (originalPath.includes('_devproxy=')) return _match
1685
+ const sep = originalPath.includes('?') ? '&' : '?'
1686
+ return `${attr}=${quote}${originalPath}${sep}_devproxy=1`
1687
+ }
1688
+ return `${attr}=${quote}${proxyPath(originalPath)}`
1689
+ },
1690
+ )
1691
+
1692
+ // Rewrite xlink:href for SVG <use> elements (absolute paths starting with /)
1693
+ html = html.replace(
1694
+ /xlink:href=(["'])(\/[^"']*)/g,
1695
+ (_match: string, quote: string, originalPath: string) => {
1696
+ if (originalPath.startsWith('/api/proxy')) return _match
1697
+ return `xlink:href=${quote}${proxyPath(originalPath)}`
1698
+ },
1699
+ )
1700
+
1701
+ // Rewrite srcset attributes — each entry is "url descriptor, ..."
1702
+ html = html.replace(
1703
+ /srcset=(["'])([^"']+)/g,
1704
+ (_match: string, quote: string, srcsetValue: string) => {
1705
+ const rewritten = srcsetValue.replace(
1706
+ /(\/[^\s,]+)/g,
1707
+ (urlPart: string) => {
1708
+ if (urlPart.startsWith('/api/proxy')) return urlPart
1709
+ return proxyPath(urlPart)
1710
+ },
1711
+ )
1712
+ return `srcset=${quote}${rewritten}`
1713
+ },
1714
+ )
1715
+
1716
+ // Rewrite data-src and data-srcset (common lazy-loading patterns)
1717
+ html = html.replace(
1718
+ /data-src=(["'])(\/[^"']*)/g,
1719
+ (_match: string, quote: string, originalPath: string) => {
1720
+ if (originalPath.startsWith('/api/proxy')) return _match
1721
+ return `data-src=${quote}${proxyPath(originalPath)}`
1722
+ },
1723
+ )
1724
+ html = html.replace(
1725
+ /data-srcset=(["'])([^"']+)/g,
1726
+ (_match: string, quote: string, srcsetValue: string) => {
1727
+ const rewritten = srcsetValue.replace(
1728
+ /(\/[^\s,]+)/g,
1729
+ (urlPart: string) => {
1730
+ if (urlPart.startsWith('/api/proxy')) return urlPart
1731
+ return proxyPath(urlPart)
1732
+ },
1733
+ )
1734
+ return `data-srcset=${quote}${rewritten}`
1735
+ },
1736
+ )
1737
+
1738
+ // Rewrite CSS url() references in inline styles — both absolute paths and full URLs
1739
+ html = html.replace(
1740
+ /url\((["']?)(\/[^)"']+)\1\)/g,
1741
+ (_match: string, quote: string, originalPath: string) => {
1742
+ if (originalPath.startsWith('/api/proxy')) return _match
1743
+ return `url(${quote}${proxyPath(originalPath)}${quote})`
1744
+ },
1745
+ )
1746
+ html = html.replace(
1747
+ new RegExp(`url\\((["']?)${escapedOrigin}(/[^)"']+)\\1\\)`, 'g'),
1748
+ (_match: string, quote: string, pathPart: string) => {
1749
+ return `url(${quote}${proxyPath(pathPart)}${quote})`
1750
+ },
1751
+ )
1752
+
1753
+ // Rewrite @import in <style> blocks
1754
+ html = html.replace(
1755
+ /@import\s+(["'])(\/[^"']+)\1/g,
1756
+ (_match: string, quote: string, originalPath: string) => {
1757
+ return `@import ${quote}${proxyPath(originalPath)}${quote}`
1758
+ },
1759
+ )
1760
+
1761
+ // --- Strip target-page scripts to prevent hydration failures ---
1762
+ // Remove ALL <script> tags from the target HTML except:
1763
+ // - type="application/ld+json" (structured data / JSON-LD)
1764
+ // The SSR-rendered HTML + CSS is sufficient for visual editing.
1765
+ // Allowing target scripts to execute causes Next.js hydration to
1766
+ // run, fail (missing chunks like GSAP, layout.js → 404), and wipe
1767
+ // out the pre-rendered content entirely — leaving a blank page.
1768
+ // Our own scripts (nav blocker, interceptor, inspector) are injected
1769
+ // AFTER this stripping step, so they are not affected.
1770
+ html = html.replace(
1771
+ /<script\b[^>]*>[\s\S]*?<\/script>/gi,
1772
+ (match: string) => {
1773
+ if (/type\s*=\s*["']application\/ld\+json["']/i.test(match)) {
1774
+ return match
1775
+ }
1776
+ return ''
1777
+ },
1778
+ )
1779
+
1780
+ // --- Navigation blocker ---
1781
+ // Even with target scripts stripped, we inject a navigation blocker that:
1782
+ // 1. Fixes the URL via history.replaceState so the inspector reports
1783
+ // correct paths (not /api/proxy/...)
1784
+ // 2. Rewrites dynamically-set resource URLs through the proxy
1785
+ // 3. Intercepts any remaining navigations to keep the iframe in the proxy
1786
+ // 4. Patches fetch/XHR in case the inspector or other injected scripts
1787
+ // need to make requests through the proxy
1788
+ // 5. Suppresses HMR-related errors as a safety net
1789
+ // 6. Detects infinite reload loops as a safety net
1790
+ const targetPagePath = '/' + (path || '')
1791
+ const safePagePath = JSON.stringify(targetPagePath)
1792
+ const safeTargetUrl = JSON.stringify(targetUrl)
1793
+ const safeEncodedTarget = JSON.stringify(encodedTarget)
1794
+ const safeProxyHeader = JSON.stringify(PROXY_HEADER)
1795
+
1796
+ const navigationBlockerScript = `<script data-dev-editor-nav-blocker>
1797
+ (function(){
1798
+ var tP=${safePagePath},tU=${safeTargetUrl},eT=${safeEncodedTarget},pH=${safeProxyHeader};
1799
+
1800
+ // Fix URL so client-side routers see the correct path
1801
+ try {
1802
+ var p = new URLSearchParams(window.location.search);
1803
+ p.delete(pH);
1804
+ var qs = p.toString();
1805
+ history.replaceState(history.state, '', tP + (qs ? '?' + qs : ''));
1806
+ } catch(e) {}
1807
+
1808
+ // Block duplicate inspector scripts — the proxy injects its own inline
1809
+ // inspector, so any external dev-editor-inspector.js must be suppressed.
1810
+ // Intercept HTMLScriptElement.src setter to prevent dynamic loading.
1811
+ var scrDesc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src');
1812
+ if (scrDesc && scrDesc.set) {
1813
+ Object.defineProperty(HTMLScriptElement.prototype, 'src', {
1814
+ get: scrDesc.get,
1815
+ set: function(val) {
1816
+ if (typeof val === 'string' && val.indexOf('dev-editor-inspector') >= 0) return;
1817
+ if (typeof val === 'string') {
1818
+ var r = proxyResUrl(val);
1819
+ if (r) val = r;
1820
+ }
1821
+ scrDesc.set.call(this, val);
1822
+ },
1823
+ configurable: true, enumerable: true
1824
+ });
1825
+ }
1826
+ // Reload safety net - detect infinite reload loops
1827
+ var rk = '_der';
1828
+ var rc = parseInt(sessionStorage.getItem(rk) || '0');
1829
+ sessionStorage.setItem(rk, String(rc + 1));
1830
+ setTimeout(function(){ sessionStorage.removeItem(rk); }, 3000);
1831
+ if (rc > 4) { sessionStorage.removeItem(rk); window.stop(); return; }
1832
+
1833
+ // HMR isolation: return alive-looking WebSocket/EventSource mocks for HMR
1834
+ // connections. Turbopack bootstrapping does NOT require the HMR WebSocket —
1835
+ // initial module loading happens via <script> tags. The HMR connection is
1836
+ // only for live updates. Routing it to the target server causes CORS failures.
1837
+ // IMPORTANT: Mocks must report readyState=1 (OPEN) and fire the 'open' event
1838
+ // so the HMR client believes it's connected. Previous mocks reported CLOSED
1839
+ // and fired close/error, causing the HMR client to enter a reconnection loop
1840
+ // that eventually triggered window.location.reload() every ~40 seconds.
1841
+ var OWS = window.WebSocket;
1842
+ window.WebSocket = function(u, pr) {
1843
+ var s = String(u);
1844
+ if (s.indexOf('_next') >= 0 || s.indexOf('hmr') >= 0 || s.indexOf('webpack') >= 0 || s.indexOf('turbopack') >= 0 || s.indexOf('hot-update') >= 0) {
1845
+ var _wl = {};
1846
+ var m = {
1847
+ readyState: 1,
1848
+ bufferedAmount: 0, extensions: '', protocol: '', url: s, binaryType: 'blob',
1849
+ close: function(){ m.readyState = 3; },
1850
+ send: function(){},
1851
+ addEventListener: function(t, fn){ if(!_wl[t]) _wl[t]=[]; _wl[t].push(fn); },
1852
+ removeEventListener: function(t, fn){ if(_wl[t]) _wl[t]=_wl[t].filter(function(f){return f!==fn;}); },
1853
+ dispatchEvent: function(){ return true; }
1854
+ };
1855
+ setTimeout(function(){
1856
+ var ev = {type:'open'};
1857
+ if (m.onopen) try { m.onopen(ev); } catch(e) {}
1858
+ if (_wl.open) for (var i=0; i<_wl.open.length; i++) try { _wl.open[i](ev); } catch(e) {}
1859
+ }, 10);
1860
+ return m;
1861
+ }
1862
+ return pr !== undefined ? new OWS(u, pr) : new OWS(u);
1863
+ };
1864
+ window.WebSocket.CONNECTING=0; window.WebSocket.OPEN=1; window.WebSocket.CLOSING=2; window.WebSocket.CLOSED=3;
1865
+
1866
+ var OES = window.EventSource;
1867
+ if (OES) {
1868
+ window.EventSource = function(u, c) {
1869
+ var s = String(u);
1870
+ if (s.indexOf('hmr') >= 0 || s.indexOf('hot') >= 0 || s.indexOf('turbopack') >= 0 || s.indexOf('webpack') >= 0 || s.indexOf('_next') >= 0) {
1871
+ var _el = {};
1872
+ var m = {
1873
+ readyState: 1, url: s, withCredentials: false,
1874
+ close: function(){ m.readyState = 2; },
1875
+ addEventListener: function(t, fn){ if(!_el[t]) _el[t]=[]; _el[t].push(fn); },
1876
+ removeEventListener: function(t, fn){ if(_el[t]) _el[t]=_el[t].filter(function(f){return f!==fn;}); },
1877
+ dispatchEvent: function(){ return true; }
1878
+ };
1879
+ setTimeout(function(){
1880
+ var ev = {type:'open'};
1881
+ if (m.onopen) try { m.onopen(ev); } catch(e) {}
1882
+ if (_el.open) for (var i=0; i<_el.open.length; i++) try { _el.open[i](ev); } catch(e) {}
1883
+ }, 10);
1884
+ return m;
1885
+ }
1886
+ return c ? new OES(u, c) : new OES(u);
1887
+ };
1888
+ window.EventSource.CONNECTING=0; window.EventSource.OPEN=1; window.EventSource.CLOSED=2;
1889
+ }
1890
+
1891
+ // Intercept navigations via Navigation API to prevent the iframe from
1892
+ // escaping the proxy. After history.replaceState changes the URL to the
1893
+ // target path (e.g. http://localhost:4000/), any unintercepted navigation
1894
+ // would load pAInt's own page instead of the target — causing
1895
+ // the "recursive embed" bug where the setup modal appears inside the iframe.
1896
+ if (window.navigation) {
1897
+ window.navigation.addEventListener('navigate', function(e) {
1898
+ if (e.hashChange) return;
1899
+ try {
1900
+ var d = new URL(e.destination.url);
1901
+ // Already going through proxy — allow
1902
+ if (d.pathname.indexOf('/api/proxy') === 0) return;
1903
+ // Cross-origin navigation — allow (external links)
1904
+ if (d.origin !== window.location.origin) return;
1905
+ // Same-origin, not through proxy — must intercept to prevent
1906
+ // loading pAInt's own page (recursive embed)
1907
+ if (e.canIntercept) {
1908
+ e.intercept({
1909
+ handler: function() {
1910
+ // User-initiated (link click, form submit) or reload:
1911
+ // redirect through the proxy so the target page loads correctly
1912
+ if (e.userInitiated || e.navigationType === 'reload') {
1913
+ if (e.userInitiated) {
1914
+ window.parent.postMessage({type:'PAGE_NAVIGATE', payload:{path:d.pathname}}, window.location.origin);
1915
+ }
1916
+ var sep = d.search ? '&' : '?';
1917
+ window.location.replace('/api/proxy' + d.pathname + d.search + sep + pH + '=' + eT);
1918
+ return new Promise(function() {});
1919
+ }
1920
+ // Programmatic push/replace (SPA router internal navigation):
1921
+ // resolve immediately without full navigation to prevent
1922
+ // infinite reload loops while keeping the router functional
1923
+ return Promise.resolve();
1924
+ }
1925
+ });
1926
+ }
1927
+ } catch(err) {}
1928
+ });
1929
+ }
1930
+
1931
+ // Patch fetch for same-origin AND target-origin API calls
1932
+ var oF = window.fetch;
1933
+ var tO = new URL(tU).origin;
1934
+ function rewriteUrl(s) {
1935
+ if (typeof s !== 'string') return s;
1936
+ // Relative paths starting with /
1937
+ if (s.charAt(0) === '/' && s.indexOf('/api/proxy') !== 0) {
1938
+ return '/api/proxy' + s + (s.indexOf('?') >= 0 ? '&' : '?') + pH + '=' + eT;
1939
+ }
1940
+ // Fully-qualified target-origin URLs (e.g. http://localhost:4000/api/data)
1941
+ if (s.indexOf(tO) === 0) {
1942
+ var path = s.substring(tO.length) || '/';
1943
+ return '/api/proxy' + path + (path.indexOf('?') >= 0 ? '&' : '?') + pH + '=' + eT;
1944
+ }
1945
+ return s;
1946
+ }
1947
+ window.fetch = function(i, n) {
1948
+ try {
1949
+ if (typeof i === 'string') {
1950
+ i = rewriteUrl(i);
1951
+ } else if (typeof Request !== 'undefined' && i instanceof Request) {
1952
+ var u = new URL(i.url);
1953
+ if ((u.origin === window.location.origin && u.pathname.indexOf('/api/proxy') !== 0) || u.origin === tO) {
1954
+ var rp = u.origin === tO ? u.pathname : u.pathname;
1955
+ i = new Request('/api/proxy' + rp + u.search + (u.search ? '&' : '?') + pH + '=' + eT, i);
1956
+ }
1957
+ }
1958
+ } catch(e) {}
1959
+ return oF.call(this, i, n);
1960
+ };
1961
+
1962
+ // Patch XMLHttpRequest for same-origin AND target-origin calls
1963
+ var oX = XMLHttpRequest.prototype.open;
1964
+ XMLHttpRequest.prototype.open = function(m, u) {
1965
+ try {
1966
+ if (typeof u === 'string') {
1967
+ arguments[1] = rewriteUrl(u);
1968
+ }
1969
+ } catch(e) {}
1970
+ return oX.apply(this, arguments);
1971
+ };
1972
+
1973
+ // Synchronous property interceptors — rewrite URLs BEFORE the browser
1974
+ // starts fetching resources. MutationObserver is async (too late for images).
1975
+ // All interceptors use proxyResUrl() which handles fragments correctly.
1976
+ // Patch Element.prototype.setAttribute to catch attr-based URL setting.
1977
+ var oSA = Element.prototype.setAttribute;
1978
+ Element.prototype.setAttribute = function(name, value) {
1979
+ if (typeof value === 'string') {
1980
+ var n = name.toLowerCase();
1981
+ // Block duplicate inspector scripts set via setAttribute
1982
+ if (n === 'src' && this.tagName === 'SCRIPT' && value.indexOf('dev-editor-inspector') >= 0) {
1983
+ return;
1984
+ }
1985
+ if (n === 'src' || n === 'poster' || n === 'data-src' || (n === 'href' && this.tagName !== 'A')) {
1986
+ var r = proxyResUrl(value);
1987
+ if (r) value = r;
1988
+ }
1989
+ if (n === 'srcset') {
1990
+ var parts = value.split(',');
1991
+ var rewritten = [];
1992
+ for (var si = 0; si < parts.length; si++) {
1993
+ var entry = parts[si].trim();
1994
+ var spIdx = entry.indexOf(' ');
1995
+ var url = spIdx >= 0 ? entry.substring(0, spIdx) : entry;
1996
+ var desc = spIdx >= 0 ? entry.substring(spIdx) : '';
1997
+ var ru = proxyResUrl(url);
1998
+ rewritten.push((ru || url) + desc);
1999
+ }
2000
+ value = rewritten.join(', ');
2001
+ }
2002
+ }
2003
+ return oSA.call(this, name, value);
2004
+ };
2005
+
2006
+ // Patch .src property on HTMLImageElement — React sets img.src directly
2007
+ var imgDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');
2008
+ if (imgDesc && imgDesc.set) {
2009
+ Object.defineProperty(HTMLImageElement.prototype, 'src', {
2010
+ get: imgDesc.get,
2011
+ set: function(val) {
2012
+ var r = proxyResUrl(val);
2013
+ imgDesc.set.call(this, r || val);
2014
+ },
2015
+ configurable: true, enumerable: true
2016
+ });
2017
+ }
2018
+
2019
+ // Patch .src on HTMLSourceElement (for <source> in <picture>/<video>)
2020
+ var srcDesc = Object.getOwnPropertyDescriptor(HTMLSourceElement.prototype, 'src');
2021
+ if (srcDesc && srcDesc.set) {
2022
+ Object.defineProperty(HTMLSourceElement.prototype, 'src', {
2023
+ get: srcDesc.get,
2024
+ set: function(val) {
2025
+ var r = proxyResUrl(val);
2026
+ srcDesc.set.call(this, r || val);
2027
+ },
2028
+ configurable: true, enumerable: true
2029
+ });
2030
+ }
2031
+
2032
+ // Intercept FontFace constructor — Expo/React Native Web and other frameworks
2033
+ // load icon fonts (Ionicons, Material Icons, etc.) via new FontFace('name', 'url(...)')
2034
+ // instead of <style> elements. Without this, the font URL bypasses the proxy.
2035
+ var OFontFace = window.FontFace;
2036
+ if (OFontFace) {
2037
+ window.FontFace = function(family, source, descriptors) {
2038
+ if (typeof source === 'string') {
2039
+ source = source.replace(/url\\(\\s*(['"]?)([^)'"\\s]+)\\1\\s*\\)/g, function(m, q, urlVal) {
2040
+ var r = proxyResUrl(urlVal);
2041
+ return r ? 'url(' + q + r + q + ')' : m;
2042
+ });
2043
+ }
2044
+ return new OFontFace(family, source, descriptors);
2045
+ };
2046
+ window.FontFace.prototype = OFontFace.prototype;
2047
+ // Preserve static methods if any
2048
+ Object.keys(OFontFace).forEach(function(k) {
2049
+ try { window.FontFace[k] = OFontFace[k]; } catch(e) {}
2050
+ });
2051
+ }
2052
+
2053
+ // Rewrite url() references in dynamically-injected <style> elements
2054
+ // (e.g., icon font CSS injected by Font Awesome, Material Icons, etc.)
2055
+ var _processedStyles = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
2056
+ function rewriteStyleUrls(styleEl) {
2057
+ if (_processedStyles) {
2058
+ if (_processedStyles.has(styleEl)) return;
2059
+ _processedStyles.add(styleEl);
2060
+ }
2061
+ var css = styleEl.textContent;
2062
+ if (!css || css.indexOf('url(') < 0) return;
2063
+ var newCss = css.replace(/url\\(\\s*(['"]?)([^)'"\\s]+)\\1\\s*\\)/g, function(m, q, urlVal) {
2064
+ var r = proxyResUrl(urlVal);
2065
+ return r ? 'url(' + q + r + q + ')' : m;
2066
+ });
2067
+ if (newCss !== css) styleEl.textContent = newCss;
2068
+ }
2069
+
2070
+ // Runtime interceptor: rewrite src/href on dynamically-created elements
2071
+ // and url() in <style> elements (icon fonts, etc.)
2072
+ var rObs = new MutationObserver(function(mutations) {
2073
+ for (var mi = 0; mi < mutations.length; mi++) {
2074
+ var added = mutations[mi].addedNodes;
2075
+ for (var ni = 0; ni < added.length; ni++) {
2076
+ var node = added[ni];
2077
+ // Text node added inside <style> (textContent change on icon font CSS)
2078
+ if (node.nodeType === 3 && node.parentElement && node.parentElement.tagName === 'STYLE') {
2079
+ rewriteStyleUrls(node.parentElement);
2080
+ continue;
2081
+ }
2082
+ if (node.nodeType !== 1) continue;
2083
+ // <style> element with @font-face or background-image url() references
2084
+ if (node.tagName === 'STYLE') {
2085
+ rewriteStyleUrls(node);
2086
+ }
2087
+ rewriteNodeUrls(node);
2088
+ if (node.querySelectorAll) {
2089
+ var children = node.querySelectorAll('[src],[href],[poster],[data-src],[srcset]');
2090
+ for (var ci = 0; ci < children.length; ci++) rewriteNodeUrls(children[ci]);
2091
+ // Also rewrite url() in <style> elements within the added subtree
2092
+ var styles = node.querySelectorAll('style');
2093
+ for (var sti = 0; sti < styles.length; sti++) rewriteStyleUrls(styles[sti]);
2094
+ }
2095
+ }
2096
+ // Handle attribute changes (e.g., lazy-load libraries setting src)
2097
+ if (mutations[mi].type === 'attributes') {
2098
+ rewriteNodeUrls(mutations[mi].target);
2099
+ }
2100
+ }
2101
+ });
2102
+ // Rewrite a resource URL to go through proxy. Returns null if no rewrite needed.
2103
+ // Handles URL fragments (#id) by placing them after the query string.
2104
+ function proxyResUrl(val) {
2105
+ if (!val || typeof val !== 'string') return null;
2106
+ if (val.indexOf('/api/proxy') === 0) return null;
2107
+ if (val.indexOf('data:') === 0 || val.indexOf('blob:') === 0 || val.charAt(0) === '#' || val.indexOf('javascript:') === 0) return null;
2108
+ // Mark /_next/ paths with ?_devproxy=1 so the middleware can identify them as
2109
+ // iframe-originated requests. We must NOT add the /api/proxy/ prefix
2110
+ // because that corrupts turbopack's getPathFromScript() chunk path matching.
2111
+ // The middleware strips the query string before proxying. Turbopack's
2112
+ // getPathFromScript also strips query strings, so path matching still works.
2113
+ // NOTE: Do NOT use "_dp" — Next.js uses ?_dp=1 internally for CSS preloading.
2114
+ if (val.indexOf('/_next/') === 0) {
2115
+ if (val.indexOf('_devproxy=') >= 0) return null; // Already marked
2116
+ return val + (val.indexOf('?') >= 0 ? '&' : '?') + '_devproxy=1';
2117
+ }
2118
+ var fragment = '';
2119
+ var hashIdx = val.indexOf('#');
2120
+ var urlPart = val;
2121
+ if (hashIdx >= 0) { urlPart = val.substring(0, hashIdx); fragment = val.substring(hashIdx); }
2122
+ var proxied = null;
2123
+ if (urlPart.indexOf(tO) === 0) {
2124
+ proxied = '/api/proxy' + (urlPart.substring(tO.length) || '/');
2125
+ } else if (urlPart.charAt(0) === '/') {
2126
+ proxied = '/api/proxy' + urlPart;
2127
+ }
2128
+ if (proxied) {
2129
+ return proxied + (proxied.indexOf('?') >= 0 ? '&' : '?') + pH + '=' + eT + fragment;
2130
+ }
2131
+ return null;
2132
+ }
2133
+ function rewriteNodeUrls(el) {
2134
+ if (!el || !el.getAttribute) return;
2135
+ var tag = el.tagName;
2136
+ // Resource attributes that need proxy rewriting
2137
+ var attrs = ['src', 'poster', 'data-src'];
2138
+ // Rewrite href for non-anchor elements (stylesheets, etc.) but NOT <a> tags
2139
+ // (anchor clicks are handled by the Navigation API intercept above)
2140
+ if (tag !== 'A') attrs.push('href');
2141
+ for (var ai = 0; ai < attrs.length; ai++) {
2142
+ var val = el.getAttribute(attrs[ai]);
2143
+ var r = proxyResUrl(val);
2144
+ if (r) el.setAttribute(attrs[ai], r);
2145
+ }
2146
+ // SVG xlink:href for <use> elements (e.g., <use xlink:href="/sprites.svg#icon">)
2147
+ if (el.getAttributeNS) {
2148
+ var xval = el.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
2149
+ var xr = proxyResUrl(xval);
2150
+ if (xr) el.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', xr);
2151
+ }
2152
+ // Handle srcset — split on comma, rewrite each entry
2153
+ var srcset = el.getAttribute('srcset');
2154
+ if (srcset) {
2155
+ var parts = srcset.split(',');
2156
+ var changed = false;
2157
+ var rewritten = [];
2158
+ for (var si = 0; si < parts.length; si++) {
2159
+ var entry = parts[si].trim();
2160
+ var spIdx = entry.indexOf(' ');
2161
+ var url = spIdx >= 0 ? entry.substring(0, spIdx) : entry;
2162
+ var desc = spIdx >= 0 ? entry.substring(spIdx) : '';
2163
+ var ru = proxyResUrl(url);
2164
+ if (ru) { rewritten.push(ru + desc); changed = true; }
2165
+ else rewritten.push(entry);
2166
+ }
2167
+ if (changed) {
2168
+ el.setAttribute('srcset', rewritten.join(', '));
2169
+ }
2170
+ }
2171
+ }
2172
+ // Scan existing <style> elements in <head> that may have been added before
2173
+ // the observer started (e.g., server-rendered @font-face or early JS injection)
2174
+ var existingStyles = document.querySelectorAll('head style, style');
2175
+ for (var esi = 0; esi < existingStyles.length; esi++) {
2176
+ rewriteStyleUrls(existingStyles[esi]);
2177
+ }
2178
+
2179
+ // Observe document.documentElement (<html>) instead of just document.body
2180
+ // so we catch <style> elements with @font-face injected into <head> by
2181
+ // frameworks like Expo/React Native Web and icon font loaders.
2182
+ var obsRoot = document.documentElement || document.body;
2183
+ if (obsRoot) {
2184
+ rObs.observe(obsRoot, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'href', 'poster', 'data-src', 'srcset'] });
2185
+ } else {
2186
+ document.addEventListener('DOMContentLoaded', function() {
2187
+ rObs.observe(document.documentElement || document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'href', 'poster', 'data-src', 'srcset'] });
2188
+ });
2189
+ }
2190
+
2191
+ // Suppress HMR and chunk-loading errors
2192
+ function isProxyNoise(s) {
2193
+ return s.indexOf('hmr') >= 0 || s.indexOf('hot') >= 0 || s.indexOf('WebSocket') >= 0 || s.indexOf('__webpack') >= 0 || s.indexOf('turbopack') >= 0 || s.indexOf('ChunkLoadError') >= 0 || s.indexOf('Loading chunk') >= 0 || s.indexOf('Loading CSS chunk') >= 0;
2194
+ }
2195
+ window.addEventListener('error', function(e) {
2196
+ if (isProxyNoise(e.message || '')) {
2197
+ e.stopImmediatePropagation(); e.preventDefault(); return false;
2198
+ }
2199
+ });
2200
+ window.addEventListener('unhandledrejection', function(e) {
2201
+ if (isProxyNoise(e.reason ? String(e.reason) : '')) {
2202
+ e.stopImmediatePropagation(); e.preventDefault();
2203
+ }
2204
+ });
2205
+ // Hide Next.js dev error overlay (uses <nextjs-portal> custom element)
2206
+ var hs=document.createElement('style');
2207
+ hs.textContent='nextjs-portal{display:none!important}';
2208
+ document.documentElement.appendChild(hs);
2209
+ })();
2210
+ </script>`
2211
+
2212
+ // Cookie setter for resource loading
2213
+ const urlInterceptorScript = `<script data-dev-editor-interceptor>
2214
+ (function(){
2215
+ document.cookie='${PROXY_HEADER}='+encodeURIComponent('${targetUrl}')+';path=/;SameSite=Strict;max-age=86400';
2216
+ })();
2217
+ </script>`
2218
+
2219
+ // Inject navigation blocker + cookie setter at the top of <head>.
2220
+ // IMPORTANT: Use function replacements to prevent $ characters in the
2221
+ // injected scripts from being interpreted as special replacement patterns
2222
+ // ($' = text after match, $& = matched text, etc.).
2223
+ const headInjection = navigationBlockerScript + urlInterceptorScript
2224
+ if (/<head>/i.test(html)) {
2225
+ html = html.replace(/<head>/i, (match) => match + headInjection)
2226
+ } else if (/<head\s/i.test(html)) {
2227
+ html = html.replace(/<head\s[^>]*>/i, (match) => match + headInjection)
2228
+ } else {
2229
+ html = headInjection + html
2230
+ }
2231
+
2232
+ // Set cookie on the response for dynamic resource loading
2233
+ responseHeaders.append(
2234
+ 'Set-Cookie',
2235
+ `${PROXY_HEADER}=${encodeURIComponent(targetUrl)}; Path=/; SameSite=Strict; Max-Age=86400`,
2236
+ )
2237
+
2238
+ // Strip CSP meta tags that could block the inline inspector script
2239
+ html = html.replace(
2240
+ /<meta\s+http-equiv=["']?Content-Security-Policy["']?[^>]*>/gi,
2241
+ '',
2242
+ )
2243
+
2244
+ // Inject inspector script before </body> (case-insensitive)
2245
+ if (/<\/body>/i.test(html)) {
2246
+ html = html.replace(/<\/body>/i, () => INSPECTOR_SCRIPT + '</body>')
2247
+ } else {
2248
+ html += INSPECTOR_SCRIPT
2249
+ }
2250
+
2251
+ responseHeaders.set('content-type', 'text/html; charset=utf-8')
2252
+ responseHeaders.set(
2253
+ 'cache-control',
2254
+ 'no-cache, no-store, must-revalidate',
2255
+ )
2256
+ responseHeaders.delete('content-length')
2257
+
2258
+ return new NextResponse(html, {
2259
+ status: response.status,
2260
+ headers: responseHeaders,
2261
+ })
2262
+ }
2263
+
2264
+ // Rewrite url() references in CSS responses
2265
+ if (contentType.includes('text/css')) {
2266
+ let css = await response.text()
2267
+ const cssEncodedTarget = encodeURIComponent(targetUrl)
2268
+ const cssTargetOrigin = new URL(targetUrl).origin
2269
+ const cssEscapedOrigin = cssTargetOrigin.replace(
2270
+ /[.*+?^${}()|[\]\\]/g,
2271
+ '\\$&',
2272
+ )
2273
+ // Rewrite absolute-path url() references
2274
+ css = css.replace(
2275
+ /url\(\s*(["']?)(\/[^)"'\s]+)\1\s*\)/g,
2276
+ (_match: string, quote: string, originalPath: string) => {
2277
+ if (originalPath.startsWith('/api/proxy')) return _match
2278
+ const separator = originalPath.includes('?') ? '&' : '?'
2279
+ return `url(${quote}/api/proxy${originalPath}${separator}${PROXY_HEADER}=${cssEncodedTarget}${quote})`
2280
+ },
2281
+ )
2282
+ // Rewrite fully-qualified target-origin url() references
2283
+ css = css.replace(
2284
+ new RegExp(
2285
+ `url\\(\\s*(["']?)${cssEscapedOrigin}(/[^)"'\\s]+)\\1\\s*\\)`,
2286
+ 'g',
2287
+ ),
2288
+ (_match: string, quote: string, pathPart: string) => {
2289
+ const separator = pathPart.includes('?') ? '&' : '?'
2290
+ return `url(${quote}/api/proxy${pathPart}${separator}${PROXY_HEADER}=${cssEncodedTarget}${quote})`
2291
+ },
2292
+ )
2293
+ // Rewrite @import with absolute paths
2294
+ css = css.replace(
2295
+ /@import\s+(["'])(\/[^"']+)\1/g,
2296
+ (_match: string, quote: string, originalPath: string) => {
2297
+ const separator = originalPath.includes('?') ? '&' : '?'
2298
+ return `@import ${quote}/api/proxy${originalPath}${separator}${PROXY_HEADER}=${cssEncodedTarget}${quote}`
2299
+ },
2300
+ )
2301
+ responseHeaders.set('content-type', 'text/css; charset=utf-8')
2302
+ responseHeaders.set(
2303
+ 'cache-control',
2304
+ 'no-cache, no-store, must-revalidate',
2305
+ )
2306
+ responseHeaders.delete('content-length')
2307
+ return new NextResponse(css, {
2308
+ status: response.status,
2309
+ headers: responseHeaders,
2310
+ })
2311
+ }
2312
+
2313
+ // Add CORS headers for fonts (often needed for cross-origin loading)
2314
+ if (
2315
+ contentType.includes('font/') ||
2316
+ contentType.includes('application/font') ||
2317
+ path.match(/\.(woff2?|ttf|eot|otf)(\?|$)/)
2318
+ ) {
2319
+ responseHeaders.set('access-control-allow-origin', '*')
2320
+ responseHeaders.set(
2321
+ 'cache-control',
2322
+ 'public, max-age=31536000, immutable',
2323
+ )
2324
+ }
2325
+
2326
+ // Images: always revalidate so updated assets on the target are
2327
+ // reflected immediately instead of being served from browser cache.
2328
+ if (
2329
+ contentType.includes('image/') ||
2330
+ path.match(/\.(png|jpg|jpeg|gif|svg|ico|webp|avif)(\?|$)/)
2331
+ ) {
2332
+ responseHeaders.set(
2333
+ 'cache-control',
2334
+ 'no-cache, no-store, must-revalidate',
2335
+ )
2336
+ }
2337
+
2338
+ // Passthrough other responses (streams body directly — no buffering)
2339
+ return new NextResponse(response.body, {
2340
+ status: response.status,
2341
+ headers: responseHeaders,
2342
+ })
2343
+ } catch (error) {
2344
+ if (error instanceof DOMException && error.name === 'AbortError') {
2345
+ return NextResponse.json(
2346
+ { error: 'Target server timeout (10s)' },
2347
+ { status: 504 },
2348
+ )
2349
+ }
2350
+ return NextResponse.json(
2351
+ { error: 'Target server is unreachable' },
2352
+ { status: 502 },
2353
+ )
2354
+ }
2355
+ }
2356
+
2357
+ export async function GET(
2358
+ request: NextRequest,
2359
+ context: { params: Promise<{ path?: string[] }> },
2360
+ ) {
2361
+ const params = await context.params
2362
+ return handleProxy(request, params)
2363
+ }
2364
+
2365
+ export async function POST(
2366
+ request: NextRequest,
2367
+ context: { params: Promise<{ path?: string[] }> },
2368
+ ) {
2369
+ const params = await context.params
2370
+ return handleProxy(request, params)
2371
+ }
2372
+
2373
+ export async function PUT(
2374
+ request: NextRequest,
2375
+ context: { params: Promise<{ path?: string[] }> },
2376
+ ) {
2377
+ const params = await context.params
2378
+ return handleProxy(request, params)
2379
+ }
2380
+
2381
+ export async function DELETE(
2382
+ request: NextRequest,
2383
+ context: { params: Promise<{ path?: string[] }> },
2384
+ ) {
2385
+ const params = await context.params
2386
+ return handleProxy(request, params)
2387
+ }
2388
+
2389
+ export async function OPTIONS(request: NextRequest) {
2390
+ return new NextResponse(null, {
2391
+ status: 204,
2392
+ headers: {
2393
+ 'access-control-allow-origin': '*',
2394
+ 'access-control-allow-methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
2395
+ 'access-control-allow-headers':
2396
+ request.headers.get('access-control-request-headers') || '*',
2397
+ 'access-control-max-age': '86400',
2398
+ },
2399
+ })
2400
+ }