import { useMemo, type CSSProperties, type HTMLAttributes, type MouseEvent, type ReactNode, type SVGProps } from "react"; import { useTranslation } from "../../i18n/index"; import { cn } from "../../app/renderer/lib/utils"; import { NodeResizeHandles } from "./NodeResizeHandles"; import { WindowLayoutMenuButton } from "../workspaceDesktop/view/WindowLayoutMenuButton"; import { useNodeFrameResize, type ResizeEdges } from "../../utils/nodeFrameResize"; import type { NodeFrame, Point, WorkspaceNodeKind } from "../../types"; import type { DesktopSize } from "../workspaceDesktop/types"; interface WorkspaceNodeWindowInteractionOptions { normalizeViewport?: boolean; selectNode?: boolean; shiftKey?: boolean; } interface WorkspaceNodeWindowRenderFrame { position: Point; size: { width: number; height: number; }; } type WorkspaceNodeWindowStyle = CSSProperties & { "--node-header-padding-x"?: string; "--node-header-traffic-light-title-offset"?: string; }; export interface WorkspaceNodeWindowProps { nodeId: string; kind: WorkspaceNodeKind; title: string; position: Point; width: number; height: number; desktopSize: DesktopSize; minSize: { width: number; height: number }; className?: string; bodyClassName?: string; rootProps?: Omit< HTMLAttributes, "className" | "children" | "style" > & Record<`data-${string}`, string | number | boolean | undefined>; sizeStyle?: CSSProperties; appearance?: "window" | "embedded"; children: ReactNode | ((frame: WorkspaceNodeWindowRenderFrame) => ReactNode); customHeader?: ReactNode; titleIcon?: ReactNode; titleAccessory?: ReactNode; headerAccessory?: ReactNode; controlStartAccessory?: ReactNode; hideHeader?: boolean; onClose: () => void; onResize: (frame: NodeFrame) => void; onInteractionStart?: ( options?: WorkspaceNodeWindowInteractionOptions ) => void; isMaximized?: boolean; isMuted?: boolean; hideMaximizeButton?: boolean; onMinimize?: () => void; onToggleMaximize?: () => void; resizeTestIdPrefix?: string; resizeHandlePointerDown?: ( edges: ResizeEdges ) => (event: React.PointerEvent) => void; } export function WorkspaceNodeWindow({ nodeId, kind, title, position, width, height, desktopSize, minSize, className, bodyClassName, rootProps, sizeStyle, appearance = "window", children, customHeader, titleIcon, titleAccessory, headerAccessory, controlStartAccessory, hideHeader = false, onClose, onResize, onInteractionStart, isMaximized = false, isMuted = false, hideMaximizeButton = false, onMinimize, onToggleMaximize, resizeTestIdPrefix = `${kind}-node-resizer`, resizeHandlePointerDown }: WorkspaceNodeWindowProps): React.JSX.Element { "use memo"; const { t } = useTranslation(); const { onClickCapture, ...restRootProps } = rootProps ?? {}; const { draftFrame, handleResizePointerDown } = useNodeFrameResize({ position, width, height, minSize, onResize }); const renderedFrame = draftFrame ?? { position, size: { width, height } }; const style = useMemo( () => ({ width: renderedFrame.size.width, height: renderedFrame.size.height, transform: renderedFrame.position.x !== position.x || renderedFrame.position.y !== position.y ? `translate(${renderedFrame.position.x - position.x}px, ${renderedFrame.position.y - position.y}px)` : undefined }), [ position.x, position.y, renderedFrame.position.x, renderedFrame.position.y, renderedFrame.size.height, renderedFrame.size.width ] ); const resolvedStyle = sizeStyle ?? style; const renderedChildren = typeof children === "function" ? children(renderedFrame) : children; const rootStyle: WorkspaceNodeWindowStyle = appearance === "embedded" ? { "--node-header-padding-x": "16px", "--node-header-traffic-light-title-offset": "64px", ...resolvedStyle, width: "100%", height: "100%", transform: undefined, background: "transparent", border: "0", boxShadow: "none", backdropFilter: "none", WebkitBackdropFilter: "none" } : { "--node-header-padding-x": "16px", "--node-header-traffic-light-title-offset": "64px", ...resolvedStyle, background: "transparent", border: "1px solid var(--node-window-border)", boxShadow: "var(--window-drop-shadow)", backdropFilter: "var(--node-window-backdrop-filter)", WebkitBackdropFilter: "var(--node-window-backdrop-filter)" }; const resolvedResizeHandlePointerDown = resizeHandlePointerDown ?? handleResizePointerDown; return (
{ if (event.button !== 0 || !(event.target instanceof Element)) { return; } if (event.target.closest(".nodrag")) { return; } event.stopPropagation(); onInteractionStart?.({ shiftKey: event.shiftKey }); }) } > {hideHeader ? null : customHeader ? ( customHeader ) : (
{ if ( event.target instanceof Element && event.target.closest(".nodrag") ) { return; } event.stopPropagation(); onToggleMaximize?.(); }} >
{onMinimize ? ( ) : null} {onToggleMaximize && !hideMaximizeButton ? ( ) : null}
{titleIcon ? ( {titleIcon} ) : null} {title} {titleAccessory ? ( {titleAccessory} ) : null}
{headerAccessory ? (
{headerAccessory}
) : null}
{controlStartAccessory}
)}
{renderedChildren}
{!isMaximized ? ( ) : null}
); } function WorkspaceNodeTrafficLightButton({ ariaLabel, onClick, pressed, testId, tone }: { ariaLabel: string; onClick: () => void; pressed?: boolean; testId?: string; tone: "close" | "maximize" | "minimize"; }): React.JSX.Element { const handleClick = (event: MouseEvent): void => { event.stopPropagation(); onClick(); }; const iconName = tone === "maximize" ? (pressed ? "unfullscreen" : "fullscreen") : tone; return ( ); } function WorkspaceNodeWindowTrafficLightIcon({ className, iconName, ...props }: SVGProps & { iconName: "close" | "fullscreen" | "minimize" | "unfullscreen"; }): React.JSX.Element { return ( ); } const trafficLightIconPathByName = { close: "M16.9395 4.93953C17.5253 4.35374 18.4748 4.35374 19.0606 4.93953C19.6463 5.52532 19.6464 6.47486 19.0606 7.06062L14.1212 12.0001L19.0606 16.9395C19.6463 17.5253 19.6464 18.4749 19.0606 19.0606C18.4749 19.6464 17.5253 19.6463 16.9395 19.0606L12.0001 14.1212L7.06062 19.0606C6.47486 19.6464 5.52532 19.6463 4.93953 19.0606C4.35374 18.4748 4.35374 17.5253 4.93953 16.9395L9.87898 12.0001L4.93953 7.06062C4.35374 6.47484 4.35374 5.52532 4.93953 4.93953C5.52532 4.35374 6.47484 4.35374 7.06062 4.93953L12.0001 9.87898L16.9395 4.93953Z", fullscreen: "M18.1465 7.85352C18.4615 7.53861 18.9999 7.76165 19 8.20703V18.5C19 18.7761 18.7761 19 18.5 19H8.20703C7.76165 18.9999 7.53861 18.4615 7.85352 18.1465L18.1465 7.85352ZM15.793 5C16.2384 5.00006 16.4614 5.53855 16.1465 5.85352L5.85352 16.1465C5.53855 16.4614 5.00006 16.2384 5 15.793V5.5C5 5.22386 5.22386 5 5.5 5H15.793Z", minimize: "M5 10.5H19C19.8284 10.5 20.5 11.1716 20.5 12C20.5 12.8284 19.8284 13.5 19 13.5H5C4.17157 13.5 3.5 12.8284 3.5 12C3.5 11.1716 4.17157 10.5 5 10.5Z", unfullscreen: "M20.793 12C21.2384 12.0001 21.4614 12.5386 21.1465 12.8536L12.8536 21.1465C12.5386 21.4614 12.0001 21.2384 12.0001 20.793V12.5C12.0001 12.2239 12.2239 12 12.5001 12H20.793ZM11.1465 2.85356C11.4615 2.53864 12 2.76166 12.0001 3.20708V11.5C12 11.7761 11.7762 12 11.5001 12H3.20708C2.76166 12 2.53864 11.4615 2.85357 11.1465L11.1465 2.85356Z" } as const;