feat: precompute selector mapping, shadow dom support

This commit is contained in:
Rohit
2025-07-16 00:27:47 +05:30
parent 8cdb4f0bfd
commit 8940e8dbd6

View File

@@ -147,7 +147,18 @@ export const BrowserWindow = () => {
const { browserWidth, browserHeight } = useBrowserDimensionsStore();
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
const [screenShot, setScreenShot] = useState<string>("");
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[], groupElements?: Array<{ element: HTMLElement; rect: DOMRect } >} | null>(null);
const [highlighterData, setHighlighterData] = useState<{
rect: DOMRect;
selector: string;
elementInfo: ElementInfo | null;
isShadow?: boolean;
childSelectors?: string[];
groupElements?: Array<{ element: HTMLElement; rect: DOMRect }>;
similarElements?: {
elements: HTMLElement[];
rects: DOMRect[];
};
} | null>(null);
const [showAttributeModal, setShowAttributeModal] = useState(false);
const [attributeOptions, setAttributeOptions] = useState<AttributeOption[]>([]);
const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null);
@@ -161,11 +172,20 @@ export const BrowserWindow = () => {
const [paginationSelector, setPaginationSelector] = useState<string>('');
const highlighterUpdateRef = useRef<number>(0);
const [isCachingChildSelectors, setIsCachingChildSelectors] = useState(false);
const [cachedListSelector, setCachedListSelector] = useState<string | null>(
null
);
const [pendingNotification, setPendingNotification] = useState<{
type: "error" | "warning" | "info" | "success";
message: string;
count?: number;
} | null>(null);
const { socket } = useSocketStore();
const { notify, currentTextActionId, currentListActionId, updateDOMMode, isDOMMode, currentSnapshot } = useGlobalInfoStore();
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
const { addTextStep, addListStep, updateListStepData } = useBrowserSteps();
const { addTextStep, addListStep } = useBrowserSteps();
const [currentGroupInfo, setCurrentGroupInfo] = useState<{
isGroupElement: boolean;
@@ -270,17 +290,6 @@ export const BrowserWindow = () => {
[user?.id, socket, updateDOMMode]
);
const screenshotModeHandler = useCallback(
(data: any) => {
if (!data.userId || data.userId === user?.id) {
updateDOMMode(false);
socket?.emit("screenshot-mode-enabled");
setIsLoading(false);
}
},
[user?.id, updateDOMMode]
);
const domModeErrorHandler = useCallback(
(data: any) => {
if (!data.userId || data.userId === user?.id) {
@@ -300,28 +309,68 @@ export const BrowserWindow = () => {
}, [isDOMMode, getList, listSelector, paginationMode]);
useEffect(() => {
if (isDOMMode && listSelector) {
socket?.emit("setGetList", { getList: true });
socket?.emit("listSelector", { selector: listSelector });
if (isDOMMode && listSelector) {
socket?.emit("setGetList", { getList: true });
socket?.emit("listSelector", { selector: listSelector });
clientSelectorGenerator.setListSelector(listSelector);
clientSelectorGenerator.setListSelector(listSelector);
setCachedChildSelectors([]);
if (currentSnapshot && cachedListSelector !== listSelector) {
setCachedChildSelectors([]);
setIsCachingChildSelectors(true);
setCachedListSelector(listSelector);
if (currentSnapshot) {
const iframeElement = document.querySelector(
"#dom-browser-iframe"
) as HTMLIFrameElement;
if (iframeElement?.contentDocument) {
const childSelectors = clientSelectorGenerator.getChildSelectors(
iframeElement.contentDocument,
listSelector
);
setCachedChildSelectors(childSelectors);
const iframeElement = document.querySelector(
"#dom-browser-iframe"
) as HTMLIFrameElement;
if (iframeElement?.contentDocument) {
setTimeout(() => {
try {
const childSelectors =
clientSelectorGenerator.getChildSelectors(
iframeElement.contentDocument as Document,
listSelector
);
clientSelectorGenerator.precomputeChildSelectorMappings(
childSelectors,
iframeElement.contentDocument as Document
);
setCachedChildSelectors(childSelectors);
} catch (error) {
console.error("Error during child selector caching:", error);
} finally {
setIsCachingChildSelectors(false);
if (pendingNotification) {
notify(pendingNotification.type, pendingNotification.message);
setPendingNotification(null);
}
}
}
}, 100);
} else {
setIsCachingChildSelectors(false);
}
}
}, [isDOMMode, listSelector, socket, getList, currentSnapshot]);
}
}, [
isDOMMode,
listSelector,
socket,
getList,
currentSnapshot,
cachedListSelector,
pendingNotification,
notify,
]);
useEffect(() => {
if (!listSelector) {
setCachedListSelector(null);
}
}, [listSelector]);
useEffect(() => {
coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height);
@@ -389,7 +438,6 @@ export const BrowserWindow = () => {
socket.on("screencast", screencastHandler);
socket.on("domcast", rrwebSnapshotHandler);
socket.on("dom-mode-enabled", domModeHandler);
// socket.on("screenshot-mode-enabled", screenshotModeHandler);
socket.on("dom-mode-error", domModeErrorHandler);
}
@@ -403,7 +451,6 @@ export const BrowserWindow = () => {
socket.off("screencast", screencastHandler);
socket.off("domcast", rrwebSnapshotHandler);
socket.off("dom-mode-enabled", domModeHandler);
// socket.off("screenshot-mode-enabled", screenshotModeHandler);
socket.off("dom-mode-error", domModeErrorHandler);
}
};
@@ -415,7 +462,6 @@ export const BrowserWindow = () => {
screencastHandler,
rrwebSnapshotHandler,
domModeHandler,
// screenshotModeHandler,
domModeErrorHandler,
]);
@@ -425,12 +471,17 @@ export const BrowserWindow = () => {
selector: string;
elementInfo: ElementInfo | null;
childSelectors?: string[];
isShadow?: boolean;
groupInfo?: {
isGroupElement: boolean;
groupSize: number;
groupElements: HTMLElement[];
groupFingerprint: ElementFingerprint;
};
similarElements?: {
elements: HTMLElement[];
rects: DOMRect[];
};
isDOMMode?: boolean;
}) => {
if (!getText && !getList) {
@@ -460,6 +511,22 @@ export const BrowserWindow = () => {
const iframeRect = iframeElement.getBoundingClientRect();
const IFRAME_BODY_PADDING = 16;
let mappedSimilarElements;
if (data.similarElements) {
mappedSimilarElements = {
elements: data.similarElements.elements,
rects: data.similarElements.rects.map(
(rect) =>
new DOMRect(
rect.x + iframeRect.left - IFRAME_BODY_PADDING,
rect.y + iframeRect.top - IFRAME_BODY_PADDING,
rect.width,
rect.height
)
),
};
}
if (data.groupInfo) {
setCurrentGroupInfo(data.groupInfo);
} else {
@@ -686,6 +753,7 @@ export const BrowserWindow = () => {
(highlighterData: {
rect: DOMRect;
selector: string;
isShadow?: boolean;
elementInfo: ElementInfo | null;
childSelectors?: string[];
groupInfo?: {
@@ -713,11 +781,17 @@ export const BrowserWindow = () => {
)
);
addListStep(
listSelector!,
fields,
currentListId || 0,
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: paginationType, selector: highlighterData.selector }
listSelector!,
fields,
currentListId || 0,
currentListActionId || `list-${crypto.randomUUID()}`,
{
type: paginationType,
selector: highlighterData.selector,
isShadow: highlighterData.isShadow
},
undefined,
highlighterData.isShadow
);
socket?.emit("setPaginationMode", { pagination: false });
}
@@ -776,7 +850,7 @@ export const BrowserWindow = () => {
selectorObj: {
selector: currentSelector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
isShadow: highlighterData.elementInfo?.isShadowRoot,
attribute,
},
};
@@ -794,7 +868,9 @@ export const BrowserWindow = () => {
updatedFields,
currentListId,
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: "", selector: paginationSelector }
{ type: "", selector: paginationSelector },
undefined,
highlighterData.isShadow
);
}
} else {
@@ -829,7 +905,7 @@ export const BrowserWindow = () => {
{
selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
isShadow: highlighterData.isShadow || highlighterData.elementInfo?.isShadowRoot,
attribute,
},
currentTextActionId || `text-${crypto.randomUUID()}`
@@ -908,7 +984,7 @@ export const BrowserWindow = () => {
{
selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
isShadow: highlighterData.isShadow || highlighterData.elementInfo?.isShadowRoot,
attribute,
},
currentTextActionId || `text-${crypto.randomUUID()}`
@@ -942,7 +1018,9 @@ export const BrowserWindow = () => {
fields,
currentListId || 0,
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: paginationType, selector: highlighterData.selector }
{ type: paginationType, selector: highlighterData.selector, isShadow: highlighterData.isShadow },
undefined,
highlighterData.isShadow
);
socket?.emit("setPaginationMode", { pagination: false });
}
@@ -1000,7 +1078,7 @@ export const BrowserWindow = () => {
selectorObj: {
selector: currentSelector,
tag: highlighterData.elementInfo?.tagName,
shadow: highlighterData.elementInfo?.isShadowRoot,
isShadow: highlighterData.elementInfo?.isShadowRoot,
attribute,
},
};
@@ -1018,7 +1096,9 @@ export const BrowserWindow = () => {
updatedFields,
currentListId,
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: "", selector: paginationSelector }
{ type: "", selector: paginationSelector, isShadow: highlighterData.isShadow },
undefined,
highlighterData.isShadow
);
}
} else {
@@ -1052,7 +1132,7 @@ export const BrowserWindow = () => {
addTextStep('', data, {
selector: selectedElement.selector,
tag: selectedElement.info?.tagName,
shadow: selectedElement.info?.isShadowRoot,
isShadow: highlighterData?.isShadow || selectedElement.info?.isShadowRoot,
attribute: attribute
}, currentTextActionId || `text-${crypto.randomUUID()}`);
}
@@ -1065,7 +1145,7 @@ export const BrowserWindow = () => {
selectorObj: {
selector: selectedElement.selector,
tag: selectedElement.info?.tagName,
shadow: selectedElement.info?.isShadowRoot,
isShadow: selectedElement.info?.isShadowRoot,
attribute: attribute
}
};
@@ -1083,7 +1163,9 @@ export const BrowserWindow = () => {
updatedFields,
currentListId,
currentListActionId || `list-${crypto.randomUUID()}`,
{ type: '', selector: paginationSelector }
{ type: "", selector: paginationSelector, isShadow: highlighterData?.isShadow },
undefined,
highlighterData?.isShadow
);
}
}
@@ -1110,260 +1192,370 @@ export const BrowserWindow = () => {
}, [paginationMode, resetPaginationSelector]);
return (
<div onClick={handleClick} style={{ width: browserWidth }} id="browser-window">
{
getText === true || getList === true ? (
<GenericModal
isOpen={showAttributeModal}
onClose={() => {
setShowAttributeModal(false);
setSelectedElement(null);
setAttributeOptions([]);
}}
canBeClosed={true}
modalStyle={modalStyle}
<div
onClick={handleClick}
style={{ width: browserWidth }}
id="browser-window"
>
{/* Attribute selection modal */}
{(getText === true || getList === true) && (
<GenericModal
isOpen={showAttributeModal}
onClose={() => {
setShowAttributeModal(false);
setSelectedElement(null);
setAttributeOptions([]);
}}
canBeClosed={true}
modalStyle={modalStyle}
>
<div>
<h2>Select Attribute</h2>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "20px",
marginTop: "30px",
}}
>
{attributeOptions.map((option) => (
<Button
variant="outlined"
size="medium"
key={option.value}
onClick={() => {
handleAttributeSelection(option.value);
}}
style={{
justifyContent: "flex-start",
maxWidth: "80%",
overflow: "hidden",
}}
sx={{
color: "#ff00c3 !important",
borderColor: "#ff00c3 !important",
backgroundColor: "whitesmoke !important",
}}
>
<span
style={{
display: "block",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "100%",
}}
>
<div>
<h2>Select Attribute</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', marginTop: '30px' }}>
{attributeOptions.map((option) => (
<Button
variant="outlined"
size="medium"
key={option.value}
onClick={() => handleAttributeSelection(option.value)}
style={{
justifyContent: 'flex-start',
maxWidth: '80%',
overflow: 'hidden',
padding: '5px 10px',
}}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}
>
<span style={{
display: 'block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%'
}}>
{option.label}
</span>
</Button>
))}
</div>
</div>
</GenericModal>
) : null
}
{option.label}
</span>
</Button>
))}
</div>
</div>
</GenericModal>
)}
{datePickerInfo && (
<DatePicker
coordinates={datePickerInfo.coordinates}
selector={datePickerInfo.selector}
onClose={() => setDatePickerInfo(null)}
/>
)}
{dropdownInfo && (
<Dropdown
coordinates={dropdownInfo.coordinates}
selector={dropdownInfo.selector}
options={dropdownInfo.options}
onClose={() => setDropdownInfo(null)}
/>
)}
{timePickerInfo && (
<TimePicker
coordinates={timePickerInfo.coordinates}
selector={timePickerInfo.selector}
onClose={() => setTimePickerInfo(null)}
/>
)}
{dateTimeLocalInfo && (
<DateTimeLocalPicker
coordinates={dateTimeLocalInfo.coordinates}
selector={dateTimeLocalInfo.selector}
onClose={() => setDateTimeLocalInfo(null)}
/>
)}
{datePickerInfo && (
<DatePicker
coordinates={datePickerInfo.coordinates}
selector={datePickerInfo.selector}
onClose={() => setDatePickerInfo(null)}
/>
)}
{dropdownInfo && (
<Dropdown
coordinates={dropdownInfo.coordinates}
selector={dropdownInfo.selector}
options={dropdownInfo.options}
onClose={() => setDropdownInfo(null)}
/>
)}
{timePickerInfo && (
<TimePicker
coordinates={timePickerInfo.coordinates}
selector={timePickerInfo.selector}
onClose={() => setTimePickerInfo(null)}
/>
)}
{dateTimeLocalInfo && (
<DateTimeLocalPicker
coordinates={dateTimeLocalInfo.coordinates}
selector={dateTimeLocalInfo.selector}
onClose={() => setDateTimeLocalInfo(null)}
/>
)}
<div style={{ height: dimensions.height, overflow: "hidden" }}>
{(getText === true || getList === true) &&
!showAttributeModal &&
highlighterData?.rect != null && (
<>
{!isDOMMode && canvasRef?.current && (
<Highlighter
unmodifiedRect={highlighterData?.rect}
displayedSelector={highlighterData?.selector}
width={dimensions.width}
height={dimensions.height}
canvasRect={canvasRef.current.getBoundingClientRect()}
/>
{/* Main content area */}
<div style={{ height: dimensions.height, overflow: "hidden" }}>
{/* Add CSS for the spinner animation */}
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
{(getText === true || getList === true) &&
!showAttributeModal &&
highlighterData?.rect != null && (
<>
{!isDOMMode && canvasRef?.current && (
<Highlighter
unmodifiedRect={highlighterData?.rect}
displayedSelector={highlighterData?.selector}
width={dimensions.width}
height={dimensions.height}
canvasRect={canvasRef.current.getBoundingClientRect()}
/>
)}
{isDOMMode && highlighterData && (
<>
{/* Individual element highlight (for non-group or hovered element) */}
{getText && !listSelector && (
<div
style={{
position: "absolute",
left: Math.max(0, highlighterData.rect.x),
top: Math.max(0, highlighterData.rect.y),
width: Math.min(
highlighterData.rect.width,
dimensions.width
),
height: Math.min(
highlighterData.rect.height,
dimensions.height
),
background: "rgba(255, 0, 195, 0.15)",
border: "2px solid #ff00c3",
borderRadius: "3px",
pointerEvents: "none",
zIndex: 1000,
boxShadow: "0 0 0 1px rgba(255, 255, 255, 0.8)",
transition: "all 0.1s ease-out",
}}
/>
)}
{isDOMMode && highlighterData && (
<>
{/* Individual element highlight (for non-group or hovered element) */}
{(!getList ||
listSelector ||
!currentGroupInfo?.isGroupElement) && (
<div
style={{
position: "absolute",
left: Math.max(0, highlighterData.rect.x),
top: Math.max(0, highlighterData.rect.y),
width: Math.min(
highlighterData.rect.width,
dimensions.width
),
height: Math.min(
highlighterData.rect.height,
dimensions.height
),
background: "rgba(255, 0, 195, 0.15)",
border: "2px solid #ff00c3",
borderRadius: "3px",
pointerEvents: "none",
zIndex: 1000,
boxShadow: "0 0 0 1px rgba(255, 255, 255, 0.8)",
transition: "all 0.1s ease-out",
}}
/>
)}
{/* Group elements highlighting with real-time coordinates */}
{getList &&
!listSelector &&
currentGroupInfo?.isGroupElement &&
highlighterData.groupElements &&
highlighterData.groupElements.map(
(groupElement, index) => (
<React.Fragment key={index}>
{/* Highlight box */}
<div
style={{
position: "absolute",
left: Math.max(0, groupElement.rect.x),
top: Math.max(0, groupElement.rect.y),
width: Math.min(
groupElement.rect.width,
dimensions.width
),
height: Math.min(
groupElement.rect.height,
dimensions.height
),
background: "rgba(255, 0, 195, 0.15)",
border: "2px dashed #ff00c3",
borderRadius: "3px",
pointerEvents: "none",
zIndex: 1000,
boxShadow: "0 0 0 1px rgba(255, 255, 255, 0.8)",
transition: "all 0.1s ease-out",
}}
/>
{/* Group elements highlighting with real-time coordinates */}
{getList &&
!listSelector &&
currentGroupInfo?.isGroupElement &&
highlighterData.groupElements &&
highlighterData.groupElements.map((groupElement, index) => (
<React.Fragment key={index}>
{/* Highlight box */}
<div
style={{
position: "absolute",
left: Math.max(0, groupElement.rect.x),
top: Math.max(0, groupElement.rect.y),
width: Math.min(
groupElement.rect.width,
dimensions.width
),
height: Math.min(
groupElement.rect.height,
dimensions.height
),
background: "rgba(255, 0, 195, 0.15)",
border: "2px dashed #ff00c3",
borderRadius: "3px",
pointerEvents: "none",
zIndex: 1000,
boxShadow: "0 0 0 1px rgba(255, 255, 255, 0.8)",
transition: "all 0.1s ease-out",
}}
/>
<div
style={{
position: "absolute",
left: Math.max(0, groupElement.rect.x),
top: Math.max(0, groupElement.rect.y - 20),
background: "#ff00c3",
color: "white",
padding: "2px 6px",
fontSize: "10px",
fontWeight: "bold",
borderRadius: "2px",
pointerEvents: "none",
zIndex: 1001,
whiteSpace: "nowrap",
}}
>
List item {index + 1}
</div>
</React.Fragment>
)
)}
<div
style={{
position: "absolute",
left: Math.max(0, groupElement.rect.x),
top: Math.max(0, groupElement.rect.y - 20),
background: "#ff00c3",
color: "white",
padding: "2px 6px",
fontSize: "10px",
fontWeight: "bold",
borderRadius: "2px",
pointerEvents: "none",
zIndex: 1001,
whiteSpace: "nowrap",
}}
>
List item {index + 1}
</div>
</React.Fragment>
))}
</>
)}
</>
{getList &&
listSelector &&
!paginationMode &&
!limitMode &&
highlighterData?.similarElements &&
highlighterData.similarElements.rects.map(
(rect, index) => (
<React.Fragment key={`item-${index}`}>
{/* Highlight box for similar element */}
<div
style={{
position: "absolute",
left: Math.max(0, rect.x),
top: Math.max(0, rect.y),
width: Math.min(rect.width, dimensions.width),
height: Math.min(
rect.height,
dimensions.height
),
background: "rgba(255, 0, 195, 0.15)",
border: "2px dashed #ff00c3",
borderRadius: "3px",
pointerEvents: "none",
zIndex: 1000,
boxShadow: "0 0 0 1px rgba(255, 255, 255, 0.8)",
transition: "all 0.1s ease-out",
}}
/>
{/* Label for similar element */}
<div
style={{
position: "absolute",
left: Math.max(0, rect.x),
top: Math.max(0, rect.y - 20),
background: "#ff00c3",
color: "white",
padding: "2px 6px",
fontSize: "10px",
fontWeight: "bold",
borderRadius: "2px",
pointerEvents: "none",
zIndex: 1001,
whiteSpace: "nowrap",
}}
>
Item {index + 1}
</div>
</React.Fragment>
)
)}
</>
)}
</>
)}
{isDOMMode ? (
currentSnapshot ? (
<DOMBrowserRenderer
width={dimensions.width}
height={dimensions.height}
snapshot={currentSnapshot}
getList={getList}
getText={getText}
listSelector={listSelector}
cachedChildSelectors={cachedChildSelectors}
paginationMode={paginationMode}
paginationType={paginationType}
limitMode={limitMode}
onHighlight={(data: any) => {
domHighlighterHandler(data);
}}
onElementSelect={handleDOMElementSelection}
onShowDatePicker={handleShowDatePicker}
onShowDropdown={handleShowDropdown}
onShowTimePicker={handleShowTimePicker}
onShowDateTimePicker={handleShowDateTimePicker}
/>
) : (
<div
style={{
width: dimensions.width,
height: dimensions.height,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#f5f5f5",
borderRadius: "5px",
flexDirection: "column",
gap: "20px",
}}
>
<div
style={{
width: "60px",
height: "60px",
borderTop: "4px solid transparent",
borderRadius: "50%",
animation: "spin 1s linear infinite",
}}
/>
<div
style={{
fontSize: "18px",
color: "#ff00c3",
fontWeight: "bold",
}}
>
Loading website...
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
)
) : (
/* Screenshot mode canvas */
<Canvas
onCreateRef={setCanvasReference}
width={dimensions.width}
height={dimensions.height}
/>
)}
{isDOMMode ? (
<div
style={{ position: "relative", width: "100%", height: "100%" }}
>
{currentSnapshot ? (
<DOMBrowserRenderer
width={dimensions.width}
height={dimensions.height}
snapshot={currentSnapshot}
getList={getList}
getText={getText}
listSelector={listSelector}
cachedChildSelectors={cachedChildSelectors}
paginationMode={paginationMode}
paginationType={paginationType}
limitMode={limitMode}
onHighlight={(data) => {
domHighlighterHandler(data);
}}
isCachingChildSelectors={isCachingChildSelectors}
onElementSelect={handleDOMElementSelection}
onShowDatePicker={handleShowDatePicker}
onShowDropdown={handleShowDropdown}
onShowTimePicker={handleShowTimePicker}
onShowDateTimePicker={handleShowDateTimePicker}
/>
) : (
<div
style={{
width: dimensions.width,
height: dimensions.height,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#f5f5f5",
borderRadius: "5px",
flexDirection: "column",
gap: "20px",
}}
>
<div
style={{
width: "60px",
height: "60px",
borderTop: "4px solid transparent",
borderRadius: "50%",
animation: "spin 1s linear infinite",
}}
/>
<div
style={{
fontSize: "18px",
color: "#ff00c3",
fontWeight: "bold",
}}
>
Loading website...
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
)}
{/* Loading overlay positioned specifically over DOM content */}
{isCachingChildSelectors && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
background: "rgba(255, 255, 255, 0.8)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 9999,
pointerEvents: "none",
borderRadius: "0px 0px 5px 5px", // Match the DOM renderer border radius
}}
>
<div
style={{
width: "40px",
height: "40px",
border: "4px solid #f3f3f3",
borderTop: "4px solid #ff00c3",
borderRadius: "50%",
animation: "spin 1s linear infinite",
}}
/>
</div>
)}
</div>
) : (
/* Screenshot mode canvas */
<Canvas
onCreateRef={setCanvasReference}
width={dimensions.width}
height={dimensions.height}
/>
)}
</div>
</div>
);
};