// Modular dockable panel system - handles drag, lock, snap-back, window-shade // Procedural style: Functions, not classes import { saveSettings, loadSettings, getDefaultSettings, } from "./settings-manager"; interface PanelDockState { panels: Map; dragState: DragState | null; dockZones: DockZone[]; } interface DragState { panelId: string; startX: number; startY: number; currentX: number; currentY: number; isLocked: boolean; } interface DockZone { side: "left" | "right"; x: number; width: number; height: number; } const state: PanelDockState = { panels: new Map(), dragState: null, dockZones: [ { side: "left", x: 0, width: 400, height: window.innerHeight }, { side: "right", x: window.innerWidth - 400, width: 400, height: window.innerHeight, }, ], }; // Initialize all panels from settings function initializePanelDockSystem(): void { const settings = loadSettings(); for (const [panelId, panelState] of Object.entries(settings.panel_layout)) { registerPanel(panelId, panelState); } setupDragHandlers(); setupWindowShadeHandlers(); setupLockHandlers(); } // Register a panel with the dock system function registerPanel(panelId: string, panelState: PanelState): void { state.panels.set(panelId, panelState); applyPanelState(panelId, panelState); } // Apply panel state to DOM function applyPanelState(panelId: string, panelState: PanelState): void { const panel = document.querySelector(`[data-panel="${panelId}"]`); if (!panel) return; const container = panel.parentElement; if (!container) return; // Apply side positioning if (panelState.side === "left") { container.style.left = "0"; container.style.right = "auto"; } else if (panelState.side === "right") { container.style.right = "0"; container.style.left = "auto"; } else { container.style.left = "-9999px"; } // Apply width panel.style.width = `${panelState.width_px}px`; // Apply collapsed (window-shade) state if (panelState.collapsed) { panel.classList.add("panel-collapsed"); panel.querySelector(".panel-content")?.classList.add("hidden"); } else { panel.classList.remove("panel-collapsed"); panel.querySelector(".panel-content")?.classList.remove("hidden"); } // Apply lock state const lockBtn = panel.querySelector(".panel-lock"); if (lockBtn) { lockBtn.textContent = panelState.locked ? "🔒" : "🔓"; } } // Setup mouse/touch drag handlers function setupDragHandlers(): void { document .querySelectorAll(".dockable-panel .panel-header") .forEach((header) => { header.addEventListener("mousedown", handleDragStart); header.addEventListener("touchstart", handleDragStart, { passive: false, }); }); document.addEventListener("mousemove", handleDragMove); document.addEventListener("touchmove", handleDragMove, { passive: false }); document.addEventListener("mouseup", handleDragEnd); document.addEventListener("touchend", handleDragEnd); } function handleDragStart(e: MouseEvent | TouchEvent): void { const header = e.target.closest(".panel-header") as HTMLElement; const panel = header?.closest(".dockable-panel") as HTMLElement; if (!panel) return; const panelId = panel.dataset.panel; const panelState = state.panels.get(panelId); // Check if locked if (panelState?.locked) return; const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; state.dragState = { panelId: panelId!, startX: clientX, startY: clientY, currentX: clientX, currentY: clientY, isLocked: panelState?.locked || false, }; panel.classList.add("dragging"); } function handleDragMove(e: MouseEvent | TouchEvent): void { if (!state.dragState) return; const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; state.dragState.currentX = clientX; state.dragState.currentY = clientY; const panel = document.querySelector( `[data-panel="${state.dragState.panelId}"]`, ); const container = panel?.parentElement; if (container) { container.style.transform = `translateX(${clientX - state.dragState.startX}px)`; } } function handleDragEnd(e: MouseEvent | TouchEvent): void { if (!state.dragState) return; const { panelId, currentX } = state.dragState; const panel = document.querySelector(`[data-panel="${panelId}"]`); const container = panel?.parentElement; // Reset transform container.style.transform = ""; panel?.classList.remove("dragging"); // Determine drop zone const newSide = currentX < window.innerWidth / 2 ? "left" : "right"; const panelState = state.panels.get(panelId); if (panelState) { // Check if dropped in valid zone const isValidDrop = newSide === "left" || newSide === "right"; if (isValidDrop) { panelState.last_valid_side = panelState.side; // Save previous valid position panelState.side = newSide; } else { // Snap back to last valid position panelState.side = panelState.last_valid_side; } applyPanelState(panelId, panelState); savePanelState(panelId, panelState); } state.dragState = null; } // Setup window-shade (collapse/expand) handlers function setupWindowShadeHandlers(): void { document.querySelectorAll(".window-shade-toggle").forEach((btn) => { btn.addEventListener("click", (e) => { const panel = (e.target as HTMLElement).closest( ".dockable-panel", ) as HTMLElement; const panelId = panel.dataset.panel; const panelState = state.panels.get(panelId); if (panelState) { panelState.collapsed = !panelState.collapsed; applyPanelState(panelId, panelState); savePanelState(panelId, panelState); } }); }); } // Setup lock toggle handlers function setupLockHandlers(): void { document.querySelectorAll(".panel-lock").forEach((btn) => { btn.addEventListener("click", (e) => { const panel = (e.target as HTMLElement).closest( ".dockable-panel", ) as HTMLElement; const panelId = panel.dataset.panel; const panelState = state.panels.get(panelId); if (panelState) { panelState.locked = !panelState.locked; applyPanelState(panelId, panelState); savePanelState(panelId, panelState); } }); }); } // Persist panel state to settings async function savePanelState( panelId: string, panelState: PanelState, ): Promise { const settings = loadSettings(); settings.panel_layout[panelId as keyof typeof settings.panel_layout] = panelState; await saveSettings(settings); } export { initializePanelDockSystem, registerPanel, applyPanelState };