creatr-devtools
Version:
Mini-Sentry + DOM Inspector toolkit for any React/Next app
1,211 lines (1,203 loc) • 63.3 kB
JavaScript
"use strict";
"use client";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/inspector/DOMInspector.tsx
var DOMInspector_exports = {};
__export(DOMInspector_exports, {
DOMInspector: () => DOMInspector
});
var import_react3, import_jsx_runtime2, DOMInspector;
var init_DOMInspector = __esm({
"src/inspector/DOMInspector.tsx"() {
"use strict";
"use client";
import_react3 = require("react");
import_jsx_runtime2 = require("react/jsx-runtime");
DOMInspector = ({
children,
highlightColor = "#4CAF50",
highlightWidth = 2
}) => {
const [isInspectorActive, setIsInspectorActive] = (0, import_react3.useState)(false);
const [isImageInspectorActive, setIsImageInspectorActive] = (0, import_react3.useState)(false);
const [isEditorActive, setIsEditorActive] = (0, import_react3.useState)(false);
const hoverHighlightRef = (0, import_react3.useRef)(null);
const activeHighlightRef = (0, import_react3.useRef)(null);
const selectedHighlightsRef = (0, import_react3.useRef)([]);
const containerRef = (0, import_react3.useRef)(null);
const hoveredElementRef = (0, import_react3.useRef)(null);
const activeElementRef = (0, import_react3.useRef)(null);
const elementObserversRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
const elementVisibilityRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
const [tooltipPosition, setTooltipPosition] = (0, import_react3.useState)({
x: 0,
y: 0
});
const [hoveredMetadata, setHoveredMetadata] = (0, import_react3.useState)(null);
const [loadingImages, setLoadingImages] = (0, import_react3.useState)(
/* @__PURE__ */ new Set()
);
function checkElementOcclusion(element) {
const rect = element.getBoundingClientRect();
const isInViewport = rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
if (!isInViewport) {
return {
element,
isVisible: false,
visibilityRatio: 0,
occludingElements: []
};
}
const occludingElements = [];
const elementZIndex = parseInt(window.getComputedStyle(element).zIndex) || 0;
const elementPosition = window.getComputedStyle(element).position;
const potentialOccluders = Array.from(document.elementsFromPoint(
rect.left + rect.width / 2,
rect.top + rect.height / 2
));
for (const potential of potentialOccluders) {
if (potential === element || element.contains(potential) || potential.contains(element)) {
continue;
}
const potentialRect = potential.getBoundingClientRect();
const isIntersecting = !(rect.right < potentialRect.left || rect.left > potentialRect.right || rect.bottom < potentialRect.top || rect.top > potentialRect.bottom);
if (isIntersecting) {
const potentialZIndex = parseInt(window.getComputedStyle(potential).zIndex) || 0;
const potentialPosition = window.getComputedStyle(potential).position;
const isOccluding = (
// Higher z-index and positioned
potentialZIndex > elementZIndex && (potentialPosition === "absolute" || potentialPosition === "relative" || potentialPosition === "fixed" || potentialPosition === "sticky") || // Or appears later in DOM when z-index is the same
potentialZIndex === elementZIndex && potential.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING
);
if (isOccluding) {
occludingElements.push(potential);
}
}
}
let visibleArea = rect.width * rect.height;
let totalArea = visibleArea;
for (const occluder of occludingElements) {
const occluderRect = occluder.getBoundingClientRect();
const xOverlap = Math.max(
0,
Math.min(rect.right, occluderRect.right) - Math.max(rect.left, occluderRect.left)
);
const yOverlap = Math.max(
0,
Math.min(rect.bottom, occluderRect.bottom) - Math.max(rect.top, occluderRect.top)
);
const overlapArea = xOverlap * yOverlap;
visibleArea -= overlapArea;
}
visibleArea = Math.max(0, visibleArea);
const visibilityRatio = totalArea > 0 ? visibleArea / totalArea : 0;
return {
element,
isVisible: visibilityRatio > 0.05,
// Consider visible if at least 5% is showing
visibilityRatio,
occludingElements
};
}
function observeElementVisibility(element) {
if (elementObserversRef.current.has(element)) {
return;
}
const initialVisibility = checkElementOcclusion(element);
elementVisibilityRef.current.set(element, initialVisibility);
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.target === element) {
const visibilityInfo = checkElementOcclusion(element);
elementVisibilityRef.current.set(element, visibilityInfo);
updateHighlightForElement(element);
}
}
},
{ threshold: [0, 0.1, 0.5, 1] }
);
observer.observe(element);
elementObserversRef.current.set(element, observer);
}
function unobserveElementVisibility(element) {
const observer = elementObserversRef.current.get(element);
if (observer) {
observer.disconnect();
elementObserversRef.current.delete(element);
elementVisibilityRef.current.delete(element);
}
}
function positionHighlightBox(highlightEl, element) {
if (!highlightEl || !element || !containerRef.current) {
if (highlightEl) highlightEl.style.display = "none";
return;
}
const visibilityInfo = elementVisibilityRef.current.get(element) || checkElementOcclusion(element);
if (!visibilityInfo.isVisible) {
highlightEl.style.display = "none";
return;
}
const containerRect = containerRef.current.getBoundingClientRect();
const elemRect = element.getBoundingClientRect();
const offsetLeft = elemRect.left - containerRect.left + containerRef.current.scrollLeft;
const offsetTop = elemRect.top - containerRect.top + containerRef.current.scrollTop;
highlightEl.style.display = "block";
highlightEl.style.left = `${offsetLeft}px`;
highlightEl.style.top = `${offsetTop}px`;
highlightEl.style.width = `${elemRect.width}px`;
highlightEl.style.height = `${elemRect.height}px`;
highlightEl.style.boxSizing = "border-box";
const opacity = 0.1 * visibilityInfo.visibilityRatio;
highlightEl.style.backgroundColor = highlightEl === activeHighlightRef.current ? `rgba(76, 175, 80, ${opacity})` : `rgba(33, 150, 243, ${opacity})`;
if (visibilityInfo.occludingElements.length > 0 && false) {
let clipPath = "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)";
highlightEl.style.clipPath = clipPath;
}
}
function updateHighlightForElement(element) {
if (activeElementRef.current === element && activeHighlightRef.current) {
positionHighlightBox(activeHighlightRef.current, element);
}
if (hoveredElementRef.current === element && hoverHighlightRef.current) {
positionHighlightBox(hoverHighlightRef.current, element);
}
selectedHighlightsRef.current.forEach((highlight) => {
if (highlight["targetElementRef"] === element) {
positionHighlightBox(highlight, element);
}
});
}
function createHighlight(element) {
const highlight = document.createElement("div");
highlight.style.position = "absolute";
highlight.style.pointerEvents = "none";
highlight.style.border = `${highlightWidth}px solid ${highlightColor}`;
highlight.style.backgroundColor = "rgba(76, 175, 80, 0.1)";
highlight.style.zIndex = "999999";
highlight.style.boxSizing = "border-box";
highlight.dataset.targetElement = element.outerHTML;
highlight["targetElementRef"] = element;
if (element.dataset.uniqueId) {
highlight["targetElementUniqueId"] = element.dataset.uniqueId;
}
containerRef.current?.appendChild(highlight);
observeElementVisibility(element);
positionHighlightBox(highlight, element);
return highlight;
}
function handlePositionUpdates() {
const elementsToCheck = /* @__PURE__ */ new Set();
selectedHighlightsRef.current.forEach((highlight) => {
const element = highlight["targetElementRef"];
if (element) {
elementsToCheck.add(element);
}
});
if (activeElementRef.current) {
elementsToCheck.add(activeElementRef.current);
}
if (hoveredElementRef.current) {
elementsToCheck.add(hoveredElementRef.current);
}
elementsToCheck.forEach((element) => {
const visibilityInfo = checkElementOcclusion(element);
elementVisibilityRef.current.set(element, visibilityInfo);
});
selectedHighlightsRef.current.forEach((highlight) => {
const element = highlight["targetElementRef"];
if (element) {
positionHighlightBox(highlight, element);
}
});
if (activeElementRef.current && activeHighlightRef.current) {
positionHighlightBox(activeHighlightRef.current, activeElementRef.current);
}
if (hoveredElementRef.current && hoverHighlightRef.current) {
positionHighlightBox(hoverHighlightRef.current, hoveredElementRef.current);
}
}
function clearHighlights() {
console.log("Clearing all highlights");
console.log(
"Before clear - selected highlights:",
selectedHighlightsRef.current.length
);
selectedHighlightsRef.current.forEach((highlight) => {
const element = highlight["targetElementRef"];
if (element) {
unobserveElementVisibility(element);
}
});
if (hoveredElementRef.current) {
unobserveElementVisibility(hoveredElementRef.current);
}
if (activeElementRef.current) {
unobserveElementVisibility(activeElementRef.current);
}
hoveredElementRef.current = null;
activeElementRef.current = null;
if (activeHighlightRef.current) {
activeHighlightRef.current.style.display = "none";
}
if (hoverHighlightRef.current) {
hoverHighlightRef.current.style.display = "none";
}
selectedHighlightsRef.current.forEach((highlight) => {
if (highlight["resizeObserver"]) {
highlight["resizeObserver"].disconnect();
}
if (highlight.parentNode) {
highlight.parentNode.removeChild(highlight);
}
});
selectedHighlightsRef.current = [];
console.log(
"After clear - selected highlights:",
selectedHighlightsRef.current.length
);
}
(0, import_react3.useEffect)(() => {
window.parent.postMessage(
{
type: "URL_CHANGE",
url: window.location.href
},
"*"
);
const handleNavigation = () => {
window.parent.postMessage(
{
type: "URL_CHANGE",
url: window.location.href
},
"*"
);
};
window.addEventListener("popstate", handleNavigation);
return () => {
window.removeEventListener("popstate", handleNavigation);
};
}, []);
(0, import_react3.useEffect)(() => {
const handleResize = handlePositionUpdates;
const handleScroll = handlePositionUpdates;
window.addEventListener("resize", handleResize);
window.addEventListener("scroll", handleScroll, true);
if (containerRef.current) {
containerRef.current.addEventListener("scroll", handleScroll, true);
}
let parent = containerRef.current?.parentElement;
while (parent) {
const overflowY = window.getComputedStyle(parent).overflowY;
const overflowX = window.getComputedStyle(parent).overflowX;
if (overflowY === "auto" || overflowY === "scroll" || overflowX === "auto" || overflowX === "scroll") {
parent.addEventListener("scroll", handleScroll, true);
}
parent = parent.parentElement;
}
const resizeObserver = new ResizeObserver((entries) => {
handlePositionUpdates();
});
selectedHighlightsRef.current.forEach((highlight) => {
const element = highlight["targetElementRef"];
if (element) {
resizeObserver.observe(element);
highlight["resizeObserver"] = resizeObserver;
}
});
const mutationObserver = new MutationObserver(() => {
handlePositionUpdates();
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "class"]
});
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("scroll", handleScroll, true);
if (containerRef.current) {
containerRef.current.removeEventListener("scroll", handleScroll, true);
}
let parent2 = containerRef.current?.parentElement;
while (parent2) {
const overflowY = window.getComputedStyle(parent2).overflowY;
const overflowX = window.getComputedStyle(parent2).overflowX;
if (overflowY === "auto" || overflowY === "scroll" || overflowX === "auto" || overflowX === "scroll") {
parent2.removeEventListener("scroll", handleScroll, true);
}
parent2 = parent2.parentElement;
}
resizeObserver.disconnect();
mutationObserver.disconnect();
elementObserversRef.current.forEach((observer) => {
observer.disconnect();
});
elementObserversRef.current.clear();
elementVisibilityRef.current.clear();
};
}, [selectedHighlightsRef.current, activeElementRef.current, hoveredElementRef.current]);
(0, import_react3.useEffect)(() => {
let styleEl = null;
if (isImageInspectorActive) {
styleEl = document.createElement("style");
styleEl.id = "only-images-clickable";
styleEl.textContent = `
/* Disable pointer events on everything... */
* {
pointer-events: none !important;
}
/* ...re-enable for <img> and <video> */
img, video {
pointer-events: auto !important;
}
`;
document.head.appendChild(styleEl);
}
return () => {
if (styleEl && styleEl.parentNode) {
styleEl.parentNode.removeChild(styleEl);
}
};
}, [isImageInspectorActive]);
function getElementMetadata(element) {
const uniqueId = element.dataset.uniqueId || "";
let duplicateCount = 1;
if (uniqueId) {
duplicateCount = document.querySelectorAll(`[data-unique-id="${uniqueId}"]`).length;
}
const hasDynamicText = element.dataset.dynamicText === "true";
return {
tagName: element.tagName.toLowerCase(),
classNames: typeof element.className === "string" ? element.className : element.className && element.className.baseVal || "",
elementId: element.id || null,
textContent: element.textContent,
boundingBox: element.getBoundingClientRect(),
attributes: Array.from(element.attributes).map((attr) => ({
name: attr.name,
value: attr.value
})),
uniqueId,
duplicateCount,
hasDynamicText
};
}
(0, import_react3.useEffect)(() => {
const handleMessage = (event) => {
const { type, enabled, targetId } = event.data;
console.log("BROOO", type, targetId);
function isTextOnly(el) {
return el.childNodes.length === 1 && el.childNodes[0].nodeType === Node.TEXT_NODE;
}
if (type === "GET_ELEMENT_STYLES") {
const targetElements = document.querySelectorAll(`[data-unique-id="${targetId}"]`);
console.log("GET_ELEMENT_STYLES", targetId);
if (targetElements.length > 0) {
const targetElement = targetElements[0];
const computedStyle = window.getComputedStyle(targetElement);
const hasDirectText = Array.from(targetElement.childNodes).some(
(node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0
);
const isElementTextOnly = isTextOnly(targetElement);
const hasDynamicText = targetElement.dataset.dynamicText === "true";
console.log("HAS DYNAMIC TEXT", hasDynamicText);
const isDynamicallyRendered = targetElements.length > 1 || hasDynamicText;
const computedStyles = {
// Padding
paddingTop: computedStyle.paddingTop,
paddingRight: computedStyle.paddingRight,
paddingBottom: computedStyle.paddingBottom,
paddingLeft: computedStyle.paddingLeft,
// Margin
margin: computedStyle.margin,
marginTop: computedStyle.marginTop,
marginRight: computedStyle.marginRight,
marginBottom: computedStyle.marginBottom,
marginLeft: computedStyle.marginLeft,
// Dimensions
width: computedStyle.width,
height: computedStyle.height,
// Colors and appearance
backgroundColor: computedStyle.backgroundColor,
color: computedStyle.color,
borderRadius: computedStyle.borderRadius,
// Border properties
borderWidth: computedStyle.borderWidth,
borderStyle: computedStyle.borderStyle,
borderColor: computedStyle.borderColor,
// Text properties
fontSize: computedStyle.fontSize,
fontWeight: computedStyle.fontWeight
};
const textContent = targetElement.textContent;
console.log("GET_ELEMENT_STYLES", {
computedStyles,
tailwindClasses: targetElement.className.split(" "),
isDynamicallyRendered,
duplicateCount: targetElements.length,
hasDirectText,
isTextOnly: isElementTextOnly,
hasDynamicText
});
window.parent.postMessage({
type: "ELEMENT_STYLES_RETRIEVED",
targetId,
metadata: {
computedStyles,
tailwindClasses: targetElement.className.split(" "),
isDynamicallyRendered,
duplicateCount: targetElements.length,
hasDirectText,
isTextOnly: isElementTextOnly,
hasDynamicText
}
}, "*");
}
}
if (type === "GET_ELEMENT_TEXT") {
const targetElement = document.querySelector(`[data-unique-id="${targetId}"]`);
console.log("GET_ELEMENT_TEXT", targetId);
console.log("GET_ELEMENT_TEXT", {
textContent: targetElement.textContent ?? null,
targetId
});
window.parent.postMessage({
type: "ELEMENT_TEXT_RETRIEVED",
targetId,
metadata: {
textContent: targetElement.textContent ?? null,
targetId
}
}, "*");
}
if (type === "UPDATE_ELEMENT_STYLES") {
const targetElements = document.querySelectorAll(`[data-unique-id="${targetId}"]`);
console.log("UPDATE_ELEMENT_STYLES", targetId, event.data.styles);
if (targetElements.length > 0) {
const keys = Object.keys(event.data.styles);
targetElements.forEach((element) => {
for (const key of keys) {
element.style[key] = event.data.styles[key];
}
});
handlePositionUpdates();
} else {
console.error(`No elements with data-unique-id="${targetId}" found`);
}
}
if (type === "UPDATE_ELEMENT_TEXT") {
const targetElements = document.querySelectorAll(`[data-unique-id="${targetId}"]`);
console.log("UPDATE_ELEMENT_TEXT", targetId, event.data.text);
if (targetElements.length > 0) {
targetElements.forEach((element) => {
element.textContent = event.data.text;
});
handlePositionUpdates();
} else {
console.error(`No elements with data-unique-id="${targetId}" found for text update`);
}
}
if (type === "DELETE_ELEMENT") {
const targetElements = document.querySelectorAll(`[data-unique-id="${targetId}"]`);
console.log("DELETE_ELEMENT", targetId);
if (targetElements.length > 0) {
targetElements.forEach((element) => {
element.parentNode.removeChild(element);
});
clearHighlights();
} else {
console.error(`No elements with data-unique-id="${targetId}" found for deletion`);
}
}
if (type === "TOGGLE_EDITOR") {
setIsEditorActive(!!enabled);
if (!enabled) clearHighlights();
if (containerRef.current) {
containerRef.current.style.cursor = enabled ? "crosshair" : "";
}
}
if (type === "TOGGLE_INSPECTOR") {
setIsInspectorActive(!!enabled);
if (enabled) setIsImageInspectorActive(false);
if (!enabled) clearHighlights();
if (containerRef.current) {
containerRef.current.style.cursor = enabled ? "crosshair" : "";
}
} else if (type === "TOGGLE_IMAGE_INSPECTOR") {
setIsImageInspectorActive(!!enabled);
if (enabled) setIsInspectorActive(false);
if (!enabled) clearHighlights();
if (containerRef.current) {
containerRef.current.style.cursor = enabled ? "crosshair" : "";
}
} else if (type === "UPDATE_IMAGE_URL") {
const { url } = event.data;
if (activeElementRef.current && url) {
const activeEl = activeElementRef.current;
const currentTagName = activeEl.tagName.toLowerCase();
const isVideoUrl = url.match(/\.(mp4|webm|ogg)$/i);
const shouldBeVideo = isVideoUrl !== null;
try {
if (shouldBeVideo && currentTagName === "img") {
const videoEl = document.createElement("video");
videoEl.autoplay = true;
videoEl.loop = true;
videoEl.muted = true;
videoEl.playsInline = true;
Array.from(activeEl.attributes).forEach((attr) => {
if (attr.name !== "src" && attr.name !== "alt") {
videoEl.setAttribute(attr.name, attr.value);
}
});
const styles = window.getComputedStyle(activeEl);
Array.from(styles).forEach((key) => {
try {
videoEl.style[key] = styles.getPropertyValue(key);
} catch (e) {
}
});
videoEl.src = url;
if (activeEl.parentNode) {
activeEl.parentNode.replaceChild(videoEl, activeEl);
activeElementRef.current = videoEl;
positionHighlightBox(activeHighlightRef.current, videoEl);
selectedHighlightsRef.current.forEach((highlight) => {
if (highlight.dataset.targetElement === activeEl.outerHTML) {
positionHighlightBox(highlight, videoEl);
highlight.dataset.targetElement = videoEl.outerHTML;
}
});
}
} else if (!shouldBeVideo && currentTagName === "video") {
const imgEl = document.createElement("img");
Array.from(activeEl.attributes).forEach((attr) => {
if (attr.name !== "src" && !attr.name.startsWith("autoplay")) {
imgEl.setAttribute(attr.name, attr.value);
}
});
const styles = window.getComputedStyle(activeEl);
Array.from(styles).forEach((key) => {
try {
imgEl.style[key] = styles.getPropertyValue(key);
} catch (e) {
}
});
imgEl.src = url;
if (activeEl.parentNode) {
activeEl.parentNode.replaceChild(imgEl, activeEl);
activeElementRef.current = imgEl;
positionHighlightBox(activeHighlightRef.current, imgEl);
selectedHighlightsRef.current.forEach((highlight) => {
if (highlight.dataset.targetElement === activeEl.outerHTML) {
positionHighlightBox(highlight, imgEl);
highlight.dataset.targetElement = imgEl.outerHTML;
}
});
}
} else {
if (currentTagName === "img") {
activeEl.src = url;
} else if (currentTagName === "video") {
activeEl.src = url;
}
}
} catch (error) {
console.error("Error during element conversion:", error);
}
}
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, []);
(0, import_react3.useEffect)(() => {
const container = containerRef.current;
if (!container) return;
function preventDefault(e) {
if (isInspectorActive || isImageInspectorActive || isEditorActive) {
e.preventDefault();
}
}
const handleMouseMove = (e) => {
if (!isInspectorActive && !isImageInspectorActive && !isEditorActive) return;
const target = e.target;
if (isImageInspectorActive && target.tagName.toLowerCase() !== "img" && target.tagName.toLowerCase() !== "video") {
hoveredElementRef.current = null;
positionHighlightBox(hoverHighlightRef.current, null);
setHoveredMetadata(null);
return;
}
if (hoveredElementRef.current !== target) {
if (target && target !== hoveredElementRef.current) {
observeElementVisibility(target);
}
hoveredElementRef.current = target;
positionHighlightBox(hoverHighlightRef.current, target);
setTooltipPosition({ x: e.pageX + 10, y: e.pageY + 10 });
const id = target.id ? `#${target.id}` : "";
const classes = typeof target.className === "string" && target.className.trim() ? `.${target.className.trim().split(" ").filter(Boolean).join(".")}` : "";
setHoveredMetadata(`<${target.tagName.toLowerCase()}>${id}${classes}`);
}
};
const handleMouseLeave = () => {
hoveredElementRef.current = null;
positionHighlightBox(hoverHighlightRef.current, null);
setHoveredMetadata(null);
};
const handleClick = (e) => {
if (!isInspectorActive && !isImageInspectorActive && !isEditorActive) {
return;
}
const targetElement = e.target;
e.preventDefault();
e.stopPropagation();
if (isImageInspectorActive) {
const tagName = targetElement.tagName.toLowerCase();
if (tagName !== "img" && tagName !== "video") {
return;
}
if (!targetElement.dataset.imageInspectorId) {
const uniqueId = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
targetElement.dataset.imageInspectorId = uniqueId;
}
}
if (isEditorActive) {
const uniqueId = targetElement.dataset.uniqueId;
const isAlreadySelected2 = uniqueId && selectedHighlightsRef.current.some(
(highlight) => highlight["targetElementUniqueId"] === uniqueId
);
clearHighlights();
if (isAlreadySelected2) {
const msg3 = {
type: "ELEMENT_INSPECTED",
metadata: getElementMetadata(targetElement),
isShiftPressed: e.shiftKey
};
window.parent.postMessage(msg3, "*");
return;
}
selectedHighlightsRef.current = [];
let elementsToHighlight = [targetElement];
if (uniqueId) {
const matchingElements = document.querySelectorAll(`[data-unique-id="${uniqueId}"]`);
elementsToHighlight = Array.from(matchingElements);
}
const highlights = elementsToHighlight.map((element) => {
observeElementVisibility(element);
const highlight = createHighlight(element);
highlight["targetElementUniqueId"] = uniqueId;
return highlight;
});
selectedHighlightsRef.current = highlights;
activeElementRef.current = targetElement;
const metadata = getElementMetadata(targetElement);
const msg2 = {
type: "ELEMENT_INSPECTED",
metadata,
// Already includes duplicateCount from getElementMetadata
isShiftPressed: false
// Always false in editor mode
};
window.parent.postMessage(msg2, "*");
return;
}
const isAlreadySelected = selectedHighlightsRef.current.some(
(highlight) => {
const targetRect = targetElement.getBoundingClientRect();
const highlightRect = highlight.getBoundingClientRect();
const heightMatch = Math.abs(highlightRect.height - targetRect.height) < 1;
const widthMatch = Math.abs(highlightRect.width - targetRect.width) < 1;
const topMatch = Math.abs(highlightRect.top - targetRect.top) < 1;
const horizontalOverlap = !(highlightRect.right < targetRect.left || highlightRect.left > targetRect.right);
const isMatch = heightMatch && widthMatch && topMatch && horizontalOverlap;
return isMatch;
}
);
if (!e.shiftKey && isAlreadySelected) {
clearHighlights();
activeElementRef.current = null;
if (!isImageInspectorActive) {
const msg2 = {
type: "ELEMENT_INSPECTED",
metadata: null,
isShiftPressed: false
};
window.parent.postMessage(msg2, "*");
}
return;
}
if (e.shiftKey) {
if (isAlreadySelected) {
selectedHighlightsRef.current = selectedHighlightsRef.current.filter(
(highlight) => {
const highlightRect = highlight.getBoundingClientRect();
const targetRect = targetElement.getBoundingClientRect();
const heightMatch = Math.abs(highlightRect.height - targetRect.height) < 1;
const widthMatch = Math.abs(highlightRect.width - targetRect.width) < 1;
const topMatch = Math.abs(highlightRect.top - targetRect.top) < 1;
const horizontalOverlap = !(highlightRect.right < targetRect.left || highlightRect.left > targetRect.right);
const shouldRemove = heightMatch && widthMatch && topMatch && horizontalOverlap;
if (shouldRemove) {
const element = highlight["targetElementRef"];
if (element) {
unobserveElementVisibility(element);
}
if (highlight["resizeObserver"]) {
highlight["resizeObserver"].disconnect();
}
if (highlight.parentNode) {
highlight.parentNode.removeChild(highlight);
}
}
return !shouldRemove;
}
);
} else {
observeElementVisibility(targetElement);
const highlight = createHighlight(targetElement);
selectedHighlightsRef.current.push(highlight);
const resizeObserver = new ResizeObserver(() => {
if (highlight["targetElementRef"]) {
positionHighlightBox(highlight, highlight["targetElementRef"]);
}
});
resizeObserver.observe(targetElement);
highlight["resizeObserver"] = resizeObserver;
}
} else {
selectedHighlightsRef.current.forEach((highlight2) => {
const element = highlight2["targetElementRef"];
if (element) {
unobserveElementVisibility(element);
}
if (highlight2["resizeObserver"]) {
highlight2["resizeObserver"].disconnect();
}
if (highlight2.parentNode) {
highlight2.parentNode.removeChild(highlight2);
}
});
selectedHighlightsRef.current = [];
observeElementVisibility(targetElement);
const highlight = createHighlight(targetElement);
selectedHighlightsRef.current = [highlight];
const resizeObserver = new ResizeObserver(() => {
if (highlight["targetElementRef"]) {
positionHighlightBox(highlight, highlight["targetElementRef"]);
}
});
resizeObserver.observe(targetElement);
highlight["resizeObserver"] = resizeObserver;
}
activeElementRef.current = targetElement;
const msg = {
type: "ELEMENT_INSPECTED",
metadata: getElementMetadata(targetElement),
isShiftPressed: e.shiftKey
};
window.parent.postMessage(msg, "*");
};
container.addEventListener("click", preventDefault, true);
container.addEventListener("mousedown", preventDefault, true);
container.addEventListener("mouseup", preventDefault, true);
container.addEventListener("submit", preventDefault, true);
container.addEventListener("mousemove", handleMouseMove);
container.addEventListener("mouseleave", handleMouseLeave);
document.addEventListener("click", handleClick, true);
return () => {
container.removeEventListener("click", preventDefault, true);
container.removeEventListener("mousedown", preventDefault, true);
container.removeEventListener("mouseup", preventDefault, true);
container.removeEventListener("submit", preventDefault, true);
container.removeEventListener(
"mousemove",
handleMouseMove
);
container.removeEventListener("mouseleave", handleMouseLeave);
document.removeEventListener("click", handleClick, true);
};
}, [isInspectorActive, isImageInspectorActive, isEditorActive]);
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
"div",
{
ref: containerRef,
className: `relative h-full w-full ${isInspectorActive || isImageInspectorActive || isEditorActive ? "cursor-crosshair" : ""}`,
children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"div",
{
ref: hoverHighlightRef,
style: {
position: "absolute",
display: "none",
pointerEvents: "none",
border: `${highlightWidth}px dashed #2196F3`,
backgroundColor: "rgba(33, 150, 243, 0.1)",
// Light blue with 10% opacity
zIndex: 999999
}
}
),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"div",
{
ref: activeHighlightRef,
style: {
position: "absolute",
display: "none",
pointerEvents: "none",
border: `${highlightWidth}px solid ${highlightColor}`,
backgroundColor: "rgba(76, 175, 80, 0.1)",
// Light green with 10% opacity
zIndex: 999999
}
}
),
children
]
}
);
};
}
});
// src/index.tsx
var index_exports = {};
__export(index_exports, {
DevtoolsProvider: () => DevtoolsProvider,
createIframeProxy: () => createIframeProxy,
initMiniSentry: () => initMiniSentry
});
module.exports = __toCommonJS(index_exports);
// src/DevtoolsProvider.tsx
var import_react4 = require("react");
// src/minisentry/error-normaliser.ts
var import_error_stack_parser = __toESM(require("error-stack-parser"));
function normaliseError(err) {
const e = err instanceof Error ? err : new Error(String(err));
const frames = import_error_stack_parser.default.parse(e).map((f) => ({
file: f.fileName ?? "<unknown>",
lineNumber: f.lineNumber ?? null,
column: f.columnNumber ?? null,
methodName: f.functionName ?? "<anon>"
}));
return {
name: e.name,
message: e.message,
stack: e.stack ?? "",
frames,
route: typeof window !== "undefined" ? window.location.pathname : "<ssr>"
// NEW
};
}
// src/minisentry/snippet-resolver.ts
var import_source_map_js = require("source-map-js");
var warned = false;
var failedSources = /* @__PURE__ */ new Set();
var originalFetch = typeof window !== "undefined" ? window.fetch.bind(window) : fetch;
async function getCodeFrame(frame, ctxLines = 5) {
if (!frame.file || frame.lineNumber == null || frame.column == null) return null;
let cleanUrl = frame.file;
if (cleanUrl.includes("(") && cleanUrl.includes(")")) {
const match = cleanUrl.match(/\(([^)]+)\)/);
if (match) {
cleanUrl = match[1];
}
}
const mapUrl = `${cleanUrl}.map`;
if (failedSources.has(mapUrl)) {
return null;
}
try {
const mapResp = await originalFetch(mapUrl);
if (!mapResp.ok) {
failedSources.add(mapUrl);
throw new Error("map 404");
}
const raw = await mapResp.json();
const consumer = new import_source_map_js.SourceMapConsumer(raw);
const orig = consumer.originalPositionFor({
line: frame.lineNumber,
column: frame.column,
bias: import_source_map_js.SourceMapConsumer.GREATEST_LOWER_BOUND
});
const content = orig.source && consumer.sourceContentFor(orig.source, true) || null;
consumer.destroy?.();
if (!content || orig.line == null) return null;
const lines = content.split("\n");
const start = Math.max(0, orig.line - 1 - ctxLines);
const end = Math.min(lines.length, orig.line + ctxLines);
const width = String(end).length;
const snippet = lines.slice(start, end).map((ln, idx) => {
const realLine = start + idx + 1;
const prefix = realLine === orig.line ? ">" : " ";
return `${prefix} ${String(realLine).padStart(width)} \u2502 ${ln}`;
}).join("\n");
return {
file: orig.source,
line: orig.line,
column: orig.column ?? 0,
snippet
// ← string instead of array
};
} catch (e) {
failedSources.add(mapUrl);
if (!warned) {
console.warn("[creatr-devtools] snippet resolver failed", e);
warned = true;
}
return null;
}
}
// src/minisentry/network-patcher.ts
var HANDLED = Symbol("__miniSentryHandled");
var INTERNAL_RE = /mini?sentry\/network-patcher/;
var SNIPPET_CTX = 5;
var BODY_LIMIT = 32768;
function trim(txt) {
return txt && txt.length > BODY_LIMIT ? txt.slice(0, BODY_LIMIT) + "\u2026" : txt;
}
function parseFrames(stack) {
return stack.split("\n").map((l) => {
const m = /at\s+(.*?)\s+\(?(.+):(\d+):(\d+)\)?/.exec(l);
if (!m) return null;
return {
file: m[2],
lineNumber: Number(m[3]),
column: Number(m[4]),
methodName: m[1] || "<anon>"
};
}).filter(Boolean);
}
async function captureSnippet(stack) {
const frames = parseFrames(stack).filter((f) => !INTERNAL_RE.test(f.file));
if (!frames.length) return null;
for (const f of frames) {
try {
const cf = await getCodeFrame(f, SNIPPET_CTX);
if (!cf) continue;
const { context, line } = cf;
const markerIdx = line - (cf.contextStart ?? line) - 1;
return context.map((ln, idx) => idx === markerIdx ? `> ${ln}` : ` ${ln}`).join("\n");
} catch {
}
}
return null;
}
function parseXHRHeaders(raw) {
return raw.trim().split(/[\r\n]+/).filter(Boolean).reduce((acc, l) => {
const i = l.indexOf(":");
if (i !== -1) acc[l.slice(0, i).trim()] = l.slice(i + 1).trim();
return acc;
}, {});
}
function patchNetwork(dispatch) {
if (typeof window.fetch === "function") {
const origFetch = window.fetch.bind(window);
window.fetch = async (...args) => {
const stackHolder = new Error();
try {
const res = await origFetch(...args);
if (res.ok) return res;
const snippet = await captureSnippet(stackHolder.stack || "");
const cloned = res.clone();
let responseBody = null;
try {
responseBody = trim(await cloned.text());
} catch {
}
dispatch({
type: "network-error",
url: res.url,
method: args[0] instanceof Request ? args[0].method || "GET" : "GET",
status: res.status,
reason: `${res.status} ${res.statusText}`,
route: window.location.pathname,
snippet,
request: {
headers: args[0] instanceof Request ? Object.fromEntries(args[0].headers.entries()) : {},
body: args[0] instanceof Request ? trim(await args[0].clone().text()) : null
},
response: {
status: res.status,
headers: Object.fromEntries(res.headers.entries()),
body: responseBody
},
buildId: globalThis.__NEXT_DATA__?.buildId ?? "dev",
ts: Date.now()
});
return res;
} catch (err) {
err[HANDLED] = true;
dispatch({
type: "network-error",
url: args[0] instanceof Request ? args[0].url : String(args[0]),
method: args[0] instanceof Request ? args[0].method || "GET" : "GET",
status: 0,
reason: err.message,
route: window.location.pathname,
snippet: await captureSnippet(stackHolder.stack || ""),
request: {
headers: args[0] instanceof Request ? Object.fromEntries(args[0].headers.entries()) : {},
body: args[0] instanceof Request ? trim(await args[0].clone().text()) : null
},
response: null,
buildId: globalThis.__NEXT_DATA__?.buildId ?? "dev",
ts: Date.now()
});
throw err;
}
};
}
const OrigXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function PatchedXHR() {
const xhr = new OrigXHR();
let method = "GET";
let url = "";
let reqBody = null;
const stackHolder = new Error();
xhr.open = new Proxy(xhr.open, {
apply(t, c, args) {
method = args[0];
url = args[1];
return Reflect.apply(t, c, args);
}
});
xhr.send = new Proxy(xhr.send, {
apply(t, c, args) {
reqBody = typeof args[0] === "string" ? trim(args[0]) : null;
return Reflect.apply(t, c, args);
}
});
xhr.addEventListener("loadend", async () => {
if (xhr.status !== 0 && xhr.status < 400) return;
dispatch({
type: "network-error",
url,
method,
status: xhr.status,
reason: xhr.statusText || "network error",
route: window.location.pathname,
snippet: await captureSnippet(stackHolder.stack || ""),
request: {
headers: {},
// cannot read request headers from XHR
body: reqBody
},
response: {
status: xhr.status,
headers: parseXHRHeaders(xhr.getAllResponseHeaders()),
body: trim(xhr.responseText || null)
},
buildId: globalThis.__NEXT_DATA__?.buildId ?? "dev",
ts: Date.now()
});
});
return xhr;
};
}
// src/minisentry/index.ts
var installed = false;
function initMiniSentry(opts = {}) {
if (installed || typeof window === "undefined") return;
installed = true;
const ORIGIN = opts.parentOrigin ?? "*";
const CTX = opts.snippetContext ?? 5;
const RATE = opts.rateLimit ?? { max: 10, per: 1e4 };
let count = 0;
setInterval(() => count = 0, RATE.per);
function dispatch(evt) {
if (count >= RATE.max) return;
count++;
const finalEvt = opts.beforeSend ? opts.beforeSend(evt) : evt;
if (finalEvt) window.parent?.postMessage(finalEvt, ORIGIN);
}
async function handleRuntimeError(err) {
if (err && err[HANDLED]) return;
const base = normaliseError(err);
const candidate = base.frames.find(
(f) => f.file && f.lineNumber != null && f.column != null
);
let snippet = null;
if (candidate) {
try {
snippet = await getCodeFrame(candidate, CTX);
} catch {
}
}
const evt = {
type: "runtime-error",
buildId: globalThis.__NEXT_DATA__?.buildId ?? "dev",
ts: Date.now(),
...base,
snippet
};
dispatch(evt);
}
window.addEventListener("error", (e) => handleRuntimeError(e.error || e));
window.addEventListener("unhandledrejection", (e) => {
if (e.reason && e.reason[HANDLED]) return;
handleRuntimeError(e.reason);
});
patchNetwork((netEvt) => dispatch(netEvt));
}
// src/branding/BrandingTag.tsx
var import_react = require("react");
var import_jsx_runtime = require("react/jsx-runtime");
function Branding() {
const [isVisible, setIsVisible] = (0, import_react.useState)(true);
(0, import_react.useEffect)(() => {
const handleMessage = (event) => {
if (event.data.type === "TOGGLE_BRANDING") {
setIsVisible(event.data.visible);
}
};
const isInIframe = window !== window.parent;
if (isInIframe) {
setIsVisible(false);
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
if (!isVisible) return null;
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
.creatr-branding-fixed {
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
align-items: center;
background: rgba