/** * 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. * * Architecture: * Single-pane (default): uses existing #tab-bar, #content-area — zero DOM changes. * Multi-pane: builds a .pane-grid inside .content-wrapper, each pane gets its * own tab-bar-N and content-area-N DOM elements. * TabManager remains the singleton source of truth for open tabs. * PaneManager tracks per-pane active tab and delegates rendering. */ import { state } from './state.js'; const PANE_MAX = 4; const PaneManager = { panes: [], activePaneId: 0, _paneCounter: 1, layout: 'horizontal', // ------------------------------------------------------------------ // Init / Restore // ------------------------------------------------------------------ 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) => { if (this.panes[i]) { this.panes[i].activeTab = p.activeTab; this.panes[i].width = p.width; } }); this.activePaneId = data.panes.length > 0 ? this.panes[0].id : 0; this._applyResize(); this._highlightActive(); return; } } catch (e) { /* ignore corrupt state */ } } }, // ------------------------------------------------------------------ // DOM Helpers — return the active pane's DOM elements // ------------------------------------------------------------------ getActiveContentArea() { if (this.panes.length < 2) return document.getElementById('content-area'); return document.getElementById('content-area-' + this.activePaneId) || document.getElementById('content-area-0'); }, getActiveTabBar() { if (this.panes.length < 2) return document.getElementById('tab-bar'); return document.getElementById('tab-bar-' + this.activePaneId) || document.getElementById('tab-bar-0'); }, getActiveTabList() { if (this.panes.length < 2) return document.getElementById('tab-list'); return document.getElementById('tab-list-' + this.activePaneId) || document.getElementById('tab-list-0'); }, // ------------------------------------------------------------------ // Build / Destroy pane grid // ------------------------------------------------------------------ _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; // Save dashboard before clearing const dashboard = document.getElementById('dashboard-home'); // 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 (this.layout === 'vertical') { grid.style.gridTemplateRows = '1fr 4px 1fr'; } } else if (n === 3) { grid.style.gridTemplateColumns = '1fr 4px 1fr'; grid.style.gridTemplateRows = '1fr 4px 1fr'; // 3 panes: first two in top row, third spans bottom row } if (n === 4) { grid.style.gridTemplateColumns = '1fr 4px 1fr'; grid.style.gridTemplateRows = '1fr 4px 1fr'; } wrapper.innerHTML = ''; wrapper.appendChild(grid); this.panes = []; 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); if (dashboard && i === 0 && pane) { // Re-attach dashboard to first pane's content area const contentArea = document.getElementById('content-area-0') || origContent; if (contentArea && !contentArea.querySelector('#dashboard-home')) { contentArea.innerHTML = ''; contentArea.appendChild(dashboard); dashboard.style.display = ''; } } this.panes.push(pane); if (i < n - 1) { this._addResizeHandle(grid, i, i + 1); } } 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'; reuseTabBar.id = 'tab-bar-0'; // Rename tab-list too const tl = reuseTabBar.querySelector('.tab-list') || reuseTabBar.querySelector('#tab-list'); if (tl) tl.id = 'tab-list-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; content.setAttribute('aria-label', 'Contenu panneau ' + (idx + 1)); container.appendChild(tabBar); container.appendChild(content); } container.addEventListener('click', () => { if (this.activePaneId !== idx) { this.activePaneId = idx; this._highlightActive(); // Sync global state with this pane's active tab const pane = this.panes[idx]; if (pane && pane.activeTab) { const parts = pane.activeTab.split('::'); if (parts.length === 2) { state.currentVault = parts[0]; state.currentPath = parts[1]; } } // Re-render if needed — delegate to TabManager if (typeof window.TabManager !== 'undefined' && window.TabManager.activatePaneTab) { window.TabManager.activatePaneTab(idx); } } }); grid.appendChild(container); return { id: idx, element: container, width: null, activeTab: null }; }, _addResizeHandle(grid, leftIdx, rightIdx) { 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, leftIdx, rightIdx)); handle.addEventListener('dblclick', () => this._resetResize(leftIdx, rightIdx)); grid.appendChild(handle); }, // ------------------------------------------------------------------ // Split actions // ------------------------------------------------------------------ splitRight() { if (this.panes.length >= PANE_MAX) return; this.layout = 'horizontal'; if (this.panes.length === 0) { 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; if (n === 3) { grid.style.gridTemplateColumns = '1fr 4px 1fr'; grid.style.gridTemplateRows = '1fr 4px 1fr'; } if (n === 4) { grid.style.gridTemplateColumns = '1fr 4px 1fr'; grid.style.gridTemplateRows = '1fr 4px 1fr'; } const handle = document.createElement('div'); handle.className = 'pane-resize-handle'; handle.style.cursor = this.layout === 'horizontal' ? 'col-resize' : 'row-resize'; handle.addEventListener('mousedown', (e) => this._onResizeStart(e, n - 2, n - 1)); handle.addEventListener('dblclick', () => this._resetResize(n - 2, n - 1)); grid.appendChild(handle); const pane = this._createPane(n - 1, grid, null, null); pane.activeTab = this.panes.length > 0 ? this.panes[this.activePaneId].activeTab : null; this.panes.push(pane); this.activePaneId = n - 1; this._highlightActive(); this._persist(); }, // ------------------------------------------------------------------ // Close / collapse // ------------------------------------------------------------------ closePane(paneId) { if (this.panes.length <= 1) return; const idx = this.panes.findIndex(p => p.id === paneId); if (idx < 0) return; 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(); }, closeActivePane() { if (this.panes.length <= 1) return; this.closePane(this.activePaneId); }, closeOtherPanes() { if (this.panes.length <= 1) return; const keepId = this.activePaneId; const toClose = this.panes.filter(p => p.id !== keepId); for (const p of toClose.reverse()) { this.closePane(p.id); } }, _collapseToSingle() { const grid = document.querySelector('.pane-grid'); if (!grid) return; const wrapper = grid.parentElement; const pane0 = this.panes[0]; wrapper.innerHTML = ''; const tabBar = (pane0 && pane0.element.querySelector('.tab-bar')) || document.getElementById('tab-bar-0'); const content = (pane0 && pane0.element.querySelector('.content-area')) || document.getElementById('content-area-0'); if (tabBar) { tabBar.id = 'tab-bar'; wrapper.appendChild(tabBar); } if (content) { content.id = 'content-area'; wrapper.appendChild(content); } // Restore tab-list ID const tl = wrapper.querySelector('.tab-list'); if (tl) tl.id = 'tab-list'; this.panes = []; this.activePaneId = 0; this._persist(); }, // ------------------------------------------------------------------ // Resize // ------------------------------------------------------------------ _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) { if (this.panes[leftIdx]) this.panes[leftIdx].element.style.flexBasis = '50%'; if (this.panes[rightIdx]) this.panes[rightIdx].element.style.flexBasis = '50%'; this._persist(); }, _applyResize() { this.panes.forEach(p => { if (p.width && p.element) p.element.style.flexBasis = p.width; }); }, // ------------------------------------------------------------------ // Highlight // ------------------------------------------------------------------ _highlightActive() { this.panes.forEach((p, i) => { if (p.element) p.element.classList.toggle('pane-active', i === this.activePaneId); }); }, // ------------------------------------------------------------------ // Persistence // ------------------------------------------------------------------ _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)); }, // ------------------------------------------------------------------ // Queries // ------------------------------------------------------------------ isSplit() { return this.panes.length >= 2; }, getPaneCount() { return Math.max(this.panes.length, 1); }, }; // ------------------------------------------------------------------ // Export + auto-init // ------------------------------------------------------------------ if (typeof window !== 'undefined') { window.PaneManager = PaneManager; document.addEventListener('DOMContentLoaded', () => { // Keyboard shortcuts — Ctrl+Alt+\ for split right, Ctrl+Alt+Shift+\ for split down 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(); } // Ctrl+Alt+W — close active pane if (e.ctrlKey && e.altKey && !e.shiftKey && (e.key === 'w' || e.key === 'W') && PaneManager.isSplit()) { e.preventDefault(); PaneManager.closeActivePane(); } }); // Init if saved state exists setTimeout(() => PaneManager.init(), 100); }); } export default PaneManager;