fix: grille CSS grid-row/column explicites pour 2/3/4 panneaux
CI / lint (push) Failing after 9s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 13s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled

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
This commit is contained in:
2026-07-28 11:08:08 -04:00
parent 257e35198d
commit ffefe4eb16
2 changed files with 167 additions and 63 deletions
+163 -60
View File
@@ -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';
+4 -3
View File
@@ -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."""