UNPKG

creatr-devtools

Version:

Mini-Sentry + DOM Inspector toolkit for any React/Next app

1 lines 108 kB
{"version":3,"sources":["../src/inspector/DOMInspector.tsx","../src/DevtoolsProvider.tsx","../src/minisentry/error-normaliser.ts","../src/minisentry/snippet-resolver.ts","../src/minisentry/network-patcher.ts","../src/minisentry/index.ts","../src/branding/BrandingTag.tsx","../src/analytics/AnalyticsTracker.tsx","../src/messaging/createProxy.ts"],"sourcesContent":["//@ts-nocheck\n\"use client\";\n\nimport React, { useEffect, useState, useRef, PropsWithChildren } from \"react\";\n// Types and Interfaces\ninterface ElementMetadata {\n\ttagName: string;\n\tclassNames: string;\n\telementId: string | null;\n\ttextContent: string | null;\n\tboundingBox: DOMRect;\n\tattributes: Array<{\n\t\tname: string;\n\t\tvalue: string;\n\t}>;\n\tuniqueId: string;\n\tduplicateCount?: number; // Track number of elements with the same uniqueId\n\thasDynamicText?: boolean; // Track if element has data-dynamic-text attribute\n}\n\n/******************\n * DOM Inspector\n ******************/\n\ntype InspectorMessage =\n\t| {\n\t\ttype: \"TOGGLE_INSPECTOR\" | \"TOGGLE_IMAGE_INSPECTOR\";\n\t\tenabled: boolean;\n\t}\n\t| {\n\t\ttype: \"UPDATE_IMAGE_URL\";\n\t\turl: string;\n\t};\n\ninterface InspectorEvent extends MessageEvent {\n\tdata: InspectorMessage;\n}\n\ninterface InspectedElementMessage {\n\ttype: \"ELEMENT_INSPECTED\";\n\tmetadata: ElementMetadata | null;\n\tisShiftPressed: boolean;\n}\n\ninterface DOMInspectorProps {\n\thighlightColor?: string;\n\thighlightWidth?: number;\n}\n\ninterface HoverTooltipPosition {\n\tx: number;\n\ty: number;\n}\n\nconst DOMInspector: React.FC<PropsWithChildren<DOMInspectorProps>> = ({\n\tchildren,\n\thighlightColor = \"#4CAF50\",\n\thighlightWidth = 2,\n}) => {\n\tconst [isInspectorActive, setIsInspectorActive] = useState(false);\n\tconst [isImageInspectorActive, setIsImageInspectorActive] = useState(false);\n\tconst [isEditorActive, setIsEditorActive] = useState(false);\n\n\t// Refs for hover/active highlight boxes\n\tconst hoverHighlightRef = useRef<HTMLDivElement>(null);\n\tconst activeHighlightRef = useRef<HTMLDivElement>(null);\n\tconst selectedHighlightsRef = useRef<HTMLDivElement[]>([]);\n\n\t// Container for the inspector\n\tconst containerRef = useRef<HTMLDivElement>(null);\n\n\t// Track hovered/active elements\n\tconst hoveredElementRef = useRef<HTMLElement | null>(null);\n\tconst activeElementRef = useRef<HTMLElement | null>(null);\n\n\t// Track the IntersectionObserver instances\n\tconst elementObserversRef = useRef<Map<HTMLElement, IntersectionObserver>>(new Map());\n\n\t// Track element visibility states\n\tconst elementVisibilityRef = useRef<Map<HTMLElement, ElementVisibilityInfo>>(new Map());\n\n\t// Optional tooltip data\n\tconst [tooltipPosition, setTooltipPosition] = useState<HoverTooltipPosition>({\n\t\tx: 0,\n\t\ty: 0,\n\t});\n\tconst [hoveredMetadata, setHoveredMetadata] = useState<string | null>(null);\n\n\t// If you need to handle \"loading\" images\n\tconst [loadingImages, setLoadingImages] = useState<Set<HTMLImageElement>>(\n\t\tnew Set(),\n\t);\n\n\t/**\n\t * Check if an element is occluded by others\n\t */\n\tfunction checkElementOcclusion(element: HTMLElement): ElementVisibilityInfo {\n\t\t// Get element's bounding rectangle\n\t\tconst rect = element.getBoundingClientRect();\n\n\t\t// Check if element is in viewport at all\n\t\tconst isInViewport = (\n\t\t\trect.top < window.innerHeight &&\n\t\t\trect.bottom > 0 &&\n\t\t\trect.left < window.innerWidth &&\n\t\t\trect.right > 0\n\t\t);\n\n\t\tif (!isInViewport) {\n\t\t\treturn {\n\t\t\t\telement,\n\t\t\t\tisVisible: false,\n\t\t\t\tvisibilityRatio: 0,\n\t\t\t\toccludingElements: []\n\t\t\t};\n\t\t}\n\n\t\t// Find elements that might be occluding this one\n\t\tconst occludingElements: HTMLElement[] = [];\n\n\t\t// Get computed z-index of our element\n\t\tconst elementZIndex = parseInt(window.getComputedStyle(element).zIndex) || 0;\n\t\tconst elementPosition = window.getComputedStyle(element).position;\n\n\t\t// Get elements that intersect with our element's rectangle\n\t\tconst potentialOccluders = Array.from(document.elementsFromPoint(\n\t\t\trect.left + rect.width / 2,\n\t\t\trect.top + rect.height / 2\n\t\t)) as HTMLElement[];\n\n\t\t// Filter to find actual occluders (higher z-index or naturally above in DOM)\n\t\tfor (const potential of potentialOccluders) {\n\t\t\t// Skip the element itself\n\t\t\tif (potential === element || element.contains(potential) || potential.contains(element)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst potentialRect = potential.getBoundingClientRect();\n\n\t\t\t// Check for actual intersection\n\t\t\tconst isIntersecting = !(\n\t\t\t\trect.right < potentialRect.left ||\n\t\t\t\trect.left > potentialRect.right ||\n\t\t\t\trect.bottom < potentialRect.top ||\n\t\t\t\trect.top > potentialRect.bottom\n\t\t\t);\n\n\t\t\tif (isIntersecting) {\n\t\t\t\tconst potentialZIndex = parseInt(window.getComputedStyle(potential).zIndex) || 0;\n\t\t\t\tconst potentialPosition = window.getComputedStyle(potential).position;\n\n\t\t\t\t// Compare stacking contexts\n\t\t\t\tconst isOccluding = (\n\t\t\t\t\t// Higher z-index and positioned\n\t\t\t\t\t(potentialZIndex > elementZIndex &&\n\t\t\t\t\t\t(potentialPosition === 'absolute' || potentialPosition === 'relative' ||\n\t\t\t\t\t\t\tpotentialPosition === 'fixed' || potentialPosition === 'sticky')) ||\n\t\t\t\t\t// Or appears later in DOM when z-index is the same\n\t\t\t\t\t(potentialZIndex === elementZIndex &&\n\t\t\t\t\t\tpotential.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING)\n\t\t\t\t);\n\n\t\t\t\tif (isOccluding) {\n\t\t\t\t\toccludingElements.push(potential);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Calculate approximate visibility ratio\n\t\tlet visibleArea = rect.width * rect.height;\n\t\tlet totalArea = visibleArea;\n\n\t\tfor (const occluder of occludingElements) {\n\t\t\tconst occluderRect = occluder.getBoundingClientRect();\n\n\t\t\t// Calculate intersection area\n\t\t\tconst xOverlap = Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.min(rect.right, occluderRect.right) - Math.max(rect.left, occluderRect.left)\n\t\t\t);\n\n\t\t\tconst yOverlap = Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.min(rect.bottom, occluderRect.bottom) - Math.max(rect.top, occluderRect.top)\n\t\t\t);\n\n\t\t\tconst overlapArea = xOverlap * yOverlap;\n\t\t\tvisibleArea -= overlapArea;\n\t\t}\n\n\t\t// Ensure we don't go below 0\n\t\tvisibleArea = Math.max(0, visibleArea);\n\t\tconst visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0;\n\n\t\treturn {\n\t\t\telement,\n\t\t\tisVisible: visibilityRatio > 0.05, // Consider visible if at least 5% is showing\n\t\t\tvisibilityRatio,\n\t\t\toccludingElements\n\t\t};\n\t}\n\n\t/**\n\t * Setup visibility observer for an element\n\t */\n\tfunction observeElementVisibility(element: HTMLElement) {\n\t\t// Skip if we're already observing this element\n\t\tif (elementObserversRef.current.has(element)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Initialize visibility info\n\t\tconst initialVisibility = checkElementOcclusion(element);\n\t\telementVisibilityRef.current.set(element, initialVisibility);\n\n\t\t// Create intersection observer to track when element enters/exits viewport\n\t\tconst observer = new IntersectionObserver(\n\t\t\t(entries) => {\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tif (entry.target === element) {\n\t\t\t\t\t\t// When intersection changes, recalculate occlusion\n\t\t\t\t\t\tconst visibilityInfo = checkElementOcclusion(element);\n\t\t\t\t\t\telementVisibilityRef.current.set(element, visibilityInfo);\n\n\t\t\t\t\t\t// Update the highlight for this element\n\t\t\t\t\t\tupdateHighlightForElement(element);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ threshold: [0, 0.1, 0.5, 1.0] }\n\t\t);\n\n\t\tobserver.observe(element);\n\t\telementObserversRef.current.set(element, observer);\n\t}\n\n\t/**\n\t * Stop observing an element's visibility\n\t */\n\tfunction unobserveElementVisibility(element: HTMLElement) {\n\t\tconst observer = elementObserversRef.current.get(element);\n\t\tif (observer) {\n\t\t\tobserver.disconnect();\n\t\t\telementObserversRef.current.delete(element);\n\t\t\telementVisibilityRef.current.delete(element);\n\t\t}\n\t}\n\n\t/**\n\t * Helper: position the highlight box over an element with occlusion awareness\n\t */\n\tfunction positionHighlightBox(\n\t\thighlightEl: HTMLDivElement | null,\n\t\telement: HTMLElement | null,\n\t) {\n\t\tif (!highlightEl || !element || !containerRef.current) {\n\t\t\tif (highlightEl) highlightEl.style.display = \"none\";\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if element is visible at all\n\t\tconst visibilityInfo = elementVisibilityRef.current.get(element) || checkElementOcclusion(element);\n\t\tif (!visibilityInfo.isVisible) {\n\t\t\thighlightEl.style.display = \"none\";\n\t\t\treturn;\n\t\t}\n\n\t\tconst containerRect = containerRef.current.getBoundingClientRect();\n\t\tconst elemRect = element.getBoundingClientRect();\n\n\t\tconst offsetLeft =\n\t\t\telemRect.left - containerRect.left + containerRef.current.scrollLeft;\n\t\tconst offsetTop =\n\t\t\telemRect.top - containerRect.top + containerRef.current.scrollTop;\n\n\t\thighlightEl.style.display = \"block\";\n\t\thighlightEl.style.left = `${offsetLeft}px`;\n\t\thighlightEl.style.top = `${offsetTop}px`;\n\t\thighlightEl.style.width = `${elemRect.width}px`;\n\t\thighlightEl.style.height = `${elemRect.height}px`;\n\t\thighlightEl.style.boxSizing = \"border-box\";\n\n\t\t// Set opacity based on visibility ratio\n\t\tconst opacity = 0.1 * visibilityInfo.visibilityRatio;\n\n\t\thighlightEl.style.backgroundColor =\n\t\t\thighlightEl === activeHighlightRef.current\n\t\t\t\t? `rgba(76, 175, 80, ${opacity})`\n\t\t\t\t: `rgba(33, 150, 243, ${opacity})`;\n\n\t\t// Advanced: Use clip-path to exclude occluded areas\n\t\tif (visibilityInfo.occludingElements.length > 0 && false) { // Disabled for now as it's very complex\n\t\t\tlet clipPath = \"polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)\";\n\n\t\t\t// This is a simplified version - a full implementation would need to \n\t\t\t// generate complex polygons based on the shapes of occluding elements\n\t\t\thighlightEl.style.clipPath = clipPath;\n\t\t}\n\t}\n\n\t/**\n\t * Update the highlight for a specific element\n\t */\n\tfunction updateHighlightForElement(element: HTMLElement) {\n\t\t// Update active highlight if it's for this element\n\t\tif (activeElementRef.current === element && activeHighlightRef.current) {\n\t\t\tpositionHighlightBox(activeHighlightRef.current, element);\n\t\t}\n\n\t\t// Update hover highlight if it's for this element\n\t\tif (hoveredElementRef.current === element && hoverHighlightRef.current) {\n\t\t\tpositionHighlightBox(hoverHighlightRef.current, element);\n\t\t}\n\n\t\t// Update any selected highlights for this element\n\t\tselectedHighlightsRef.current.forEach(highlight => {\n\t\t\tif (highlight['targetElementRef'] === element) {\n\t\t\t\tpositionHighlightBox(highlight, element);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Create a highlight element\n\t */\n\tfunction createHighlight(element: HTMLElement) {\n\t\tconst highlight = document.createElement(\"div\");\n\t\thighlight.style.position = \"absolute\";\n\t\thighlight.style.pointerEvents = \"none\";\n\t\thighlight.style.border = `${highlightWidth}px solid ${highlightColor}`;\n\t\thighlight.style.backgroundColor = \"rgba(76, 175, 80, 0.1)\";\n\t\thighlight.style.zIndex = \"999999\";\n\t\thighlight.style.boxSizing = \"border-box\";\n\n\t\t// Store reference to the target element\n\t\thighlight.dataset.targetElement = element.outerHTML;\n\n\t\t// Store the element reference for resize observation\n\t\thighlight['targetElementRef'] = element;\n\n\t\t// Store the unique ID if available\n\t\tif (element.dataset.uniqueId) {\n\t\t\thighlight['targetElementUniqueId'] = element.dataset.uniqueId;\n\t\t}\n\n\t\tcontainerRef.current?.appendChild(highlight);\n\n\t\t// Start observing this element for visibility changes\n\t\tobserveElementVisibility(element);\n\n\t\tpositionHighlightBox(highlight, element);\n\t\treturn highlight;\n\t}\n\n\t// Unified function to update all highlight positions\n\tfunction handlePositionUpdates() {\n\t\t// Update visibility information for all tracked elements\n\t\tconst elementsToCheck = new Set<HTMLElement>();\n\n\t\t// Collect all elements that have highlights\n\t\tselectedHighlightsRef.current.forEach(highlight => {\n\t\t\tconst element = highlight['targetElementRef'];\n\t\t\tif (element) {\n\t\t\t\telementsToCheck.add(element);\n\t\t\t}\n\t\t});\n\n\t\tif (activeElementRef.current) {\n\t\t\telementsToCheck.add(activeElementRef.current);\n\t\t}\n\n\t\tif (hoveredElementRef.current) {\n\t\t\telementsToCheck.add(hoveredElementRef.current);\n\t\t}\n\n\t\t// Update visibility information for each element\n\t\telementsToCheck.forEach(element => {\n\t\t\tconst visibilityInfo = checkElementOcclusion(element);\n\t\t\telementVisibilityRef.current.set(element, visibilityInfo);\n\t\t});\n\n\t\t// Now update the highlights based on new visibility info\n\t\tselectedHighlightsRef.current.forEach((highlight) => {\n\t\t\tconst element = highlight['targetElementRef'];\n\t\t\tif (element) {\n\t\t\t\tpositionHighlightBox(highlight, element);\n\t\t\t}\n\t\t});\n\n\t\t// Update active and hover highlights\n\t\tif (activeElementRef.current && activeHighlightRef.current) {\n\t\t\tpositionHighlightBox(activeHighlightRef.current, activeElementRef.current);\n\t\t}\n\t\tif (hoveredElementRef.current && hoverHighlightRef.current) {\n\t\t\tpositionHighlightBox(hoverHighlightRef.current, hoveredElementRef.current);\n\t\t}\n\t}\n\n\tfunction clearHighlights() {\n\t\tconsole.log(\"Clearing all highlights\");\n\t\tconsole.log(\n\t\t\t\"Before clear - selected highlights:\",\n\t\t\tselectedHighlightsRef.current.length,\n\t\t);\n\n\t\t// Stop observing all elements\n\t\tselectedHighlightsRef.current.forEach(highlight => {\n\t\t\tconst element = highlight['targetElementRef'];\n\t\t\tif (element) {\n\t\t\t\tunobserveElementVisibility(element);\n\t\t\t}\n\t\t});\n\n\t\tif (hoveredElementRef.current) {\n\t\t\tunobserveElementVisibility(hoveredElementRef.current);\n\t\t}\n\n\t\tif (activeElementRef.current) {\n\t\t\tunobserveElementVisibility(activeElementRef.current);\n\t\t}\n\n\t\thoveredElementRef.current = null;\n\t\tactiveElementRef.current = null;\n\n\t\tif (activeHighlightRef.current) {\n\t\t\tactiveHighlightRef.current.style.display = \"none\";\n\t\t}\n\t\tif (hoverHighlightRef.current) {\n\t\t\thoverHighlightRef.current.style.display = \"none\";\n\t\t}\n\n\t\t// Disconnect resize observers before clearing\n\t\tselectedHighlightsRef.current.forEach((highlight) => {\n\t\t\tif (highlight['resizeObserver']) {\n\t\t\t\thighlight['resizeObserver'].disconnect();\n\t\t\t}\n\t\t\tif (highlight.parentNode) {\n\t\t\t\thighlight.parentNode.removeChild(highlight);\n\t\t\t}\n\t\t});\n\t\tselectedHighlightsRef.current = [];\n\n\t\tconsole.log(\n\t\t\t\"After clear - selected highlights:\",\n\t\t\tselectedHighlightsRef.current.length,\n\t\t);\n\t}\n\n\t// Add URL tracking effect\n\tuseEffect(() => {\n\t\t// Send initial URL\n\t\twindow.parent.postMessage(\n\t\t\t{\n\t\t\t\ttype: \"URL_CHANGE\",\n\t\t\t\turl: window.location.href,\n\t\t\t},\n\t\t\t\"*\",\n\t\t);\n\n\t\t// Track navigation events\n\t\tconst handleNavigation = () => {\n\t\t\twindow.parent.postMessage(\n\t\t\t\t{\n\t\t\t\t\ttype: \"URL_CHANGE\",\n\t\t\t\t\turl: window.location.href,\n\t\t\t\t},\n\t\t\t\t\"*\",\n\t\t\t);\n\t\t};\n\n\t\twindow.addEventListener(\"popstate\", handleNavigation);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"popstate\", handleNavigation);\n\t\t};\n\t}, []);\n\n\t// Add resize and scroll handling for highlights\n\tuseEffect(() => {\n\t\t// Handle window resize and scroll with the same function\n\t\tconst handleResize = handlePositionUpdates;\n\t\tconst handleScroll = handlePositionUpdates;\n\n\t\twindow.addEventListener('resize', handleResize);\n\t\twindow.addEventListener('scroll', handleScroll, true); // true for capture phase\n\n\t\t// Also listen to scroll events on the container element\n\t\tif (containerRef.current) {\n\t\t\tcontainerRef.current.addEventListener('scroll', handleScroll, true);\n\t\t}\n\n\t\t// For any potential scrollable parent elements - get all scrollable ancestors\n\t\tlet parent = containerRef.current?.parentElement;\n\t\twhile (parent) {\n\t\t\tconst overflowY = window.getComputedStyle(parent).overflowY;\n\t\t\tconst overflowX = window.getComputedStyle(parent).overflowX;\n\n\t\t\tif (overflowY === 'auto' || overflowY === 'scroll' ||\n\t\t\t\toverflowX === 'auto' || overflowX === 'scroll') {\n\t\t\t\tparent.addEventListener('scroll', handleScroll, true);\n\t\t\t}\n\t\t\tparent = parent.parentElement;\n\t\t}\n\n\t\t// Create ResizeObserver for tracking element size changes\n\t\tconst resizeObserver = new ResizeObserver((entries) => {\n\t\t\t// Just call our unified update function\n\t\t\thandlePositionUpdates();\n\t\t});\n\n\t\t// Observe all selected elements\n\t\tselectedHighlightsRef.current.forEach((highlight) => {\n\t\t\tconst element = highlight['targetElementRef'];\n\t\t\tif (element) {\n\t\t\t\tresizeObserver.observe(element);\n\t\t\t\t// Store observer on highlight for cleanup\n\t\t\t\thighlight['resizeObserver'] = resizeObserver;\n\t\t\t}\n\t\t});\n\n\t\t// Setup a mutation observer to detect DOM changes that might affect occlusion\n\t\tconst mutationObserver = new MutationObserver(() => {\n\t\t\t// When DOM changes, we should update visibility calculations\n\t\t\thandlePositionUpdates();\n\t\t});\n\n\t\t// Observe the entire document for changes to elements and attributes\n\t\tmutationObserver.observe(document.body, {\n\t\t\tchildList: true,\n\t\t\tsubtree: true,\n\t\t\tattributes: true,\n\t\t\tattributeFilter: ['style', 'class']\n\t\t});\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener('resize', handleResize);\n\t\t\twindow.removeEventListener('scroll', handleScroll, true);\n\n\t\t\t// Clean up container scroll listener\n\t\t\tif (containerRef.current) {\n\t\t\t\tcontainerRef.current.removeEventListener('scroll', handleScroll, true);\n\t\t\t}\n\n\t\t\t// Clean up all parent scroll listeners\n\t\t\tlet parent = containerRef.current?.parentElement;\n\t\t\twhile (parent) {\n\t\t\t\tconst overflowY = window.getComputedStyle(parent).overflowY;\n\t\t\t\tconst overflowX = window.getComputedStyle(parent).overflowX;\n\n\t\t\t\tif (overflowY === 'auto' || overflowY === 'scroll' ||\n\t\t\t\t\toverflowX === 'auto' || overflowX === 'scroll') {\n\t\t\t\t\tparent.removeEventListener('scroll', handleScroll, true);\n\t\t\t\t}\n\t\t\t\tparent = parent.parentElement;\n\t\t\t}\n\n\t\t\tresizeObserver.disconnect();\n\t\t\tmutationObserver.disconnect();\n\n\t\t\t// Clean up all element observers\n\t\t\telementObserversRef.current.forEach(observer => {\n\t\t\t\tobserver.disconnect();\n\t\t\t});\n\t\t\telementObserversRef.current.clear();\n\t\t\telementVisibilityRef.current.clear();\n\t\t};\n\t}, [selectedHighlightsRef.current, activeElementRef.current, hoveredElementRef.current]);\n\n\t/**\n\t * Inject <style> to disable pointer-events on all elements,\n\t * then re-enable them only for <img>\n\t */\n\tuseEffect(() => {\n\t\tlet styleEl: HTMLStyleElement | null = null;\n\n\t\tif (isImageInspectorActive) {\n\t\t\tstyleEl = document.createElement(\"style\");\n\t\t\tstyleEl.id = \"only-images-clickable\";\n\t\t\tstyleEl.textContent = `\n /* Disable pointer events on everything... */\n * {\n pointer-events: none !important;\n }\n /* ...re-enable for <img> and <video> */\n img, video {\n pointer-events: auto !important;\n }\n `;\n\t\t\tdocument.head.appendChild(styleEl);\n\t\t}\n\n\t\treturn () => {\n\t\t\t// Remove style on cleanup or when turning image-inspector mode off\n\t\t\tif (styleEl && styleEl.parentNode) {\n\t\t\t\tstyleEl.parentNode.removeChild(styleEl);\n\t\t\t}\n\t\t};\n\t}, [isImageInspectorActive]);\n\n\t/**\n\t * Get metadata about an element\n\t */\n\tfunction getElementMetadata(element: HTMLElement): ElementMetadata {\n\t\tconst uniqueId = element.dataset.uniqueId || \"\";\n\t\tlet duplicateCount = 1;\n\n\t\tif (uniqueId) {\n\t\t\t// Count elements with the same data-unique-id\n\t\t\tduplicateCount = document.querySelectorAll(`[data-unique-id=\"${uniqueId}\"]`).length;\n\t\t}\n\n\t\t// Check if element has the data-dynamic-text attribute\n\t\tconst hasDynamicText = element.dataset.dynamicText === \"true\";\n\n\t\treturn {\n\t\t\ttagName: element.tagName.toLowerCase(),\n\t\t\tclassNames: typeof element.className === 'string'\n\t\t\t\t? element.className\n\t\t\t\t: (element.className && element.className.baseVal) || '',\n\t\t\telementId: element.id || null,\n\t\t\ttextContent: element.textContent,\n\t\t\tboundingBox: element.getBoundingClientRect(),\n\t\t\tattributes: Array.from(element.attributes).map((attr) => ({\n\t\t\t\tname: attr.name,\n\t\t\t\tvalue: attr.value,\n\t\t\t})),\n\t\t\tuniqueId: uniqueId,\n\t\t\tduplicateCount: duplicateCount,\n\t\t\thasDynamicText: hasDynamicText\n\t\t};\n\t}\n\n\t/**\n\t * Handle messages from the parent window\n\t */\n\tuseEffect(() => {\n\t\tconst handleMessage = (event: InspectorEvent) => {\n\t\t\tconst { type, enabled, targetId } = event.data;\n\t\t\tconsole.log(\"BROOO\", type, targetId);\n\n\t\t\tfunction isTextOnly(el) {\n\t\t\t\treturn el.childNodes.length === 1 && el.childNodes[0].nodeType === Node.TEXT_NODE;\n\t\t\t}\n\n\t\t\tif (type === \"GET_ELEMENT_STYLES\") {\n\t\t\t\tconst targetElements = document.querySelectorAll(`[data-unique-id=\"${targetId}\"]`);\n\n\t\t\t\tconsole.log(\"GET_ELEMENT_STYLES\", targetId);\n\t\t\t\tif (targetElements.length > 0) {\n\t\t\t\t\t// Get the first element for styles\n\t\t\t\t\tconst targetElement = targetElements[0];\n\t\t\t\t\tconst computedStyle = window.getComputedStyle(targetElement);\n\n\t\t\t\t\t// Check if element has direct text content\n\t\t\t\t\tconst hasDirectText = Array.from(targetElement.childNodes).some(node =>\n\t\t\t\t\t\tnode.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0\n\t\t\t\t\t);\n\n\t\t\t\t\t// Alternative approach using the isTextOnly function\n\t\t\t\t\t// The isTextOnly function checks if element has exactly one child \n\t\t\t\t\t// and that child is a text node\n\t\t\t\t\tconst isElementTextOnly = isTextOnly(targetElement);\n\n\t\t\t\t\t// Check if element has data-dynamic-text attribute\n\t\t\t\t\tconst hasDynamicText = targetElement.dataset.dynamicText === \"true\";\n\n\t\t\t\t\tconsole.log(\"HAS DYNAMIC TEXT\", hasDynamicText)\n\n\t\t\t\t\t// An element is dynamically rendered if:\n\t\t\t\t\t// 1. There are multiple elements with the same unique ID, OR\n\t\t\t\t\t// 2. It has the data-dynamic-text attribute set to \"true\"\n\t\t\t\t\tconst isDynamicallyRendered = targetElements.length > 1 || hasDynamicText;\n\n\t\t\t\t\t// Extract the requested style properties\n\t\t\t\t\tconst computedStyles = {\n\t\t\t\t\t\t// Padding\n\t\t\t\t\t\tpaddingTop: computedStyle.paddingTop,\n\t\t\t\t\t\tpaddingRight: computedStyle.paddingRight,\n\t\t\t\t\t\tpaddingBottom: computedStyle.paddingBottom,\n\t\t\t\t\t\tpaddingLeft: computedStyle.paddingLeft,\n\n\t\t\t\t\t\t// Margin\n\t\t\t\t\t\tmargin: computedStyle.margin,\n\t\t\t\t\t\tmarginTop: computedStyle.marginTop,\n\t\t\t\t\t\tmarginRight: computedStyle.marginRight,\n\t\t\t\t\t\tmarginBottom: computedStyle.marginBottom,\n\t\t\t\t\t\tmarginLeft: computedStyle.marginLeft,\n\n\t\t\t\t\t\t// Dimensions\n\t\t\t\t\t\twidth: computedStyle.width,\n\t\t\t\t\t\theight: computedStyle.height,\n\n\t\t\t\t\t\t// Colors and appearance\n\t\t\t\t\t\tbackgroundColor: computedStyle.backgroundColor,\n\t\t\t\t\t\tcolor: computedStyle.color,\n\t\t\t\t\t\tborderRadius: computedStyle.borderRadius,\n\n\t\t\t\t\t\t// Border properties\n\t\t\t\t\t\tborderWidth: computedStyle.borderWidth,\n\t\t\t\t\t\tborderStyle: computedStyle.borderStyle,\n\t\t\t\t\t\tborderColor: computedStyle.borderColor,\n\n\t\t\t\t\t\t// Text properties\n\t\t\t\t\t\tfontSize: computedStyle.fontSize,\n\t\t\t\t\t\tfontWeight: computedStyle.fontWeight\n\t\t\t\t\t};\n\n\t\t\t\t\t// Get text content\n\t\t\t\t\tconst textContent = targetElement.textContent;\n\n\t\t\t\t\tconsole.log(\"GET_ELEMENT_STYLES\", {\n\t\t\t\t\t\tcomputedStyles: computedStyles,\n\t\t\t\t\t\ttailwindClasses: targetElement.className.split(\" \"),\n\t\t\t\t\t\tisDynamicallyRendered: isDynamicallyRendered,\n\t\t\t\t\t\tduplicateCount: targetElements.length,\n\t\t\t\t\t\thasDirectText: hasDirectText,\n\t\t\t\t\t\tisTextOnly: isElementTextOnly,\n\t\t\t\t\t\thasDynamicText: hasDynamicText\n\t\t\t\t\t});\n\n\t\t\t\t\twindow.parent.postMessage({\n\t\t\t\t\t\ttype: 'ELEMENT_STYLES_RETRIEVED',\n\t\t\t\t\t\ttargetId: targetId,\n\t\t\t\t\t\tmetadata: {\n\t\t\t\t\t\t\tcomputedStyles: computedStyles,\n\t\t\t\t\t\t\ttailwindClasses: targetElement.className.split(\" \"),\n\t\t\t\t\t\t\tisDynamicallyRendered: isDynamicallyRendered,\n\t\t\t\t\t\t\tduplicateCount: targetElements.length,\n\t\t\t\t\t\t\thasDirectText: hasDirectText,\n\t\t\t\t\t\t\tisTextOnly: isElementTextOnly,\n\t\t\t\t\t\t\thasDynamicText: hasDynamicText\n\t\t\t\t\t\t}\n\t\t\t\t\t}, '*');\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (type === \"GET_ELEMENT_TEXT\") {\n\t\t\t\tconst targetElement = document.querySelector(`[data-unique-id=\"${targetId}\"]`);\n\n\t\t\t\tconsole.log(\"GET_ELEMENT_TEXT\", targetId);\n\t\t\t\tconsole.log(\"GET_ELEMENT_TEXT\", {\n\t\t\t\t\ttextContent: targetElement.textContent ?? null,\n\t\t\t\t\ttargetId: targetId\n\t\t\t\t});\n\n\t\t\t\twindow.parent.postMessage({\n\t\t\t\t\ttype: 'ELEMENT_TEXT_RETRIEVED',\n\t\t\t\t\ttargetId: targetId,\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\ttextContent: targetElement.textContent ?? null,\n\t\t\t\t\t\ttargetId: targetId\n\t\t\t\t\t}\n\t\t\t\t}, '*');\n\t\t\t}\n\n\t\t\tif (type === \"UPDATE_ELEMENT_STYLES\") {\n\t\t\t\t// Find ALL target elements with the matching data-unique-id\n\t\t\t\tconst targetElements = document.querySelectorAll(`[data-unique-id=\"${targetId}\"]`);\n\n\t\t\t\tconsole.log(\"UPDATE_ELEMENT_STYLES\", targetId, event.data.styles);\n\t\t\t\tif (targetElements.length > 0) {\n\t\t\t\t\tconst keys = Object.keys(event.data.styles);\n\n\t\t\t\t\t// Loop through all matching elements and apply styles to each one\n\t\t\t\t\ttargetElements.forEach(element => {\n\t\t\t\t\t\tfor (const key of keys) {\n\t\t\t\t\t\t\telement.style[key] = event.data.styles[key];\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\t// After updating styles, update all highlight positions\n\t\t\t\t\thandlePositionUpdates();\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(`No elements with data-unique-id=\"${targetId}\" found`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (type === \"UPDATE_ELEMENT_TEXT\") {\n\t\t\t\t// Find ALL target elements with the matching data-unique-id\n\t\t\t\tconst targetElements = document.querySelectorAll(`[data-unique-id=\"${targetId}\"]`);\n\n\t\t\t\tconsole.log(\"UPDATE_ELEMENT_TEXT\", targetId, event.data.text);\n\t\t\t\tif (targetElements.length > 0) {\n\t\t\t\t\t// Loop through all matching elements and update their text content\n\t\t\t\t\ttargetElements.forEach(element => {\n\t\t\t\t\t\telement.textContent = event.data.text;\n\t\t\t\t\t});\n\n\t\t\t\t\t// After updating text, update all highlight positions\n\t\t\t\t\thandlePositionUpdates();\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(`No elements with data-unique-id=\"${targetId}\" found for text update`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (type === \"DELETE_ELEMENT\") {\n\t\t\t\tconst targetElements = document.querySelectorAll(`[data-unique-id=\"${targetId}\"]`);\n\t\t\t\tconsole.log(\"DELETE_ELEMENT\", targetId);\n\t\t\t\tif (targetElements.length > 0) {\n\t\t\t\t\ttargetElements.forEach(element => {\n\t\t\t\t\t\telement.parentNode.removeChild(element);\n\t\t\t\t\t});\n\n\t\t\t\t\t// Clear highlights after deletion\n\t\t\t\t\tclearHighlights();\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(`No elements with data-unique-id=\"${targetId}\" found for deletion`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (type === \"TOGGLE_EDITOR\") {\n\t\t\t\tsetIsEditorActive(!!enabled);\n\t\t\t\tif (!enabled) clearHighlights();\n\t\t\t\tif (containerRef.current) {\n\t\t\t\t\tcontainerRef.current.style.cursor = enabled ? \"crosshair\" : \"\";\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (type === \"TOGGLE_INSPECTOR\") {\n\t\t\t\tsetIsInspectorActive(!!enabled);\n\t\t\t\tif (enabled) setIsImageInspectorActive(false);\n\t\t\t\tif (!enabled) clearHighlights();\n\t\t\t\tif (containerRef.current) {\n\t\t\t\t\tcontainerRef.current.style.cursor = enabled ? \"crosshair\" : \"\";\n\t\t\t\t}\n\t\t\t} else if (type === \"TOGGLE_IMAGE_INSPECTOR\") {\n\t\t\t\tsetIsImageInspectorActive(!!enabled);\n\t\t\t\tif (enabled) setIsInspectorActive(false);\n\t\t\t\tif (!enabled) clearHighlights();\n\t\t\t\tif (containerRef.current) {\n\t\t\t\t\tcontainerRef.current.style.cursor = enabled ? \"crosshair\" : \"\";\n\t\t\t\t}\n\t\t\t} else if (type === \"UPDATE_IMAGE_URL\") {\n\t\t\t\tconst { url } = event.data;\n\t\t\t\tif (activeElementRef.current && url) {\n\t\t\t\t\tconst activeEl = activeElementRef.current;\n\t\t\t\t\tconst currentTagName = activeEl.tagName.toLowerCase();\n\n\t\t\t\t\t// Determine if the new URL is a video or image\n\t\t\t\t\tconst isVideoUrl = url.match(/\\.(mp4|webm|ogg)$/i);\n\t\t\t\t\tconst shouldBeVideo = isVideoUrl !== null;\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (shouldBeVideo && currentTagName === \"img\") {\n\t\t\t\t\t\t\t// Convert img to video\n\t\t\t\t\t\t\tconst videoEl = document.createElement(\"video\");\n\t\t\t\t\t\t\tvideoEl.autoplay = true;\n\t\t\t\t\t\t\tvideoEl.loop = true;\n\t\t\t\t\t\t\tvideoEl.muted = true;\n\t\t\t\t\t\t\tvideoEl.playsInline = true;\n\n\t\t\t\t\t\t\t// Copy over relevant attributes and styles\n\t\t\t\t\t\t\tArray.from(activeEl.attributes).forEach((attr) => {\n\t\t\t\t\t\t\t\tif (attr.name !== \"src\" && attr.name !== \"alt\") {\n\t\t\t\t\t\t\t\t\tvideoEl.setAttribute(attr.name, attr.value);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Copy computed styles\n\t\t\t\t\t\t\tconst styles = window.getComputedStyle(activeEl);\n\t\t\t\t\t\t\tArray.from(styles).forEach((key) => {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tvideoEl.style[key as any] = styles.getPropertyValue(key);\n\t\t\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\t\t\t// Ignore invalid style properties\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tvideoEl.src = url;\n\t\t\t\t\t\t\tif (activeEl.parentNode) {\n\t\t\t\t\t\t\t\tactiveEl.parentNode.replaceChild(videoEl, activeEl);\n\t\t\t\t\t\t\t\tactiveElementRef.current = videoEl;\n\n\t\t\t\t\t\t\t\t// Update highlights\n\t\t\t\t\t\t\t\tpositionHighlightBox(activeHighlightRef.current, videoEl);\n\t\t\t\t\t\t\t\tselectedHighlightsRef.current.forEach((highlight) => {\n\t\t\t\t\t\t\t\t\tif (highlight.dataset.targetElement === activeEl.outerHTML) {\n\t\t\t\t\t\t\t\t\t\tpositionHighlightBox(highlight, videoEl);\n\t\t\t\t\t\t\t\t\t\thighlight.dataset.targetElement = videoEl.outerHTML;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (!shouldBeVideo && currentTagName === \"video\") {\n\t\t\t\t\t\t\t// Convert video to img\n\t\t\t\t\t\t\tconst imgEl = document.createElement(\"img\");\n\n\t\t\t\t\t\t\t// Copy over relevant attributes and styles\n\t\t\t\t\t\t\tArray.from(activeEl.attributes).forEach((attr) => {\n\t\t\t\t\t\t\t\tif (attr.name !== \"src\" && !attr.name.startsWith(\"autoplay\")) {\n\t\t\t\t\t\t\t\t\timgEl.setAttribute(attr.name, attr.value);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Copy computed styles\n\t\t\t\t\t\t\tconst styles = window.getComputedStyle(activeEl);\n\t\t\t\t\t\t\tArray.from(styles).forEach((key) => {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\timgEl.style[key as any] = styles.getPropertyValue(key);\n\t\t\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\t\t\t// Ignore invalid style properties\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\timgEl.src = url;\n\t\t\t\t\t\t\tif (activeEl.parentNode) {\n\t\t\t\t\t\t\t\tactiveEl.parentNode.replaceChild(imgEl, activeEl);\n\t\t\t\t\t\t\t\tactiveElementRef.current = imgEl;\n\n\t\t\t\t\t\t\t\t// Update highlights\n\t\t\t\t\t\t\t\tpositionHighlightBox(activeHighlightRef.current, imgEl);\n\t\t\t\t\t\t\t\tselectedHighlightsRef.current.forEach((highlight) => {\n\t\t\t\t\t\t\t\t\tif (highlight.dataset.targetElement === activeEl.outerHTML) {\n\t\t\t\t\t\t\t\t\t\tpositionHighlightBox(highlight, imgEl);\n\t\t\t\t\t\t\t\t\t\thighlight.dataset.targetElement = imgEl.outerHTML;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Just update the source if no conversion needed\n\t\t\t\t\t\t\tif (currentTagName === \"img\") {\n\t\t\t\t\t\t\t\t(activeEl as HTMLImageElement).src = url;\n\t\t\t\t\t\t\t} else if (currentTagName === \"video\") {\n\t\t\t\t\t\t\t\t(activeEl as HTMLVideoElement).src = url;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error(\"Error during element conversion:\", error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"message\", handleMessage as EventListener);\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"message\", handleMessage as EventListener);\n\t\t};\n\t}, []);\n\n\t/**\n\t * Attach the inspector event listeners\n\t */\n\tuseEffect(() => {\n\t\tconst container = containerRef.current;\n\t\tif (!container) return;\n\n\t\tfunction preventDefault(e: Event) {\n\t\t\tif (isInspectorActive || isImageInspectorActive || isEditorActive) {\n\t\t\t\te.preventDefault();\n\t\t\t}\n\t\t}\n\n\t\tconst handleMouseMove = (e: MouseEvent) => {\n\t\t\tif (!isInspectorActive && !isImageInspectorActive && !isEditorActive) return;\n\t\t\tconst target = e.target as HTMLElement;\n\n\t\t\tif (\n\t\t\t\tisImageInspectorActive &&\n\t\t\t\ttarget.tagName.toLowerCase() !== \"img\" &&\n\t\t\t\ttarget.tagName.toLowerCase() !== \"video\"\n\t\t\t) {\n\t\t\t\t// If in image-only mode, ignore hovers on non-img/video elements\n\t\t\t\thoveredElementRef.current = null;\n\t\t\t\tpositionHighlightBox(hoverHighlightRef.current, null);\n\t\t\t\tsetHoveredMetadata(null);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Hover changed\n\t\t\tif (hoveredElementRef.current !== target) {\n\t\t\t\t// Start observing the new element\n\t\t\t\tif (target && target !== hoveredElementRef.current) {\n\t\t\t\t\tobserveElementVisibility(target);\n\t\t\t\t}\n\n\t\t\t\thoveredElementRef.current = target;\n\t\t\t\tpositionHighlightBox(hoverHighlightRef.current, target);\n\t\t\t\tsetTooltipPosition({ x: e.pageX + 10, y: e.pageY + 10 });\n\t\t\t\t// For optional tooltip\n\t\t\t\tconst id = target.id ? `#${target.id}` : \"\";\n\t\t\t\tconst classes =\n\t\t\t\t\ttypeof target.className === \"string\" && target.className.trim()\n\t\t\t\t\t\t? `.${target.className.trim().split(\" \").filter(Boolean).join(\".\")}`\n\t\t\t\t\t\t: \"\";\n\t\t\t\tsetHoveredMetadata(`<${target.tagName.toLowerCase()}>${id}${classes}`);\n\t\t\t}\n\t\t};\n\n\t\tconst handleMouseLeave = () => {\n\t\t\thoveredElementRef.current = null;\n\t\t\tpositionHighlightBox(hoverHighlightRef.current, null);\n\t\t\tsetHoveredMetadata(null);\n\t\t};\n\n\t\tconst handleClick = (e: MouseEvent) => {\n\t\t\t// First check if any inspector mode is active\n\t\t\tif (!isInspectorActive && !isImageInspectorActive && !isEditorActive) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst targetElement = e.target as HTMLElement;\n\t\t\te.preventDefault();\n\t\t\te.stopPropagation();\n\n\t\t\t// Handle image inspector mode\n\t\t\tif (isImageInspectorActive) {\n\t\t\t\tconst tagName = targetElement.tagName.toLowerCase();\n\t\t\t\tif (tagName !== \"img\" && tagName !== \"video\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Add unique identifier if not present\n\t\t\t\tif (!targetElement.dataset.imageInspectorId) {\n\t\t\t\t\tconst uniqueId = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n\t\t\t\t\ttargetElement.dataset.imageInspectorId = uniqueId;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// In editor mode, handle differently to select all elements with the same data-unique-id\n\t\t\tif (isEditorActive) {\n\t\t\t\t// Get the clicked element's data-unique-id\n\t\t\t\tconst uniqueId = targetElement.dataset.uniqueId;\n\n\t\t\t\t// Check if this element or one with the same ID is already selected\n\t\t\t\tconst isAlreadySelected = uniqueId && selectedHighlightsRef.current.some(\n\t\t\t\t\thighlight => highlight['targetElementUniqueId'] === uniqueId\n\t\t\t\t);\n\n\t\t\t\t// Clear existing selection\n\t\t\t\tclearHighlights();\n\n\t\t\t\tif (isAlreadySelected) {\n\t\t\t\t\t// If already selected, just clear and send metadata to keep UI updated\n\t\t\t\t\tconst msg: InspectedElementMessage = {\n\t\t\t\t\t\ttype: \"ELEMENT_INSPECTED\",\n\t\t\t\t\t\tmetadata: getElementMetadata(targetElement),\n\t\t\t\t\t\tisShiftPressed: e.shiftKey,\n\t\t\t\t\t};\n\t\t\t\t\twindow.parent.postMessage(msg, \"*\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tselectedHighlightsRef.current = [];\n\n\t\t\t\t// Find all elements with the same data-unique-id\n\t\t\t\tlet elementsToHighlight: HTMLElement[] = [targetElement];\n\n\t\t\t\tif (uniqueId) {\n\t\t\t\t\t// Query all elements with the same data-unique-id\n\t\t\t\t\tconst matchingElements = document.querySelectorAll(`[data-unique-id=\"${uniqueId}\"]`);\n\t\t\t\t\telementsToHighlight = Array.from(matchingElements) as HTMLElement[];\n\t\t\t\t}\n\n\t\t\t\t// Create highlights for all matching elements\n\t\t\t\tconst highlights = elementsToHighlight.map(element => {\n\t\t\t\t\t// Start observing this element\n\t\t\t\t\tobserveElementVisibility(element);\n\n\t\t\t\t\tconst highlight = createHighlight(element);\n\t\t\t\t\thighlight['targetElementUniqueId'] = uniqueId;\n\t\t\t\t\treturn highlight;\n\t\t\t\t});\n\n\t\t\t\tselectedHighlightsRef.current = highlights;\n\n\t\t\t\t// Set as active element (the one actually clicked)\n\t\t\t\tactiveElementRef.current = targetElement;\n\n\t\t\t\t// Send message with metadata including count of duplicates\n\t\t\t\tconst metadata = getElementMetadata(targetElement);\n\n\t\t\t\tconst msg: InspectedElementMessage = {\n\t\t\t\t\ttype: \"ELEMENT_INSPECTED\",\n\t\t\t\t\tmetadata: metadata, // Already includes duplicateCount from getElementMetadata\n\t\t\t\t\tisShiftPressed: false, // Always false in editor mode\n\t\t\t\t};\n\t\t\t\twindow.parent.postMessage(msg, \"*\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// For non-editor modes:\n\t\t\t// Check if element is already selected\n\t\t\tconst isAlreadySelected = selectedHighlightsRef.current.some(\n\t\t\t\t(highlight) => {\n\t\t\t\t\tconst targetRect = targetElement.getBoundingClientRect();\n\t\t\t\t\tconst highlightRect = highlight.getBoundingClientRect();\n\n\t\t\t\t\t// More lenient comparison focusing on height and vertical position\n\t\t\t\t\tconst heightMatch =\n\t\t\t\t\t\tMath.abs(highlightRect.height - targetRect.height) < 1;\n\t\t\t\t\tconst widthMatch =\n\t\t\t\t\t\tMath.abs(highlightRect.width - targetRect.width) < 1;\n\t\t\t\t\tconst topMatch = Math.abs(highlightRect.top - targetRect.top) < 1;\n\n\t\t\t\t\t// Check if the elements overlap significantly\n\t\t\t\t\tconst horizontalOverlap = !(\n\t\t\t\t\t\thighlightRect.right < targetRect.left ||\n\t\t\t\t\t\thighlightRect.left > targetRect.right\n\t\t\t\t\t);\n\n\t\t\t\t\tconst isMatch =\n\t\t\t\t\t\theightMatch && widthMatch && topMatch && horizontalOverlap;\n\t\t\t\t\treturn isMatch;\n\t\t\t\t},\n\t\t\t);\n\n\t\t\t// Clear existing selection if clicking same element in single select mode\n\t\t\tif (!e.shiftKey && isAlreadySelected) {\n\t\t\t\tclearHighlights();\n\t\t\t\tactiveElementRef.current = null;\n\n\t\t\t\t// Only send null metadata for normal inspector mode\n\t\t\t\tif (!isImageInspectorActive) {\n\t\t\t\t\tconst msg: InspectedElementMessage = {\n\t\t\t\t\t\ttype: \"ELEMENT_INSPECTED\",\n\t\t\t\t\t\tmetadata: null,\n\t\t\t\t\t\tisShiftPressed: false,\n\t\t\t\t\t};\n\t\t\t\t\twindow.parent.postMessage(msg, \"*\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (e.shiftKey) {\n\t\t\t\tif (isAlreadySelected) {\n\t\t\t\t\t// Remove the highlight for this element using the same comparison logic\n\t\t\t\t\tselectedHighlightsRef.current = selectedHighlightsRef.current.filter(\n\t\t\t\t\t\t(highlight) => {\n\t\t\t\t\t\t\tconst highlightRect = highlight.getBoundingClientRect();\n\t\t\t\t\t\t\tconst targetRect = targetElement.getBoundingClientRect();\n\n\t\t\t\t\t\t\t// Use the same comparison logic as in isAlreadySelected\n\t\t\t\t\t\t\tconst heightMatch =\n\t\t\t\t\t\t\t\tMath.abs(highlightRect.height - targetRect.height) < 1;\n\t\t\t\t\t\t\tconst widthMatch =\n\t\t\t\t\t\t\t\tMath.abs(highlightRect.width - targetRect.width) < 1;\n\t\t\t\t\t\t\tconst topMatch = Math.abs(highlightRect.top - targetRect.top) < 1;\n\t\t\t\t\t\t\tconst horizontalOverlap = !(\n\t\t\t\t\t\t\t\thighlightRect.right < targetRect.left ||\n\t\t\t\t\t\t\t\thighlightRect.left > targetRect.right\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t\tconst shouldRemove =\n\t\t\t\t\t\t\t\theightMatch && widthMatch && topMatch && horizontalOverlap;\n\n\t\t\t\t\t\t\tif (shouldRemove) {\n\t\t\t\t\t\t\t\tconst element = highlight['targetElementRef'];\n\t\t\t\t\t\t\t\tif (element) {\n\t\t\t\t\t\t\t\t\tunobserveElementVisibility(element);\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (highlight['resizeObserver']) {\n\t\t\t\t\t\t\t\t\thighlight['resizeObserver'].disconnect();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (highlight.parentNode) {\n\t\t\t\t\t\t\t\t\thighlight.parentNode.removeChild(highlight);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn !shouldRemove;\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tobserveElementVisibility(targetElement);\n\t\t\t\t\tconst highlight = createHighlight(targetElement);\n\t\t\t\t\tselectedHighlightsRef.current.push(highlight);\n\n\t\t\t\t\t// Create a new ResizeObserver for this element\n\t\t\t\t\tconst resizeObserver = new ResizeObserver(() => {\n\t\t\t\t\t\tif (highlight['targetElementRef']) {\n\t\t\t\t\t\t\tpositionHighlightBox(highlight, highlight['targetElementRef']);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tresizeObserver.observe(targetElement);\n\t\t\t\t\thighlight['resizeObserver'] = resizeObserver;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Single selection mode\n\t\t\t\tselectedHighlightsRef.current.forEach((highlight) => {\n\t\t\t\t\tconst element = highlight['targetElementRef'];\n\t\t\t\t\tif (element) {\n\t\t\t\t\t\tunobserveElementVisibility(element);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (highlight['resizeObserver']) {\n\t\t\t\t\t\thighlight['resizeObserver'].disconnect();\n\t\t\t\t\t}\n\t\t\t\t\tif (highlight.parentNode) {\n\t\t\t\t\t\thighlight.parentNode.removeChild(highlight);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tselectedHighlightsRef.current = [];\n\n\t\t\t\t// Start observing the new element\n\t\t\t\tobserveElementVisibility(targetElement);\n\n\t\t\t\t// Create new highlight\n\t\t\t\tconst highlight = createHighlight(targetElement);\n\t\t\t\tselectedHighlightsRef.current = [highlight];\n\n\t\t\t\t// Create a new ResizeObserver for this element\n\t\t\t\tconst resizeObserver = new ResizeObserver(() => {\n\t\t\t\t\tif (highlight['targetElementRef']) {\n\t\t\t\t\t\tpositionHighlightBox(highlight, highlight['targetElementRef']);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tresizeObserver.observe(targetElement);\n\t\t\t\thighlight['resizeObserver'] = resizeObserver;\n\t\t\t}\n\n\t\t\tactiveElementRef.current = targetElement;\n\n\t\t\tconst msg: InspectedElementMessage = {\n\t\t\t\ttype: \"ELEMENT_INSPECTED\",\n\t\t\t\tmetadata: getElementMetadata(targetElement),\n\t\t\t\tisShiftPressed: e.shiftKey,\n\t\t\t};\n\t\t\twindow.parent.postMessage(msg, \"*\");\n\t\t};\n\n\t\t// Add the events on container\n\t\tcontainer.addEventListener(\"click\", preventDefault, true);\n\t\tcontainer.addEventListener(\"mousedown\", preventDefault, true);\n\t\tcontainer.addEventListener(\"mouseup\", preventDefault, true);\n\t\tcontainer.addEventListener(\"submit\", preventDefault, true);\n\n\t\tcontainer.addEventListener(\"mousemove\", handleMouseMove as EventListener);\n\t\tcontainer.addEventListener(\"mouseleave\", handleMouseLeave);\n\n\t\t// The actual click is listened globally (capture) so we can stop it\n\t\tdocument.addEventListener(\"click\", handleClick, true);\n\n\t\treturn () => {\n\t\t\tcontainer.removeEventListener(\"click\", preventDefault, true);\n\t\t\tcontainer.removeEventListener(\"mousedown\", preventDefault, true);\n\t\t\tcontainer.removeEventListener(\"mouseup\", preventDefault, true);\n\t\t\tcontainer.removeEventListener(\"submit\", preventDefault, true);\n\n\t\t\tcontainer.removeEventListener(\n\t\t\t\t\"mousemove\",\n\t\t\t\thandleMouseMove as EventListener,\n\t\t\t);\n\t\t\tcontainer.removeEventListener(\"mouseleave\", handleMouseLeave);\n\t\t\tdocument.removeEventListener(\"click\", handleClick, true);\n\t\t};\n\t}, [isInspectorActive, isImageInspectorActive, isEditorActive]);\n\n\treturn (\n\t\t<div\n\t\t\tref={containerRef}\n\t\t\tclassName={`relative h-full w-full ${isInspectorActive || isImageInspectorActive || isEditorActive ? \"cursor-crosshair\" : \"\"}`}\n\t\t>\n\t\t\t{/* Hover highlight */}\n\t\t\t<div\n\t\t\t\tref={hoverHighlightRef}\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tdisplay: \"none\",\n\t\t\t\t\tpointerEvents: \"none\",\n\t\t\t\t\tborder: `${highlightWidth}px dashed #2196F3`,\n\t\t\t\t\tbackgroundColor: \"rgba(33, 150, 243, 0.1)\", // Light blue with 10% opacity\n\t\t\t\t\tzIndex: 999999,\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t{/* Selected highlight */}\n\t\t\t<div\n\t\t\t\tref={activeHighlightRef}\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tdisplay: \"none\",\n\t\t\t\t\tpointerEvents: \"none\",\n\t\t\t\t\tborder: `${highlightWidth}px solid ${highlightColor}`,\n\t\t\t\t\tbackgroundColor: \"rgba(76, 175, 80, 0.1)\", // Light green with 10% opacity\n\t\t\t\t\tzIndex: 999999,\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t{children}\n\t\t</div>\n\t);\n};\n\nexport { DOMInspector };","'use client';\n\nimport React, { useEffect, useRef, type JSX } from 'react';\nimport { initMiniSentry } from './minisentry';\nimport type { PropsWithChildren } from './react-compat';\nimport Branding from './branding/BrandingTag';\nimport AnalyticsTracker from './analytics/AnalyticsTracker';\n\nexport interface DevtoolsProviderProps {\n\tinspector?: {\n\t\thighlightColor?: string;\n\t\thighlightWidth?: number;\n\t\tcontainerSelector?: string;\n\t};\n\tsentry?: Parameters<typeof initMiniSentry>[0];\n\thideBranding?: boolean;\n}\n\nexport function DevtoolsProvider(\n\t{ children, inspector, sentry, hideBranding = false }: PropsWithChildren<DevtoolsProviderProps>,\n): JSX.Element {\n\t/* install Mini-Sentry exactly once */\n\tuseEffect(() => {\n\t\tinitMiniSentry(sentry ?? {});\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, []);\n\n\t/* ---- lazy-load the DOM inspector only on the client ---- */\n\tconst InspectorRef = useRef<React.ComponentType<any> | null>(null);\n\n\tif (typeof window !== 'undefined' && InspectorRef.current === null) {\n\t\t// `require` happens only in the browser bundle\n\t\tInspectorRef.current =\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-var-requires\n\t\t\trequire('./inspector/DOMInspector').DOMInspector;\n\t}\n\n\tconst Inspector = InspectorRef.current;\n\n\tif (Inspector) {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<AnalyticsTracker />\n\t\t\t\t{!hideBranding && <Branding />}\n\t\t\t\t<Inspector {...inspector}>\n\t\t\t\t\t{children}\n\t\t\t\t</Inspector>\n\t\t\t</>\n\t\t);\n\t}\n\treturn (\n\t\t<>\n\t\t\t<AnalyticsTracker />\n\t\t\t{!hideBranding && <Branding />}\n\t\t\t{children}\n\t\t</>\n\t);\n}","import ErrorStackParser from 'error-stack-parser';\nimport type { SentryStackFrame } from '../types';\n\nexport function normaliseError(err: unknown): {\n\tname: string;\n\tmessage: string;\n\tstack: string;\n\tframes: SentryStackFrame[];\n\troute: string; // ← NEW\n} {\n\tconst e = err instanceof Error ? err : new Error(String(err));\n\tconst frames = ErrorStackParser.parse(e as Error).map<SentryStackFrame>((f) => ({\n\t\tfile: f.fileName ?? '<unknown>',\n\t\tlineNumber: f.lineNumber ?? null,\n\t\tcolumn: f.columnNumber ?? null,\n\t\tmethodName: f.functionName ?? '<anon>',\n\t}));\n\n\treturn {\n\t\tname: e.name,\n\t\tmessage: e.message,\n\t\tstack: e.stack ?? '',\n\t\tframes,\n\t\troute: typeof window !== 'undefined' ? window.location.pathname : '<ssr>', // NEW\n\t};\n}\n","import { SourceMapConsumer, type RawSourceMap } from 'source-map-js';\nimport type { CodeFrameString, SentryStackFrame } from '../types';\n\nlet warned = false;\nconst failedSources = new Set<string>();\n\n// Store original fetch to avoid infinite loops when network-patcher is active\nconst originalFetch = typeof window !== 'undefined' ? window.fetch.bind(window) : fetch;\n\n/**\n * Returns a nicely-formatted string with line numbers and a “>” pointer.\n * Example:\n *\n * 41 │ const data = await fetchUser();\n * > 42 │ console.log(undefinedVariable);\n * 43 │ return <div>{data.name}</div>;\n */\nexport async function getCodeFrame(\n frame: SentryStackFrame,\n ctxLines = 5,\n): Promise<CodeFrameString | null> {\n if (!frame.file || frame.lineNumber == null || frame.column == null) return null;\n\n // Check if we've already failed to fetch this source map\n // Extract just the URL part if frame.file contains function name\n let cleanUrl = frame.file;\n if (cleanUrl.includes('(') && cleanUrl.includes(')')) {\n // Extract URL from format like \"window.fetch (http://localhost:3002/file.js)\"\n const match = cleanUrl.match(/\\(([^)]+)\\)/);\n if (match) {\n cleanUrl = match[1];\n }\n }\n \n const mapUrl = `${cleanUrl}.map`;\n if (failedSources.has(mapUrl)) {\n return null;\n }\n\n try {\n // -- load & consume the source-map --------------------------------------------------\n const mapResp = await originalFetch(mapUrl);\n if (!mapResp.ok) {\n failedSources.add(mapUrl);\n throw new Error('ma