/** * PaneManager — Multi-pane split view for ObsiGate (v2.0.0-dev) * Supports 1–4 panes in CSS grid, split right/down, resize, drag tabs between panes. * Integrates with existing TabManager singleton. */ const PANE_MAX = 4; const PaneManager = { panes: [], activePaneId: 0, _paneCounter: 1, layout: 'horizontal', init() { const saved = localStorage.getItem('obsigate-panes'); if (saved) { try { const data = JSON.parse(saved); if (data.panes && data.panes.length >= 2) { this.layout = data.layout || 'horizontal'; this._paneCounter = data.panes.length; this._buildGrid(data.panes.length); data.panes.forEach((p, i) => { this.panes[i].activeTab = p.activeTab; this.panes[i].width = p.width; }); this.activePaneId = data.panes[0].id; this._applyResize(); return; } } catch (e) { /* ignore */ } } }, _buildGrid(n) { const wrapper = document.querySelector('.content-wrapper'); const origTabBar = document.getElementById('tab-bar'); const origContent = document.getElementById('content-area'); if (!wrapper || !origContent || n < 2) return; // Create grid container const grid = document.createElement('div'); grid.className = 'pane-grid'; if (n === 2) grid.style.gridTemplateColumns = this.layout === 'horizontal' ? '1fr 4px 1fr' : '1fr'; if (n >= 3) grid.style.gridTemplateColumns = '1fr 4px 1fr'; if (n === 4) grid.style.gridTemplateRows = '1fr 4px 1fr'; wrapper.innerHTML = ''; wrapper.appendChild(grid); for (let i = 0; i < Math.min(n, PANE_MAX); i++) { const pane = this._createPane(i, grid, i === 0 ? origTabBar : null, i === 0 ? origContent : null); this.panes.push(pane); if (i < n - 1) { const handle = document.createElement('div'); handle.className = 'pane-resize-handle'; if (this.layout === 'horizontal') handle.style.cursor = 'col-resize'; else handle.style.cursor = 'row-resize'; handle.addEventListener('mousedown', (e) => this._onResizeStart(e, i, i + 1)); handle.addEventListener('dblclick', () => this._resetResize(i, i + 1)); grid.appendChild(handle); } } this.activePaneId = 0; this._highlightActive(); }, _createPane(idx, grid, reuseTabBar, reuseContent) { const container = document.createElement('div'); container.className = 'pane-container'; container.dataset.pane = idx; if (reuseTabBar && reuseContent) { // Pane 0 reuses existing DOM container.appendChild(reuseTabBar); container.appendChild(reuseContent); reuseContent.id = 'content-area-0'; } else { const tabBar = document.createElement('div'); tabBar.className = 'tab-bar pane-tab-bar'; tabBar.id = 'tab-bar-' + idx; tabBar.innerHTML = '
'; const content = document.createElement('main'); content.className = 'content-area pane-content'; content.id = 'content-area-' + idx; container.appendChild(tabBar); container.appendChild(content); } container.addEventListener('click', () => { this.activePaneId = idx; this._highlightActive(); }); grid.appendChild(container); return { id: idx, element: container, width: 100 / this.panes.length + '%' }; }, _highlightActive() { this.panes.forEach((p, i) => { p.element.classList.toggle('pane-active', i === this.activePaneId); }); }, splitRight() { if (this.panes.length >= PANE_MAX) return; if (this.panes.length === 0) { // First split: promote single pane to grid this._buildGrid(2); } else { this._addPane(); } this._persist(); }, splitDown() { if (this.panes.length >= PANE_MAX) return; this.layout = 'vertical'; if (this.panes.length === 0) { this._buildGrid(2); } else { this._addPane(); } this._persist(); }, _addPane() { const grid = document.querySelector('.pane-grid'); if (!grid) return; const n = this.panes.length + 1; const handle = document.createElement('div'); handle.className = 'pane-resize-handle'; handle.addEventListener('mousedown', (e) => this._onResizeStart(e, n - 2, n - 1)); grid.appendChild(handle); const pane = this._createPane(n - 1, grid, null, null); this.panes.push(pane); this.activePaneId = n - 1; this._highlightActive(); if (n <= 2) grid.style.gridTemplateColumns = '1fr 4px 1fr'; else if (n === 4) grid.style.gridTemplateRows = '1fr 4px 1fr'; }, closePane(paneId) { if (this.panes.length <= 1) return; const idx = this.panes.findIndex(p => p.id === paneId); if (idx < 0) return; // Remove pane and its resize handles this.panes[idx].element.remove(); const handles = document.querySelectorAll('.pane-resize-handle'); if (handles[idx]) handles[idx].remove(); else if (handles[idx - 1]) handles[idx - 1].remove(); this.panes.splice(idx, 1); if (this.panes.length <= 1) { this._collapseToSingle(); } else { this.activePaneId = Math.min(this.activePaneId, this.panes.length - 1); this._highlightActive(); } this._persist(); }, _collapseToSingle() { const grid = document.querySelector('.pane-grid'); if (!grid) return; const wrapper = grid.parentElement; const pane0 = this.panes[0]; wrapper.innerHTML = ''; const tabBar = pane0.element.querySelector('.tab-bar') || document.getElementById('tab-bar-0'); const content = pane0.element.querySelector('.content-area') || document.getElementById('content-area-0'); if (tabBar) wrapper.appendChild(tabBar); if (content) wrapper.appendChild(content); this.panes = []; this.activePaneId = 0; }, _onResizeStart(e, leftIdx, rightIdx) { e.preventDefault(); const grid = document.querySelector('.pane-grid'); const startX = e.clientX; const startY = e.clientY; const totalSize = this.layout === 'horizontal' ? grid.offsetWidth : grid.offsetHeight; const onMove = (ev) => { const delta = this.layout === 'horizontal' ? ev.clientX - startX : ev.clientY - startY; const pct = (delta / totalSize) * 100; const leftEl = this.panes[leftIdx].element; const rightEl = this.panes[rightIdx].element; const curLeft = parseFloat(leftEl.style.flexBasis || leftEl.style.width || '50'); const curRight = parseFloat(rightEl.style.flexBasis || rightEl.style.width || '50'); const newLeft = Math.max(20, Math.min(80, curLeft + pct)); const newRight = 100 - newLeft; leftEl.style.flexBasis = newLeft + '%'; rightEl.style.flexBasis = newRight + '%'; this.panes[leftIdx].width = newLeft + '%'; this.panes[rightIdx].width = newRight + '%'; }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); this._persist(); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }, _resetResize(leftIdx, rightIdx) { this.panes[leftIdx].element.style.flexBasis = '50%'; this.panes[rightIdx].element.style.flexBasis = '50%'; this._persist(); }, _applyResize() { this.panes.forEach(p => { if (p.width) p.element.style.flexBasis = p.width; }); }, _persist() { if (this.panes.length <= 1) { localStorage.removeItem('obsigate-panes'); return; } const data = { layout: this.layout, panes: this.panes.map(p => ({ id: p.id, activeTab: p.activeTab, width: p.width })), }; localStorage.setItem('obsigate-panes', JSON.stringify(data)); }, getActiveContentArea() { if (this.panes.length < 2) return document.getElementById('content-area'); return document.getElementById('content-area-' + this.activePaneId) || document.getElementById('content-area-0'); }, isSplit() { return this.panes.length >= 2; }, }; // Export and auto-init if (typeof window !== 'undefined') { window.PaneManager = PaneManager; document.addEventListener('DOMContentLoaded', () => { // Register keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.altKey && !e.shiftKey && e.key === '\\') { e.preventDefault(); PaneManager.splitRight(); } if (e.ctrlKey && e.altKey && e.shiftKey && e.key === '\\') { e.preventDefault(); PaneManager.splitDown(); } }); // Initialize if saved state exists setTimeout(() => PaneManager.init(), 500); }); } export default PaneManager;