import React, { useState, useRef, useEffect, memo, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { X } from "lucide-react"; import { splitDragState, notifyDragEnd } from "@/lib/splitDragging"; import { tabIcon } from "@/shell/tabUtils"; import type { Tab, SplitMode } from "@/types/ui-types"; // ─── useSplitDrag ───────────────────────────────────────────────────────────── export type RowColSizes = number[][]; export function defaultSizes(mode: SplitMode): { rowSizes: number[]; rowColSizes: RowColSizes; } { switch (mode) { case "2-way": return { rowSizes: [100], rowColSizes: [[50, 50]] }; case "2-way-horizontal": return { rowSizes: [50, 50], rowColSizes: [[100], [100]] }; case "3-way": return { rowSizes: [50, 50], rowColSizes: [[50, 50], [100]] }; case "3-way-horizontal": return { rowSizes: [50, 50], rowColSizes: [[50, 50], [100]] }; case "4-way": return { rowSizes: [50, 50], rowColSizes: [ [50, 50], [50, 50], ], }; case "5-way": return { rowSizes: [50, 50], rowColSizes: [ [33.3, 33.3, 33.4], [33.3, 66.7], ], }; case "6-way": return { rowSizes: [50, 50], rowColSizes: [ [33.3, 33.3, 33.4], [33.3, 33.3, 33.4], ], }; default: return { rowSizes: [100], rowColSizes: [[100]] }; } } function useSplitDrag( rowSizes: number[], rowColSizes: RowColSizes, onRowSizesChange: (sizes: number[]) => void, onRowColSizesChange: (sizes: RowColSizes) => void, onReset?: () => void, ) { const [isDragging, setIsDragging] = useState(false); const containerRef = useRef(null); function reset() { onReset?.(); } function startDrag() { splitDragState.active = true; setIsDragging(true); } function endDrag() { splitDragState.active = false; setIsDragging(false); notifyDragEnd(); } function onRowDivider(e: React.MouseEvent, rowIdx: number) { e.preventDefault(); const container = containerRef.current; if (!container) return; startDrag(); const totalH = container.getBoundingClientRect().height; const startY = e.clientY; const a = rowSizes[rowIdx], b = rowSizes[rowIdx + 1]; function onMove(ev: MouseEvent) { const na = Math.max( 10, Math.min(a + b - 10, a + ((ev.clientY - startY) / totalH) * 100), ); const n = [...rowSizes]; n[rowIdx] = na; n[rowIdx + 1] = a + b - na; onRowSizesChange(n); } function onUp() { endDrag(); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); } window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); } function onRowDividerTouch(e: React.TouchEvent, rowIdx: number) { const container = containerRef.current; if (!container) return; startDrag(); const totalH = container.getBoundingClientRect().height; const startY = e.touches[0].clientY; const a = rowSizes[rowIdx], b = rowSizes[rowIdx + 1]; function onMove(ev: TouchEvent) { const na = Math.max( 10, Math.min( a + b - 10, a + ((ev.touches[0].clientY - startY) / totalH) * 100, ), ); const n = [...rowSizes]; n[rowIdx] = na; n[rowIdx + 1] = a + b - na; onRowSizesChange(n); } function onUp() { endDrag(); window.removeEventListener("touchmove", onMove); window.removeEventListener("touchend", onUp); } window.addEventListener("touchmove", onMove, { passive: false }); window.addEventListener("touchend", onUp); } function onColDivider(e: React.MouseEvent, rowIdx: number, colIdx: number) { e.preventDefault(); const container = containerRef.current; if (!container) return; startDrag(); const totalW = container.getBoundingClientRect().width; const startX = e.clientX; const cols = rowColSizes[rowIdx]; const a = cols[colIdx], b = cols[colIdx + 1]; function onMove(ev: MouseEvent) { const na = Math.max( 10, Math.min(a + b - 10, a + ((ev.clientX - startX) / totalW) * 100), ); const next = rowColSizes.map((r) => [...r]); next[rowIdx][colIdx] = na; next[rowIdx][colIdx + 1] = a + b - na; onRowColSizesChange(next); } function onUp() { endDrag(); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); } window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); } function onColDividerTouch( e: React.TouchEvent, rowIdx: number, colIdx: number, ) { const container = containerRef.current; if (!container) return; startDrag(); const totalW = container.getBoundingClientRect().width; const startX = e.touches[0].clientX; const cols = rowColSizes[rowIdx]; const a = cols[colIdx], b = cols[colIdx + 1]; function onMove(ev: TouchEvent) { const na = Math.max( 10, Math.min( a + b - 10, a + ((ev.touches[0].clientX - startX) / totalW) * 100, ), ); const next = rowColSizes.map((r) => [...r]); next[rowIdx][colIdx] = na; next[rowIdx][colIdx + 1] = a + b - na; onRowColSizesChange(next); } function onUp() { endDrag(); window.removeEventListener("touchmove", onMove); window.removeEventListener("touchend", onUp); } window.addEventListener("touchmove", onMove, { passive: false }); window.addEventListener("touchend", onUp); } return { isDragging, containerRef, reset, onRowDivider, onRowDividerTouch, onColDivider, onColDividerTouch, }; } // ─── Dividers ───────────────────────────────────────────────────────────────── function ColDivider({ onMouseDown, onTouchStart, }: { onMouseDown: (e: React.MouseEvent) => void; onTouchStart: (e: React.TouchEvent) => void; }) { return (
); } function RowDivider({ onMouseDown, onTouchStart, }: { onMouseDown: (e: React.MouseEvent) => void; onTouchStart: (e: React.TouchEvent) => void; }) { return (
); } // ─── Pane ───────────────────────────────────────────────────────────────────── function PaneHeader({ tab, paneIndex, isFocused, onClear, }: { tab: Tab | null; paneIndex: number; isFocused: boolean; onClear?: () => void; }) { const { t } = useTranslation(); return (
{isFocused && ( )} {tab ? ( <> {tabIcon(tab.type)} {tab.type === "dashboard" ? "Dashboard" : tab.label} ) : ( {t("splitScreen.paneEmpty", { index: paneIndex + 1 })} )}
); } function EmptyPane() { const { t } = useTranslation(); return (
{t("splitScreen.noTabAssigned")}
); } const Pane = memo(function Pane({ tab, paneIndex, isDragging, isFocused, onPaneContentRef, onPaneClick, onAssignPane, }: { tab: Tab | null; paneIndex: number; isDragging: boolean; isFocused: boolean; onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; onPaneClick?: (paneIndex: number) => void; onAssignPane?: (paneIndex: number, tabId: string) => void; }) { const [isDragOver, setIsDragOver] = useState(false); const contentRef = useCallback( (el: HTMLDivElement | null) => { onPaneContentRef?.(paneIndex, el); }, [paneIndex, onPaneContentRef], ); return (
onPaneClick?.(paneIndex)} onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { e.preventDefault(); setIsDragOver(false); const tabId = e.dataTransfer.getData("text/plain"); if (tabId) onAssignPane?.(paneIndex, tabId); }} > onAssignPane?.(paneIndex, "") : undefined} />
{tab ? (
) : ( )} {isDragOver && (
Drop to assign
)}
{isDragging && (
)}
); }); // ─── Row (top-level so React never sees a new component type) ───────────────── const Row = memo(function Row({ rowIdx, paneIndices, rowHeight, colWidths, paneTabIds, tabs, isDragging, focusedPaneIndex, onColDivider, onColDividerTouch, onPaneContentRef, onPaneClick, onAssignPane, }: { rowIdx: number; paneIndices: number[]; rowHeight: number; colWidths: number[]; paneTabIds: (string | null)[]; tabs: Tab[]; isDragging: boolean; focusedPaneIndex: number | null; onColDivider: (e: React.MouseEvent, rowIdx: number, colIdx: number) => void; onColDividerTouch: ( e: React.TouchEvent, rowIdx: number, colIdx: number, ) => void; onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; onPaneClick?: (paneIndex: number) => void; onAssignPane?: (paneIndex: number, tabId: string) => void; }) { const widths = colWidths ?? []; return (
{paneIndices.map((pIdx, ci) => { const tabId = paneTabIds[pIdx]; const tab = tabId != null ? (tabs.find((t) => t.id === tabId) ?? null) : null; return (
{ci < paneIndices.length - 1 && ( onColDivider(e, rowIdx, ci)} onTouchStart={(e) => onColDividerTouch(e, rowIdx, ci)} /> )}
); })}
); }); // ─── SplitView ──────────────────────────────────────────────────────────────── export const SplitView = memo(function SplitView({ tabs, paneTabIds, splitMode, rowSizes, rowColSizes, onRowSizesChange, onRowColSizesChange, onReset, focusedPaneIndex, onTerminalResize, onPaneContentRef, onPaneClick, onAssignPane, }: { tabs: Tab[]; paneTabIds: (string | null)[]; splitMode: SplitMode; rowSizes: number[]; rowColSizes: RowColSizes; onRowSizesChange: (sizes: number[]) => void; onRowColSizesChange: (sizes: RowColSizes) => void; onReset?: () => void; focusedPaneIndex?: number | null; onTerminalResize?: () => void; onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void; onPaneClick?: (paneIndex: number) => void; onAssignPane?: (paneIndex: number, tabId: string) => void; }) { const { isDragging, containerRef, reset, onRowDivider, onRowDividerTouch, onColDivider, onColDividerTouch, } = useSplitDrag( rowSizes, rowColSizes, onRowSizesChange, onRowColSizesChange, onReset, ); useEffect(() => { if (!isDragging) { const id = requestAnimationFrame(() => onTerminalResize?.()); return () => cancelAnimationFrame(id); } }, [isDragging, onTerminalResize]); // Inline pane lookup for the non-Row layouts (3-way, 3-way-horizontal) function tab(idx: number): Tab | null { const tabId = paneTabIds[idx]; return tabId != null ? (tabs.find((t) => t.id === tabId) ?? null) : null; } return (
{splitMode === "2-way" && ( )} {splitMode === "2-way-horizontal" && (
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "3-way" && (
onColDivider(e, 0, 0)} onTouchStart={(e) => onColDividerTouch(e, 0, 0)} />
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "3-way-horizontal" && (
onColDivider(e, 0, 0)} onTouchStart={(e) => onColDividerTouch(e, 0, 0)} />
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "4-way" && (
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "5-way" && (
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)} {splitMode === "6-way" && (
onRowDivider(e, 0)} onTouchStart={(e) => onRowDividerTouch(e, 0)} />
)}
); });