"use client"; import React, { useState, useRef, useEffect, lazy, Suspense } from "react"; import { createPortal } from "react-dom"; import { LeafNode, PanelId, PANEL_LABELS, PANEL_SECTIONS } from "./types"; import { IconX, IconLayoutColumns, IconLayoutRows, IconRefresh, } from "@tabler/icons-react"; const DashboardPanel = lazy(() => import("../panels/DashboardPanel")); const AnalyticsPanel = lazy(() => import("../panels/AnalyticsPanel")); function PanelContent({ panelId, isAuthed }: { panelId: PanelId; isAuthed: boolean }) { return ( }> {panelId === "dashboard" && } {panelId === "analytics-line" && } {panelId === "analytics-bar" && } {panelId === "analytics-candle" && } ); } // ── View picker portal ──────────────────────────────────────────────────────── type SplitDir = "right" | "down"; function ViewPickerPortal({ anchorRef, mode, currentView, onPick, onClose, }: { anchorRef: React.RefObject; mode: "change" | SplitDir; currentView: PanelId; onPick: (panelId: PanelId) => void; onClose: () => void; }) { const menuRef = useRef(null); const [pos, setPos] = useState({ top: 0, left: 0 }); useEffect(() => { const rect = anchorRef.current?.getBoundingClientRect(); if (rect) setPos({ top: rect.bottom + 6, left: rect.left }); }, []); // Clamp to viewport after mount useEffect(() => { if (!menuRef.current) return; const r = menuRef.current.getBoundingClientRect(); const pad = 8; let { left, top } = pos; if (r.right > window.innerWidth - pad) left = window.innerWidth - r.width - pad; if (r.left < pad) left = pad; if (r.bottom > window.innerHeight - pad) top = window.innerHeight - r.height - pad; if (left !== pos.left || top !== pos.top) setPos({ top, left }); }, [pos]); useEffect(() => { const handler = (e: MouseEvent) => { if (!menuRef.current?.contains(e.target as Node) && !anchorRef.current?.contains(e.target as Node)) onClose(); }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [onClose]); const title = mode === "change" ? "Change View" : mode === "right" ? "Tile Right" : "Tile Down"; // Dashboard entry + sections const dashboardActive = currentView === "dashboard"; return createPortal( {title} {/* Dashboard standalone */} { onPick("dashboard"); onClose(); }} className={[ "w-[calc(100%-8px)] mx-[4px] flex items-center gap-2.5 px-2.5 py-[5px] rounded-lg text-left text-xs transition-colors cursor-pointer", dashboardActive ? "bg-blue/12 border border-blue/30 text-blue font-medium" : "text-foreground hover:bg-secondary/60 border border-transparent", ].join(" ")} > D Dashboard {/* Power Analytics section */} {PANEL_SECTIONS.map((section, si) => ( {section.label} {section.items.map(({ panelId, label }) => { const active = panelId === currentView; return ( { onPick(panelId); onClose(); }} className={[ "w-[calc(100%-8px)] mx-[4px] flex items-center gap-2.5 px-2.5 py-[5px] rounded-lg text-left text-xs transition-colors cursor-pointer", active ? "bg-blue/12 border border-blue/30 text-blue font-medium" : "text-foreground hover:bg-secondary/60 border border-transparent", ].join(" ")} > {label.charAt(0)} {label} ); })} ))} , document.body, ); } // ── Window controls pill ────────────────────────────────────────────────────── type MenuMode = "change" | SplitDir; function WindowControls({ paneId, currentView, canClose, onClose, onSplit, onChangeView, }: { paneId: string; currentView: PanelId; canClose: boolean; onClose: (id: string) => void; onSplit: (leafId: string, dir: "h" | "v", newFirst: boolean, panelId: PanelId) => void; onChangeView: (panelId: PanelId) => void; }) { const [menu, setMenu] = useState(null); const [pillHovered, setPillHovered] = useState(false); const changeRef = useRef(null); const rightRef = useRef(null); const downRef = useRef(null); const handlePick = (panelId: PanelId) => { if (menu === "change") onChangeView(panelId); else if (menu === "right") onSplit(paneId, "h", false, panelId); else if (menu === "down") onSplit(paneId, "v", false, panelId); setMenu(null); }; const anchorRef = menu === "change" ? changeRef : menu === "right" ? rightRef : downRef; const BTN = "w-[26px] h-[26px] flex items-center justify-center rounded-full transition-colors cursor-pointer text-foreground-sec hover:text-foreground"; return ( <> setPillHovered(true)} onMouseLeave={() => setPillHovered(false)} > {canClose && ( { e.stopPropagation(); onClose(paneId); }} className={`${BTN} hover:text-red-400 hover:bg-red-500/10`} title="Close" > )} { e.stopPropagation(); setMenu(m => m === "change" ? null : "change"); }} className={`${BTN} ${menu === "change" ? "text-blue bg-blue/10" : "hover:bg-secondary/60"}`} title="Change view" > { e.stopPropagation(); setMenu(m => m === "right" ? null : "right"); }} className={`${BTN} ${menu === "right" ? "text-blue bg-blue/10" : "hover:bg-secondary/60"}`} title="Tile right" > { e.stopPropagation(); setMenu(m => m === "down" ? null : "down"); }} className={`${BTN} ${menu === "down" ? "text-blue bg-blue/10" : "hover:bg-secondary/60"}`} title="Tile down" > {menu && ( setMenu(null)} /> )} > ); } // ── WindowPane ──────────────────────────────────────────────────────────────── export default function WindowPane({ node, isFocused, canClose, onSplit, onClose, isAuthed, }: { node: LeafNode; isFocused: boolean; canClose: boolean; onSplit: (leafId: string, dir: "h" | "v", newFirst: boolean, panelId: PanelId) => void; onClose: (leafId: string) => void; isAuthed: boolean; }) { const [hovered, setHovered] = useState(false); const handleChangeView = (panelId: PanelId) => { import("@/stores/windowStore").then(({ requestViewChange }) => requestViewChange(panelId)); }; return ( setHovered(true)} onMouseLeave={() => setHovered(false)} > {/* Title bar */} {PANEL_LABELS[node.panelId]} {/* Content */} {/* Floating controls pill */} ); }
{title}
{section.label}