feat: create portal component added to view child in new window

This commit is contained in:
2025-08-22 11:49:11 +05:30
parent 40de4d77f3
commit 1db5fe5707
4 changed files with 448 additions and 163 deletions

View File

@@ -710,6 +710,27 @@ export const ExpandIcon = ({ isActive }: { isActive: boolean }) => {
);
};
export const ExpandIcon2 = () => {
return (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.78365 2H6.6001C6.46676 2 6.33342 2.08334 6.33342 2.21668V2.71672C6.33342 2.85006 6.45009 3.00007 6.6001 3.00007H7.91686C8.06687 3.00007 8.15021 3.16675 8.03353 3.26675L5.20001 6.10028C5.1 6.20028 5.1 6.3503 5.20001 6.4503L5.55003 6.80033C5.65004 6.90033 5.80005 6.90033 5.90005 6.80033L8.73358 3.9668C8.83359 3.86679 9.00026 3.93346 9.00026 4.08347V5.40023C9.00026 5.53357 9.1336 5.68358 9.26695 5.68358H9.75031C9.88366 5.68358 10.0003 5.53357 10.0003 5.40023V2.23335C10.0003 2.08334 9.91699 2 9.78365 2Z"
fill="var(--text-color)"
/>
<path
d="M7.71705 5.91777L7.15035 6.50115C7.05034 6.60115 7.00034 6.71783 7.00034 6.85117V8.7513C7.00034 8.88464 6.88366 9.00132 6.75032 9.00132H3.25008C3.11674 9.00132 3.00007 8.88464 3.00007 8.7513V5.25106C3.00007 5.11772 3.11674 5.00104 3.25008 5.00104H5.16688C5.30022 5.00104 5.43357 4.95104 5.5169 4.85103L6.08361 4.28433C6.18362 4.18432 6.11695 4.00098 5.96694 4.00098H2.66671C2.30002 4.00098 2 4.301 2 4.66769V9.33467C2 9.70136 2.30002 10.0014 2.66671 10.0014H7.33369C7.70039 10.0014 8.00041 9.70136 8.00041 9.33467V6.03445C8.00041 5.88444 7.81706 5.81777 7.71705 5.91777Z"
fill="var(--text-color)"
/>
</svg>
);
};
export const StartIcon = () => {
return (
<svg

View File

@@ -0,0 +1,182 @@
import React, { ReactNode, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
type NewWindowProps = {
children: ReactNode;
title?: string;
width?: number;
height?: number;
left?: number;
top?: number;
center?: boolean;
features?: Partial<{
toolbar: boolean;
menubar: boolean;
scrollbars: boolean;
resizable: boolean;
location: boolean;
status: boolean;
}>;
onClose?: () => void;
copyStyles?: boolean;
noopener?: boolean;
className?: string;
theme?: string | null;
};
export const RenderInNewWindow: React.FC<NewWindowProps> = ({
children,
title = "New Window",
width = 900,
height = 700,
left,
top,
center = true,
features,
onClose,
copyStyles = true,
noopener = true,
className,
theme = "light",
}) => {
const [mounted, setMounted] = useState(false);
const childWindowRef = useRef<Window | null>(null);
const containerElRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (typeof window === "undefined") return;
const screenLeft = window.screenLeft ?? window.screenX ?? 0;
const screenTop = window.screenTop ?? window.screenY ?? 0;
const availWidth = window.outerWidth ?? window.innerWidth;
const availHeight = window.outerHeight ?? window.innerHeight;
const finalLeft =
center && availWidth
? Math.max(0, screenLeft + (availWidth - width) / 2)
: left ?? 100;
const finalTop =
center && availHeight
? Math.max(0, screenTop + (availHeight - height) / 2)
: top ?? 100;
const baseFeatures = [
`width=${Math.floor(width)}`,
`height=${Math.floor(height)}`,
`left=${Math.floor(finalLeft)}`,
`top=${Math.floor(finalTop)}`,
];
const featureFlags = features ?? {
toolbar: false,
menubar: false,
scrollbars: true,
resizable: true,
location: false,
status: false,
};
Object.entries(featureFlags).forEach(([k, v]) =>
baseFeatures.push(`${k}=${v ? "yes" : "no"}`)
);
const newWin = window.open("", "_blank", baseFeatures.join(","));
if (!newWin) {
console.warn("Popup blocked or failed to open window.");
return;
}
if (noopener) {
try {
newWin.opener = null;
} catch {}
}
newWin.document.open();
newWin.document.write(`<!doctype html>
<html data-theme=${theme}>
<head>
<meta charset="utf-8" />
<title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body style="margin:0;"></body>
</html>`);
newWin.document.close();
if (copyStyles) {
const head = newWin.document.head;
Array.from(document.styleSheets).forEach((styleSheet) => {
try {
if ((styleSheet as CSSStyleSheet).cssRules) {
const newStyleEl = newWin.document.createElement("style");
const rules = Array.from(
(styleSheet as CSSStyleSheet).cssRules
).map((r) => r.cssText);
newStyleEl.appendChild(
newWin.document.createTextNode(rules.join("\n"))
);
head.appendChild(newStyleEl);
}
} catch {
const ownerNode = styleSheet.ownerNode as HTMLElement | null;
if (ownerNode && ownerNode.tagName === "LINK") {
const link = ownerNode as HTMLLinkElement;
const newLink = newWin.document.createElement("link");
newLink.rel = link.rel;
newLink.href = link.href;
newLink.media = link.media;
head.appendChild(newLink);
}
}
});
}
const container = newWin.document.createElement("div");
if (className) container.className = className;
newWin.document.body.appendChild(container);
newWin.document.title = title;
// Handle child window close
const handleChildUnload = () => {
onClose?.();
};
newWin.addEventListener("beforeunload", handleChildUnload);
// 👇 Handle parent refresh/close → auto close child
const handleParentUnload = () => {
try {
newWin.close();
} catch {}
};
window.addEventListener("beforeunload", handleParentUnload);
childWindowRef.current = newWin;
containerElRef.current = container;
setMounted(true);
return () => {
newWin.removeEventListener("beforeunload", handleChildUnload);
window.removeEventListener("beforeunload", handleParentUnload);
try {
newWin.close();
} catch {}
childWindowRef.current = null;
containerElRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const w = childWindowRef.current;
if (w && !w.closed) {
w.document.title = title;
}
}, [title]);
if (!mounted || !containerElRef.current) return null;
return createPortal(children, containerElRef.current);
};

View File

@@ -1,70 +1,31 @@
// LogList.tsx
import React, { useEffect, useState } from "react";
import { LogListIcon, CloseIcon } from "../../icons/ExportCommonIcons"; // Adjust path as needed
import { useLogger } from "./LoggerContext";
import {
LogListIcon,
CloseIcon,
ExpandIcon2,
} from "../../icons/ExportCommonIcons"; // Adjust path as needed
import { LogEntry, useLogger } from "./LoggerContext";
import { GetLogIcon } from "../../footer/getLogIcons";
import { RenderInNewWindow } from "../../templates/CreateNewWindow";
const LogList: React.FC = () => {
const {
logs,
clear,
setIsLogListVisible,
isLogListVisible,
// --- Logs Component ---
type LogsProps = {
selectedTab: "all" | "info" | "warning" | "error";
setSelectedTab: (tab: "all" | "info" | "warning" | "error") => void;
clear: () => void;
filteredLogs: LogEntry[];
formatTimestamp: (date: Date) => string;
};
const Logs: React.FC<LogsProps> = ({
selectedTab,
setSelectedTab,
} = useLogger();
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
const filteredLogs =
selectedTab === "all"
? [...logs].reverse()
: [...logs].filter((log) => log.type === selectedTab).reverse();
useEffect(() => {
if (isLogListVisible && logs.length > 0) {
const lastLog = logs[logs.length - 1];
const validTypes = ["all", "info", "warning", "error"];
if (validTypes.includes(lastLog.type)) {
setSelectedTab(lastLog.type);
} else {
setSelectedTab("all");
}
}
}, [isLogListVisible]);
clear,
filteredLogs,
formatTimestamp,
}) => {
return (
// eslint-disable-next-line
<div
className="log-list-container"
onClick={() => setIsLogListVisible(false)}
>
{/* eslint-disable-next-line */}
<div
className="log-list-wrapper"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="log-header">
<div className="log-header-wrapper">
<div className="icon">
<LogListIcon />
</div>
<div className="head">Log List</div>
</div>
<button
id="close-btn"
title="close-btn"
className="close"
onClick={() => setIsLogListVisible(false)}
>
<CloseIcon />
</button>
</div>
{/* Tabs */}
<>
<div className="log-nav-container">
<div className="log-nav-wrapper">
{["all", "info", "warning", "error"].map((type) => (
@@ -109,8 +70,113 @@ const LogList: React.FC = () => {
</div>
)}
</div>
</>
);
};
// --- LogList Component ---
const LogList: React.FC = () => {
const {
logs,
clear,
setIsLogListVisible,
isLogListVisible,
selectedTab,
setSelectedTab,
} = useLogger();
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
const [open, setOpen] = useState(false);
const filteredLogs =
selectedTab === "all"
? [...logs].reverse()
: [...logs].filter((log) => log.type === selectedTab).reverse();
useEffect(() => {
if (isLogListVisible && logs.length > 0) {
const lastLog = logs[logs.length - 1];
const validTypes = ["all", "info", "warning", "error"];
if (validTypes.includes(lastLog.type)) {
setSelectedTab(lastLog.type as any);
} else {
setSelectedTab("all");
}
}
// eslint-disable-next-line
}, [isLogListVisible]);
return (
<>
{!open ? (
<div
className="log-list-container"
onClick={() => setIsLogListVisible(false)}
>
{/* eslint-disable-next-line */}
<div
className="log-list-wrapper"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="log-header">
<div className="log-header-wrapper">
<div className="icon">
<LogListIcon />
</div>
<div className="head">Log List</div>
</div>
<div className="action-buttons-container">
<button
id="expand-log-btn"
title="open in new tab"
className="expand-btn"
onClick={() => {
setOpen(true);
}}
>
<ExpandIcon2 />
</button>
<button
id="close-btn"
title="close"
className="close"
onClick={() => setIsLogListVisible(false)}
>
<CloseIcon />
</button>
</div>
</div>
{/* Logs Section */}
<Logs
selectedTab={selectedTab as any}
setSelectedTab={setSelectedTab as any}
clear={clear}
filteredLogs={filteredLogs}
formatTimestamp={formatTimestamp}
/>
</div>
</div>
) : (
<RenderInNewWindow
title="Log list"
theme={localStorage.getItem("theme")}
onClose={() => setOpen(false)}
>
<div className="log-list-new-window-wrapper">
<Logs
selectedTab={selectedTab as any}
setSelectedTab={setSelectedTab as any}
clear={clear}
filteredLogs={filteredLogs}
formatTimestamp={formatTimestamp}
/>
</div>
</RenderInNewWindow>
)}
</>
);
};

View File

@@ -1,53 +1,6 @@
@use "../../abstracts/variables.scss" as *;
@use "../../abstracts/mixins.scss" as *;
.log-list-container {
width: 100vw;
height: 100vh;
background: var(--background-color-secondary);
@include flex-center;
.log-list-wrapper {
height: 60%;
min-width: 55%;
z-index: 5;
background: var(--background-color);
padding: 14px 12px;
border-radius: #{$border-radius-xlarge};
display: flex;
flex-direction: column;
gap: 12px;
backdrop-filter: blur(50px);
outline: 1px solid var(--border-color);
.log-header {
display: flex;
justify-content: space-between;
.log-header-wrapper {
display: flex;
align-items: center;
gap: 6px;
.icon {
@include flex-center;
scale: 0.8;
}
}
.close {
@include flex-center;
height: 28px;
width: 28px;
cursor: pointer;
border-radius: #{$border-radius-medium};
svg {
scale: 1.6;
}
&:hover {
background: var(--background-color);
}
}
}
.log-nav-container {
@include flex-space-between;
align-items: flex-end;
@@ -121,11 +74,74 @@
background: var(--background-color);
}
}
}
.no-log {
padding: 20px;
text-align: center;
color: var(--text-color);
}
}
.log-list-container {
width: 100vw;
height: 100vh;
background: var(--background-color-secondary);
@include flex-center;
.log-list-wrapper {
height: 60%;
min-width: 55%;
z-index: 5;
background: var(--background-color);
padding: 14px 12px;
border-radius: #{$border-radius-xlarge};
display: flex;
flex-direction: column;
gap: 12px;
backdrop-filter: blur(50px);
outline: 1px solid var(--border-color);
.log-header {
display: flex;
justify-content: space-between;
.log-header-wrapper {
display: flex;
align-items: center;
gap: 6px;
.icon {
@include flex-center;
scale: 0.8;
}
}
.action-buttons-container {
display: flex;
align-items: center;
gap: 4px;
.close,
.expand-btn {
@include flex-center;
height: 28px;
width: 28px;
cursor: pointer;
border-radius: #{$border-radius-medium};
svg {
scale: 1.6;
}
&:hover {
background: var(--background-color);
border: 1px solid #ffffff29;
}
}
}
}
}
}
.log-list-new-window-wrapper{
padding: 10px;
background: var(--background-color-solid);
height: 100vh;
.log-nav-container{
margin-bottom: 10px;
}
}