"use client"; import React, { useState, useCallback, useRef, useEffect, createContext, useContext, memo } from "react"; import { TileNode, LeafNode, ContainerNode, PanelId } from "./types"; import { splitLeaf, closeLeaf, updatePanelId, patchSizes, getFirstLeafId, countLeaves, getLeafPanel, } from "./treeUtils"; import { setFocusedContext, subscribeViewChange } from "@/stores/windowStore"; import WindowPane from "./WindowPane"; // ── Persistence ─────────────────────────────────────────────────────────────── const STORAGE_KEY = "wm-layout-v3"; const DEFAULT_TREE: TileNode = { type: "leaf", id: "root", panelId: "dashboard" }; function persist(t: TileNode) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(t)); } catch {} } // ── Internal context ────────────────────────────────────────────────────────── interface WMCtx { focusedId: string | null; totalPanes: number; isAuthed: boolean; onFocus: (paneId: string, panelId: PanelId) => void; onSplit: (leafId: string, dir: "h" | "v", newFirst: boolean, panelId: PanelId) => void; onClose: (leafId: string) => void; onResizeContainer: (containerId: string, sizes: number[]) => void; } const WMContext = createContext(null!); const useWM = () => useContext(WMContext); // ── Resize handle ───────────────────────────────────────────────────────────── interface ResizeHandleProps { containerId: string; index: number; dir: "h" | "v"; sizes: number[]; containerRef: React.RefObject; } function ResizeHandle({ containerId, index, dir, sizes, containerRef }: ResizeHandleProps) { const { onResizeContainer } = useWM(); const isCol = dir === "h"; const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault(); const container = containerRef.current; if (!container) return; const rect = container.getBoundingClientRect(); const totalPx = isCol ? rect.width : rect.height; const totalRatio = sizes.reduce((a, b) => a + b, 0); const startPos = isCol ? e.clientX : e.clientY; const startSizes = [...sizes]; const onMove = (mv: MouseEvent) => { const delta = (isCol ? mv.clientX : mv.clientY) - startPos; const deltaRatio = (delta / totalPx) * totalRatio; const minRatio = totalRatio * 0.08; const next = [...startSizes]; next[index] = Math.max(minRatio, startSizes[index] + deltaRatio); next[index + 1] = Math.max(minRatio, startSizes[index + 1] - deltaRatio); if (next[index] < minRatio) { next[index] = minRatio; next[index + 1] = startSizes[index] + startSizes[index + 1] - minRatio; } else if (next[index + 1] < minRatio) { next[index + 1] = minRatio; next[index] = startSizes[index] + startSizes[index + 1] - minRatio; } onResizeContainer(containerId, next); }; const onUp = () => { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; document.body.style.cursor = isCol ? "col-resize" : "row-resize"; document.body.style.userSelect = "none"; document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); }, [containerId, index, dir, sizes, containerRef, onResizeContainer, isCol]); return (
); } // ── Recursive tree renderer ─────────────────────────────────────────────────── const LeafCard = memo(function LeafCard({ leaf }: { leaf: LeafNode }) { const { focusedId, totalPanes, isAuthed, onFocus, onSplit, onClose } = useWM(); const isFocused = leaf.id === focusedId; return (
1 && isFocused ? "border-blue/60 shadow-[0_0_0_1px_rgba(66,140,226,0.2)]" : "border-secondary/60", ].join(" ")} onClick={() => onFocus(leaf.id, leaf.panelId)} > 1} onSplit={onSplit} onClose={onClose} isAuthed={isAuthed} />
); }); function RenderTree({ node }: { node: TileNode }) { const containerRef = useRef(null); const { focusedId } = useWM(); if (node.type === "leaf") return ; const container = node as ContainerNode; const isCol = container.dir === "h"; return (
{container.children.map((child, i) => (
{i < container.children.length - 1 && ( )}
))}
); } // ── Root ────────────────────────────────────────────────────────────────────── export default function WindowManager({ isAuthed }: { isAuthed: boolean }) { const [tree, setTree] = useState(DEFAULT_TREE); const [focusedId, setFocusedId] = useState(null); const [ready, setReady] = useState(false); // Refs for stale-closure-safe callbacks const treeRef = useRef(tree); const focusedIdRef = useRef(focusedId); treeRef.current = tree; focusedIdRef.current = focusedId; // Load persisted layout useEffect(() => { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) setTree(JSON.parse(saved)); } catch {} setReady(true); }, []); // Sync focused context to store whenever tree or focus changes useEffect(() => { const id = focusedId ?? getFirstLeafId(tree); const panelId = getLeafPanel(tree, id); if (id && panelId) setFocusedContext(id, panelId); }, [tree, focusedId]); // Listen for sidebar view-change requests useEffect(() => { const unsub = subscribeViewChange((panelId) => { const id = focusedIdRef.current ?? getFirstLeafId(treeRef.current); setTree((prev) => { const next = updatePanelId(prev, id, panelId); persist(next); return next; }); // Update store immediately so sidebar highlight updates setFocusedContext(id, panelId); }); return unsub; }, []); const onFocus = useCallback((paneId: string, panelId: PanelId) => { setFocusedId(paneId); setFocusedContext(paneId, panelId); }, []); const onSplit = useCallback((leafId: string, dir: "h" | "v", newFirst: boolean, panelId: PanelId) => { setTree((prev) => { const next = splitLeaf(prev, leafId, dir, newFirst, panelId); persist(next); return next; }); }, []); const onClose = useCallback((leafId: string) => { setTree((prev) => { const next = closeLeaf(prev, leafId); if (!next) return prev; persist(next); // If closed pane was focused, move focus to first leaf if (focusedIdRef.current === leafId) { const firstId = getFirstLeafId(next); const panelId = getLeafPanel(next, firstId); setFocusedId(firstId); if (panelId) setFocusedContext(firstId, panelId); } return next; }); }, []); const onResizeContainer = useCallback((containerId: string, sizes: number[]) => { setTree((prev) => { const next = patchSizes(prev, containerId, sizes); persist(next); return next; }); }, []); const total = countLeaves(tree); const ctx: WMCtx = { focusedId: focusedId ?? (ready ? getFirstLeafId(tree) : null), totalPanes: total, isAuthed, onFocus, onSplit, onClose, onResizeContainer, }; if (!ready) return null; return (
); }