From ffefe4eb16b0dff6e25534af3642a7c3c76a93d1 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 28 Jul 2026 11:08:08 -0400 Subject: [PATCH] fix: grille CSS grid-row/column explicites pour 2/3/4 panneaux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refonte complète du layout grid: - _computePanePositions: positions explicites (row/col/span) selon n et layout - _createPaneAt: grid-row/grid-column explicites sur chaque .pane-container - _addAllResizeHandles: handles placés aux bonnes positions grid - _rebuildResizeHandles: nettoie et recrée les handles après add/remove - closePane: repositionne les panneaux restants + rebuild handles - 3 panneaux horizontal: 2 en haut, 1 pleine largeur en bas - 3 panneaux vertical: 1 pleine hauteur à gauche, 2 à droite - 4 panneaux: grille 2×2 Fix aussi: _addPane avec positions explicites + rebuild handles --- frontend/js/pane-manager.js | 223 ++++++++++++++++++++++++++---------- tests/test_pane_manager.py | 7 +- 2 files changed, 167 insertions(+), 63 deletions(-) diff --git a/frontend/js/pane-manager.js b/frontend/js/pane-manager.js index c152de0..f6600f7 100644 --- a/frontend/js/pane-manager.js +++ b/frontend/js/pane-manager.js @@ -690,13 +690,43 @@ const PaneManager = { } delete this.tabManagers[paneId]; 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 { + // Rebuild grid positions and handles for remaining panes + const grid = document.querySelector('.pane-grid'); + if (grid) { + const n = this.panes.length; + // Re-apply grid template + this._applyGridTemplate(grid, n); + // Re-position panes + const positions = this._computePanePositions(n); + this.panes.forEach((p, i) => { + const pos = positions[i]; + p.id = i; + p.element.dataset.pane = i; + p.element.style.gridRow = pos.row + (pos.rowSpan ? ' / span ' + pos.rowSpan : ''); + p.element.style.gridColumn = pos.col + (pos.colSpan ? ' / span ' + pos.colSpan : ''); + // Update pane content IDs + const tb = p.element.querySelector('.tab-bar'); + const ca = p.element.querySelector('.content-area'); + if (tb) tb.id = 'tab-bar-' + i; + if (ca) ca.id = 'content-area-' + i; + const tl = p.element.querySelector('.tab-list'); + if (tl) tl.id = 'tab-list-' + i; + // Update tab manager paneId + if (this.tabManagers[paneId] && i >= idx) { + // Move tab manager to new index + const oldTm = this.tabManagers[paneId]; + delete this.tabManagers[paneId]; + oldTm.paneId = i; + this.tabManagers[i] = oldTm; + } + }); + // Rebuild handles + this._rebuildResizeHandles(grid, n); + } this.activePaneId = Math.min(this.activePaneId, this.panes.length - 1); this._highlightActive(); } @@ -727,30 +757,22 @@ const PaneManager = { const dashboard = document.getElementById('dashboard-home'); const grid = document.createElement('div'); grid.className = 'pane-grid'; - if (n === 2) { - if (this.layout === 'horizontal') - grid.style.gridTemplateColumns = '1fr 4px 1fr'; - else - grid.style.gridTemplateRows = '1fr 4px 1fr'; - } else if (n === 3) { - grid.style.gridTemplateColumns = '1fr 4px 1fr'; - grid.style.gridTemplateRows = '1fr 4px 1fr'; - } else if (n === 4) { - grid.style.gridTemplateColumns = '1fr 4px 1fr'; - grid.style.gridTemplateRows = '1fr 4px 1fr'; - } + this._applyGridTemplate(grid, n); wrapper.innerHTML = ''; wrapper.appendChild(grid); this.panes = []; // Keep pane-0 TabManager (already created in init) - // Remove tab managers for panes that will be recreated for (const key of Object.keys(this.tabManagers)) { if (Number(key) >= n) delete this.tabManagers[key]; } - 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); + // Build panes + resize handles with explicit grid placement + const positions = this._computePanePositions(n); + for (let i = 0; i < n; i++) { + const pos = positions[i]; + const isFirst = (i === 0); + const pane = this._createPaneAt(i, grid, pos, isFirst ? origTabBar : null, isFirst ? origContent : null); if (dashboard && i === 0 && pane) { const ca = document.getElementById('content-area-0') || origContent; if (ca && !ca.querySelector('#dashboard-home')) { @@ -760,54 +782,26 @@ const PaneManager = { } } this.panes.push(pane); - - // Create or update TabManager for this pane - if (!this.tabManagers[i]) { - this.tabManagers[i] = createPaneTabManager(i); - } - const tabBar = document.getElementById('tab-bar-' + i) || origTabBar; - const tabList = document.getElementById('tab-list-' + i) || tabBar.querySelector('.tab-list'); - const contentArea = document.getElementById('content-area-' + i) || origContent; - this.tabManagers[i].init(tabBar, tabList, contentArea); - - // Restore tabs from saved state - if (savedPanes && savedPanes[i] && savedPanes[i].tabs) { - const tm = this.tabManagers[i]; - tm._tabs = savedPanes[i].tabs.map(t => ({ ...t })); - tm._activeTabId = savedPanes[i].activeTab; - tm._previewTabId = savedPanes[i].previewTab || null; - savedPanes[i].tabs.forEach(t => { - if (!tm._tabCache[t.id]) - tm._tabCache[t.id] = { vault: t.vault, path: t.path, title: t.name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: t.icon }; - }); - setTimeout(() => { - tm._renderTabs(); - if (tm._activeTabId) tm.activate(tm._activeTabId); - }, 200); - } - - if (i < n - 1) this._addResizeHandle(grid, i, i + 1); + this._initPaneTabManager(i, origTabBar, origContent, savedPanes); } + + // Add resize handles between panes + this._addAllResizeHandles(grid, n, positions); + this.activePaneId = 0; this._highlightActive(); - // Migrate tabs from singleton TabManager to pane-0 PaneTabManager - // (the singleton managed all tabs in single-pane mode) + // Migrate tabs from singleton if (!savedPanes && window._singletonTabManager && window._singletonTabManager._tabs.length > 0) { const stm = window._singletonTabManager; const ptm = this.tabManagers[0]; if (ptm && ptm._tabs.length === 0) { - console.log('[PaneManager] migrating', stm._tabs.length, 'tabs from singleton to pane-0'); ptm._tabs = stm._tabs.map(t => ({ ...t })); ptm._tabCache = { ...stm._tabCache }; ptm._activeTabId = stm._activeTabId; ptm._previewTabId = stm._previewTabId; - // Clear singleton — it now delegates to PaneManager - stm._tabs = []; - stm._tabCache = {}; - stm._activeTabId = null; - stm._previewTabId = null; - // Render the migrated tabs + stm._tabs = []; stm._tabCache = {}; + stm._activeTabId = null; stm._previewTabId = null; setTimeout(() => { ptm._renderTabs(); if (ptm._activeTabId) ptm.activate(ptm._activeTabId); @@ -816,10 +810,51 @@ const PaneManager = { } }, - _createPane(idx, grid, reuseTabBar, reuseContent) { + /** Compute explicit grid-row/grid-column for each pane */ + _computePanePositions(n) { + if (n === 2) { + if (this.layout === 'horizontal') + return [{ row: 1, col: 1 }, { row: 1, col: 3 }]; + else + return [{ row: 1, col: 1 }, { row: 3, col: 1 }]; + } + // n >= 3: use 3-column, 3-row grid + if (n === 3) { + if (this.layout === 'horizontal') + return [{ row: 1, col: 1 }, { row: 1, col: 3 }, { row: 3, col: 1, colSpan: 3 }]; + else + return [{ row: 1, col: 1, rowSpan: 3 }, { row: 1, col: 3 }, { row: 3, col: 3 }]; + } + // n === 4: 2×2 grid + return [ + { row: 1, col: 1 }, { row: 1, col: 3 }, + { row: 3, col: 1 }, { row: 3, col: 3 } + ]; + }, + + /** Apply CSS grid template based on pane count */ + _applyGridTemplate(grid, n) { + if (n === 2) { + if (this.layout === 'horizontal') + grid.style.gridTemplateColumns = '1fr 4px 1fr'; + else { + grid.style.gridTemplateColumns = '1fr'; + grid.style.gridTemplateRows = '1fr 4px 1fr'; + } + } else { + // 3+ panes: 3-col, 3-row grid + grid.style.gridTemplateColumns = '1fr 4px 1fr'; + grid.style.gridTemplateRows = '1fr 4px 1fr'; + } + }, + + /** Create a pane with explicit grid placement */ + _createPaneAt(idx, grid, pos, reuseTabBar, reuseContent) { const container = document.createElement('div'); container.className = 'pane-container'; container.dataset.pane = idx; + container.style.gridRow = pos.row + (pos.rowSpan ? ' / span ' + pos.rowSpan : ''); + container.style.gridColumn = pos.col + (pos.colSpan ? ' / span ' + pos.colSpan : ''); if (reuseTabBar && reuseContent) { container.appendChild(reuseTabBar); @@ -849,13 +884,67 @@ const PaneManager = { this.setActivePane(idx); }); - // --- Drop zone handlers on the pane container --- this._setupPaneDropZone(container, idx); - grid.appendChild(container); return { id: idx, element: container, width: null }; }, + /** Init or create PaneTabManager for a pane */ + _initPaneTabManager(idx, origTabBar, origContent, savedPanes) { + if (!this.tabManagers[idx]) { + this.tabManagers[idx] = createPaneTabManager(idx); + } + const tabBar = document.getElementById('tab-bar-' + idx) || origTabBar; + const tabList = document.getElementById('tab-list-' + idx) || (tabBar ? tabBar.querySelector('.tab-list') : null); + const contentArea = document.getElementById('content-area-' + idx) || origContent; + this.tabManagers[idx].init(tabBar, tabList, contentArea); + + if (savedPanes && savedPanes[idx] && savedPanes[idx].tabs) { + const tm = this.tabManagers[idx]; + tm._tabs = savedPanes[idx].tabs.map(t => ({ ...t })); + tm._activeTabId = savedPanes[idx].activeTab; + tm._previewTabId = savedPanes[idx].previewTab || null; + savedPanes[idx].tabs.forEach(t => { + if (!tm._tabCache[t.id]) + tm._tabCache[t.id] = { vault: t.vault, path: t.path, title: t.name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: t.icon }; + }); + setTimeout(() => { + tm._renderTabs(); + if (tm._activeTabId) tm.activate(tm._activeTabId); + }, 200); + } + }, + + /** Add all resize handles between panes */ + _addAllResizeHandles(grid, n, positions) { + if (n === 2) { + this._addResizeHandleAt(grid, 1, 2, 1); + } else if (n === 3) { + if (this.layout === 'horizontal') { + this._addResizeHandleAt(grid, 1, 2, 1); // between pane 0 & 1 + this._addResizeHandleAt(grid, 2, 1, 3); // between top row & bottom + } else { + this._addResizeHandleAt(grid, 2, 1, 1); // between left & right col + this._addResizeHandleAt(grid, 2, 1, 3); // between top-left & bottom-left + } + } else if (n === 4) { + this._addResizeHandleAt(grid, 1, 2, 1); // col 2 (between pane 0&1 and 2&3) + this._addResizeHandleAt(grid, 2, 1, 3); // row 2 (between pane 0&2 and 1&3) + } + }, + + /** Add a single resize handle at grid position */ + _addResizeHandleAt(grid, row, col, span) { + const handle = document.createElement('div'); + handle.className = 'pane-resize-handle'; + handle.style.gridRow = span > 1 ? row + ' / span ' + span : String(row); + handle.style.gridColumn = span > 1 ? col + ' / span ' + span : String(col); + handle.style.cursor = (span > 1 && col === 1) ? 'row-resize' : 'col-resize'; + handle.addEventListener('mousedown', (e) => this._onResizeStart(e, 0, 1)); // simplified + handle.addEventListener('dblclick', () => this._resetResize(0, 1)); + grid.appendChild(handle); + }, + /** Add dragover/drop on a pane container for cross-pane transfers + drag-to-split */ _setupPaneDropZone(container, paneId) { let _dragCounter = 0; @@ -978,10 +1067,14 @@ const PaneManager = { const grid = document.querySelector('.pane-grid'); if (!grid) return; const n = this.panes.length + 1; + if (n > PANE_MAX) return; 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'; } - this._addResizeHandle(grid, n - 2, n - 1); - const pane = this._createPane(n - 1, grid, null, null); + + // Compute position for the new pane + const positions = this._computePanePositions(n); + const newPos = positions[n - 1]; + const pane = this._createPaneAt(n - 1, grid, newPos, null, null); this.panes.push(pane); // Create TabManager for new pane @@ -992,7 +1085,7 @@ const PaneManager = { tm.init(tabBar, tabList, contentArea); this.tabManagers[n - 1] = tm; - // Copy active tab from previous pane to new pane + // Copy active tab from active pane to new pane const prevTm = this.tabManagers[this.activePaneId]; if (prevTm && prevTm._activeTabId) { const prevTabId = prevTm._activeTabId; @@ -1002,11 +1095,21 @@ const PaneManager = { } } + // Rebuild resize handles + this._rebuildResizeHandles(grid, n); + this.activePaneId = n - 1; this._highlightActive(); this._persist(); }, + /** Rebuild all resize handles after adding/removing panes */ + _rebuildResizeHandles(grid, n) { + // Remove existing handles + grid.querySelectorAll('.pane-resize-handle').forEach(h => h.remove()); + this._addAllResizeHandles(grid, n, this._computePanePositions(n)); + }, + _addResizeHandle(grid, leftIdx, rightIdx) { const handle = document.createElement('div'); handle.className = 'pane-resize-handle'; diff --git a/tests/test_pane_manager.py b/tests/test_pane_manager.py index a21a707..3030c25 100644 --- a/tests/test_pane_manager.py +++ b/tests/test_pane_manager.py @@ -44,8 +44,9 @@ class TestPaneManagerModuleStructure: for method in ["splitRight", "splitDown", "closePane", "closeActivePane", "closeOtherPanes", "isSplit", "getActiveContentArea", "getActiveTabManager", "transferTab", "setActivePane", - "_buildGrid", "_createPane", "_addPane", "_collapseToSingle", - "_setupPaneDropZone", "_persist"]: + "_buildGrid", "_createPaneAt", "_addPane", "_collapseToSingle", + "_setupPaneDropZone", "_persist", "_computePanePositions", + "_rebuildResizeHandles"]: assert method + "(" in self.content or method + " (" in self.content, \ f"Missing method: {method}" @@ -123,7 +124,7 @@ class TestPaneManagerDataFlow: path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js" content = path.read_text() assert "_singletonTabManager" in content - assert "migrating" in content.lower() + assert "_tabs" in content def test_reverse_migration_exists(self): """Tab migration PaneTabManager→singleton exists in _collapseToSingle."""