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,9 +1,80 @@
// 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";
// --- 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,
clear,
filteredLogs,
formatTimestamp,
}) => {
return (
<>
<div className="log-nav-container">
<div className="log-nav-wrapper">
{["all", "info", "warning", "error"].map((type) => (
<button
id="log-type"
title="log-type"
key={type}
className={`log-nav ${selectedTab === type ? "active" : ""}`}
onClick={() => setSelectedTab(type as any)}
>
{`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`}
</button>
))}
</div>
<button
id="clean-btn"
title="clear-btn"
className="clear-button"
onClick={clear}
>
clear
</button>
</div>
{/* Log Entries */}
<div className="log-entry-wrapper">
{filteredLogs.length > 0 ? (
filteredLogs.map((log) => (
<div key={log.id} className={`log-entry ${log.type}`}>
<div className="log-icon">{GetLogIcon(log.type)}</div>
<div className="log-entry-message-container">
<div className="log-entry-message">{log.message}</div>
<div className="message-time">
{formatTimestamp(log.timestamp)}
</div>
</div>
</div>
))
) : (
<div className="no-log">
There are no logs to display at the moment.
</div>
)}
</div>
</>
);
};
// --- LogList Component ---
const LogList: React.FC = () => {
const {
logs,
@@ -15,6 +86,7 @@ const LogList: React.FC = () => {
} = useLogger();
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
const [open, setOpen] = useState(false);
const filteredLogs =
selectedTab === "all"
@@ -25,92 +97,86 @@ const LogList: React.FC = () => {
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);
setSelectedTab(lastLog.type as any);
} else {
setSelectedTab("all");
}
}
// eslint-disable-next-line
}, [isLogListVisible]);
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)}
<>
{!open ? (
<div
className="log-list-container"
onClick={() => setIsLogListVisible(false)}
>
{/* eslint-disable-next-line */}
<div
className="log-list-wrapper"
onClick={(e) => {
e.stopPropagation();
}}
>
<CloseIcon />
</button>
</div>
{/* Tabs */}
<div className="log-nav-container">
<div className="log-nav-wrapper">
{["all", "info", "warning", "error"].map((type) => (
<button
id="log-type"
title="log-type"
key={type}
className={`log-nav ${selectedTab === type ? "active" : ""}`}
onClick={() => setSelectedTab(type as any)}
>
{`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`}
</button>
))}
</div>
<button
id="clean-btn"
title="clear-btn"
className="clear-button"
onClick={clear}
>
clear
</button>
</div>
{/* Log Entries */}
<div className="log-entry-wrapper">
{filteredLogs.length > 0 ? (
filteredLogs.map((log) => (
<div key={log.id} className={`log-entry ${log.type}`}>
<div className="log-icon">{GetLogIcon(log.type)}</div>
<div className="log-entry-message-container">
<div className="log-entry-message">{log.message}</div>
<div className="message-time">
{formatTimestamp(log.timestamp)}
</div>
<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 className="no-log">
There are no logs to display at the moment.
</div>
)}
{/* Logs Section */}
<Logs
selectedTab={selectedTab as any}
setSelectedTab={setSelectedTab as any}
clear={clear}
filteredLogs={filteredLogs}
formatTimestamp={formatTimestamp}
/>
</div>
</div>
</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,6 +1,86 @@
@use "../../abstracts/variables.scss" as *;
@use "../../abstracts/mixins.scss" as *;
.log-nav-container {
@include flex-space-between;
align-items: flex-end;
.log-nav-wrapper {
display: flex;
gap: 6px;
.log-nav {
padding: 8px 16px;
border-radius: #{$border-radius-extra-large};
}
.log-nav.active {
background-color: var(--background-color-accent);
color: var(--text-button-color);
}
}
.clear-button {
margin: 0 6px;
padding: 4px 12px;
color: var(--text-disabled);
border-radius: #{$border-radius-large};
&:hover {
font-weight: 300;
color: var(--text-color);
background: var(--background-color-solid-gradient);
}
}
}
.log-entry-wrapper {
height: calc(100% - 80px);
display: flex;
flex-direction: column;
gap: 4px;
background: var(--background-color);
padding: 10px;
border-radius: #{$border-radius-xlarge};
outline: 1px solid var(--border-color);
outline-offset: -1px;
overflow: auto;
.log-entry {
padding: 4px;
border-radius: #{$border-radius-small};
font-size: var(--font-size-small);
display: flex;
.log-icon {
height: 24px;
width: 24px;
@include flex-center;
}
.log-entry-message-container {
@include flex-space-between;
gap: 12px;
width: 100%;
.message-time {
font-size: var(--font-size-tiny);
font-weight: 300;
opacity: 0.8;
text-wrap: nowrap;
// height: 100%;
}
.log-entry-message {
width: 100%;
}
}
&:nth-child(odd) {
background: var(--background-color);
}
}
.no-log {
padding: 20px;
text-align: center;
color: var(--text-color);
}
}
.log-list-container {
width: 100vw;
height: 100vh;
@@ -33,99 +113,35 @@
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;
.log-nav-wrapper {
.action-buttons-container {
display: flex;
gap: 6px;
.log-nav {
padding: 8px 16px;
border-radius: #{$border-radius-extra-large};
}
.log-nav.active {
background-color: var(--background-color-accent);
color: var(--text-button-color);
}
}
.clear-button{
margin: 0 6px;
padding: 4px 12px;
color: var(--text-disabled);
border-radius: #{$border-radius-large};
&:hover{
font-weight: 300;
color: var(--text-color);
background: var(--background-color-solid-gradient);
}
}
}
.log-entry-wrapper {
height: calc(100% - 80px);
display: flex;
flex-direction: column;
gap: 4px;
background: var(--background-color);
padding: 10px;
border-radius: #{$border-radius-xlarge};
outline: 1px solid var(--border-color);
outline-offset: -1px;
overflow: auto;
.log-entry {
padding: 4px;
border-radius: #{$border-radius-small};
font-size: var(--font-size-small);
display: flex;
.log-icon {
height: 24px;
width: 24px;
align-items: center;
gap: 4px;
.close,
.expand-btn {
@include flex-center;
}
.log-entry-message-container {
@include flex-space-between;
gap: 12px;
width: 100%;
.message-time {
font-size: var(--font-size-tiny);
font-weight: 300;
opacity: 0.8;
text-wrap: nowrap;
// height: 100%;
height: 28px;
width: 28px;
cursor: pointer;
border-radius: #{$border-radius-medium};
svg {
scale: 1.6;
}
.log-entry-message {
width: 100%;
&:hover {
background: var(--background-color);
border: 1px solid #ffffff29;
}
}
&:nth-child(odd) {
background: var(--background-color);
}
}
}
.no-log{
padding: 20px;
text-align: center;
color: var(--text-color);
}
}
}
.log-list-new-window-wrapper{
padding: 10px;
background: var(--background-color-solid);
height: 100vh;
.log-nav-container{
margin-bottom: 10px;
}
}