diff --git a/frontend/js/pane-manager.js b/frontend/js/pane-manager.js
index 2509e1b..a7de1c8 100644
--- a/frontend/js/pane-manager.js
+++ b/frontend/js/pane-manager.js
@@ -1,29 +1,473 @@
/**
- * 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.
+ * PaneManager + PaneTabManager — Multi-pane split view for ObsiGate (v2.0.0-dev)
*
* 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.
+ * Single-pane (default): uses existing singleton TabManager + #content-area / #tab-bar.
+ * Multi-pane: builds .pane-grid inside .content-wrapper. Each pane gets its OWN
+ * independent PaneTabManager instance with its own tabs, tab bar, and content area.
+ * Drag & drop tabs between panes.
+ * Persisted to localStorage.
*/
import { state } from './state.js';
+import { api } from './auth.js';
+import { escapeHtml, getFileIcon, safeCreateIcons } from './utils.js';
const PANE_MAX = 4;
+// =============================================================================
+// PaneTabManager factory — one per pane
+// =============================================================================
+function createPaneTabManager(paneId) {
+ const DEFAULT_BAR = document.getElementById('tab-bar');
+ const DEFAULT_LIST = document.getElementById('tab-list');
+ const DEFAULT_AREA = document.getElementById('content-area');
+
+ const tm = {
+ paneId,
+ _tabs: [],
+ _activeTabId: null,
+ _previewTabId: null,
+ _tabCache: {},
+ _dirtyTabs: new Set(),
+ _tabBar: paneId === 0 ? DEFAULT_BAR : null,
+ _tabList: paneId === 0 ? DEFAULT_LIST : null,
+ _contentArea: paneId === 0 ? DEFAULT_AREA : null,
+
+ // ------------------------------------------------------------------
+ // Init — bind to DOM
+ // ------------------------------------------------------------------
+ init(tabBar, tabList, contentArea) {
+ if (tabBar) this._tabBar = tabBar;
+ if (tabList) this._tabList = tabList;
+ if (contentArea) this._contentArea = contentArea;
+ if (!this._tabBar)
+ this._tabBar = document.getElementById('tab-bar-' + paneId);
+ if (!this._tabList)
+ this._tabList = document.getElementById('tab-list-' + paneId);
+ if (!this._contentArea)
+ this._contentArea = document.getElementById('content-area-' + paneId);
+ },
+
+ getContentArea() {
+ return this._contentArea || DEFAULT_AREA;
+ },
+ getTabBar() { return this._tabBar; },
+ getTabList() { return this._tabList; },
+
+ // ------------------------------------------------------------------
+ // Open / Activate / Close
+ // ------------------------------------------------------------------
+ async openPreview(vault, path) {
+ const tabId = `${vault}::${path}`;
+ const existing = this._tabs.find(t => t.id === tabId && !t.preview);
+ if (existing) { this.activate(tabId); return; }
+ if (this._previewTabId && this._previewTabId !== tabId)
+ this.close(this._previewTabId);
+ const previewExisting = this._tabs.find(t => t.id === tabId && t.preview);
+ if (previewExisting) { this.activate(tabId); return; }
+ const name = path.split('/').pop().replace(/\.md$/i, '');
+ const icon = getFileIcon(name + '.md');
+ this._tabs.push({ id: tabId, vault, path, name, icon, preview: true });
+ this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon };
+ this._previewTabId = tabId;
+ this._renderTabs();
+ this.activate(tabId);
+ },
+
+ async openPersistent(vault, path) {
+ const tabId = `${vault}::${path}`;
+ const previewTab = this._tabs.find(t => t.id === tabId && t.preview);
+ if (previewTab) {
+ previewTab.preview = false;
+ if (this._previewTabId === tabId) this._previewTabId = null;
+ this._renderTabs();
+ this.activate(tabId);
+ return;
+ }
+ const existing = this._tabs.find(t => t.id === tabId && !t.preview);
+ if (existing) { this.activate(tabId); return; }
+ this.open(vault, path);
+ },
+
+ async open(vault, path) {
+ const tabId = `${vault}::${path}`;
+ const existing = this._tabs.find(t => t.id === tabId);
+ if (existing) {
+ if (existing.preview) {
+ existing.preview = false;
+ if (this._previewTabId === tabId) this._previewTabId = null;
+ this._renderTabs();
+ }
+ this.activate(tabId);
+ return;
+ }
+ const name = path.split('/').pop().replace(/\.md$/i, '');
+ const icon = getFileIcon(name + '.md');
+ this._tabs.push({ id: tabId, vault, path, name, icon });
+ this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon };
+ this._renderTabs();
+ this.activate(tabId);
+ },
+
+ async activate(tabId) {
+ if (this._activeTabId === tabId && this._tabs.length > 0) return;
+ if (this._activeTabId && this._tabCache[this._activeTabId])
+ this._saveCurrentTabState();
+
+ this._activeTabId = tabId;
+ this._renderTabs();
+
+ const cache = this._tabCache[tabId];
+ if (!cache) return;
+
+ state.currentVault = cache.vault;
+ state.currentPath = cache.path;
+ // syncActiveFileTreeItem is imported below to avoid circular deps
+ if (typeof syncActiveFileTreeItem !== 'undefined')
+ syncActiveFileTreeItem(cache.vault, cache.path);
+
+ const area = this._contentArea || DEFAULT_AREA;
+ if (cache.data) {
+ this._restoreTabContent(cache, area);
+ } else {
+ area.innerHTML = '
Chargement...
';
+ try {
+ const data = await api(`/api/file/${encodeURIComponent(cache.vault)}?path=${encodeURIComponent(cache.path)}`);
+ cache.data = data;
+ cache.title = data.title;
+ this._renderFileInPane(cache.data, area);
+ if (cache.sourceView) await this._toggleSourceView(cache, area);
+ if (cache.scrollTop) area.scrollTop = cache.scrollTop;
+ } catch (err) {
+ area.innerHTML = `Erreur: ${escapeHtml(err.message)}
`;
+ }
+ }
+ if (history.pushState)
+ history.pushState(null, '', `#/file/${encodeURIComponent(cache.vault)}/${encodeURIComponent(cache.path)}`);
+ const db = document.getElementById('dashboard-home');
+ if (db) db.style.display = 'none';
+ },
+
+ close(tabId) {
+ const idx = this._tabs.findIndex(t => t.id === tabId);
+ if (idx === -1) return;
+ this._tabs.splice(idx, 1);
+ delete this._tabCache[tabId];
+ this._dirtyTabs.delete(tabId);
+ if (this._tabs.length === 0) {
+ this._activeTabId = null;
+ this._showDashboard();
+ if (this._tabBar) this._tabBar.hidden = true;
+ } else if (this._activeTabId === tabId) {
+ const newIdx = Math.min(idx, this._tabs.length - 1);
+ this.activate(this._tabs[newIdx].id);
+ }
+ this._renderTabs();
+ },
+
+ closeAll() {
+ this._tabs = [];
+ this._tabCache = {};
+ this._dirtyTabs.clear();
+ this._activeTabId = null;
+ this._showDashboard();
+ if (this._tabBar) this._tabBar.hidden = true;
+ },
+
+ closeRight(tabId) {
+ const idx = this._tabs.findIndex(t => t.id === tabId);
+ if (idx === -1) return;
+ const toClose = this._tabs.slice(idx + 1);
+ for (const tab of toClose) { delete this._tabCache[tab.id]; this._dirtyTabs.delete(tab.id); }
+ this._tabs = this._tabs.slice(0, idx + 1);
+ if (!this._tabs.find(t => t.id === this._activeTabId)) this.activate(tabId);
+ this._renderTabs();
+ },
+
+ closeOthers(tabId) {
+ const tab = this._tabs.find(t => t.id === tabId);
+ if (!tab) return;
+ for (const t of this._tabs) { if (t.id !== tabId) { delete this._tabCache[t.id]; this._dirtyTabs.delete(t.id); } }
+ this._tabs = [tab];
+ this.activate(tabId);
+ this._renderTabs();
+ },
+
+ moveTab(fromIdx, toIdx) {
+ if (fromIdx === toIdx || fromIdx < 0 || toIdx < 0) return;
+ const tab = this._tabs.splice(fromIdx, 1)[0];
+ this._tabs.splice(toIdx, 0, tab);
+ this._renderTabs();
+ },
+
+ // ------------------------------------------------------------------
+ // Transfer a tab to another pane's TabManager
+ // ------------------------------------------------------------------
+ transferTabTo(tabId, targetTm) {
+ const idx = this._tabs.findIndex(t => t.id === tabId);
+ if (idx === -1) return false;
+ const tab = this._tabs[idx];
+ const cache = this._tabCache[tabId];
+ // Remove from source
+ this._tabs.splice(idx, 1);
+ if (this._previewTabId === tabId) this._previewTabId = null;
+ delete this._tabCache[tabId];
+ this._dirtyTabs.delete(tabId);
+ // Add to target
+ targetTm._tabs.push({ ...tab, preview: false });
+ targetTm._tabCache[tabId] = cache || { vault: tab.vault, path: tab.path, title: tab.name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: tab.icon };
+ // Activate in source what's left, or show dashboard
+ if (this._tabs.length === 0) {
+ this._activeTabId = null;
+ this._showDashboard();
+ if (this._tabBar) this._tabBar.hidden = true;
+ } else if (this._activeTabId === tabId) {
+ const newIdx = Math.min(idx, this._tabs.length - 1);
+ this.activate(this._tabs[newIdx].id);
+ }
+ this._renderTabs();
+ return true;
+ },
+
+ // ------------------------------------------------------------------
+ // Internal rendering
+ // ------------------------------------------------------------------
+ _renderFileInPane(data, area) {
+ // Set temporary override so getContentArea() returns this pane's area
+ const prev = window._activePaneContentArea;
+ window._activePaneContentArea = area;
+ try {
+ // Dynamic import to avoid circular dep — renderFile is in viewer.js
+ import('./viewer.js').then(m => {
+ m.renderFile(data);
+ window._activePaneContentArea = prev;
+ });
+ } catch (e) {
+ window._activePaneContentArea = prev;
+ }
+ },
+
+ _saveCurrentTabState() {
+ const cache = this._tabCache[this._activeTabId];
+ if (!cache) return;
+ const area = this._contentArea || DEFAULT_AREA;
+ const rendered = document.getElementById('file-rendered-content');
+ cache.scrollTop = area ? area.scrollTop : 0;
+ cache.sourceView = rendered ? rendered.style.display === 'none' : false;
+ },
+
+ _restoreTabContent(cache, area) {
+ this._renderFileInPane(cache.data, area);
+ if (cache.sourceView) this._restoreSourceView(cache, area);
+ if (cache.scrollTop && area) area.scrollTop = cache.scrollTop;
+ },
+
+ async _toggleSourceView(cache, area) {
+ const rendered = document.getElementById('file-rendered-content');
+ const raw = document.getElementById('file-raw-content');
+ if (!rendered || !raw) return;
+ if (!cache.rawSource) {
+ const rawData = await api(`/api/file/${encodeURIComponent(cache.vault)}/raw?path=${encodeURIComponent(cache.path)}`);
+ cache.rawSource = rawData.raw;
+ }
+ raw.textContent = cache.rawSource;
+ rendered.style.display = 'none';
+ raw.style.display = 'block';
+ },
+
+ _restoreSourceView(cache, area) {
+ requestAnimationFrame(() => {
+ const rendered = document.getElementById('file-rendered-content');
+ const raw = document.getElementById('file-raw-content');
+ if (rendered && raw && cache.rawSource) {
+ raw.textContent = cache.rawSource;
+ rendered.style.display = 'none';
+ raw.style.display = 'block';
+ }
+ });
+ },
+
+ _showDashboard() {
+ const area = this._contentArea || DEFAULT_AREA;
+ let dashboard = document.getElementById('dashboard-home');
+ if (!dashboard) {
+ // Dashboard was destroyed — rebuild
+ area.innerHTML = '';
+ if (typeof showWelcome !== 'undefined') showWelcome();
+ return;
+ }
+ area.innerHTML = '';
+ dashboard.style.display = '';
+ area.appendChild(dashboard);
+ if (typeof DashboardStatsWidget !== 'undefined') DashboardStatsWidget.load();
+ if (typeof DashboardConflictsWidget !== 'undefined') DashboardConflictsWidget.load();
+ if (history.pushState) history.pushState(null, '', '#');
+ },
+
+ // ------------------------------------------------------------------
+ // Render tab bar
+ // ------------------------------------------------------------------
+ _renderTabs() {
+ if (!this._tabList) return;
+ this._tabList.innerHTML = '';
+ if (this._tabs.length === 0) {
+ if (this._tabBar) this._tabBar.hidden = true;
+ return;
+ }
+ if (this._tabBar) this._tabBar.hidden = false;
+
+ this._tabs.forEach((tab, idx) => {
+ const el = document.createElement('div');
+ el.className = 'tab-item' + (tab.id === this._activeTabId ? ' active' : '') + (tab.preview ? ' preview' : '');
+ el.draggable = true;
+ el.dataset.tabId = tab.id;
+ el.dataset.index = idx;
+ el.dataset.paneId = String(this.paneId);
+
+ const iconEl = document.createElement('i');
+ iconEl.setAttribute('data-lucide', tab.icon);
+ iconEl.className = 'tab-icon';
+ iconEl.style.width = '14px';
+ iconEl.style.height = '14px';
+ el.appendChild(iconEl);
+
+ const nameEl = document.createElement('span');
+ nameEl.className = 'tab-name';
+ nameEl.textContent = tab.name;
+ nameEl.title = `${tab.vault}/${tab.path}`;
+ el.appendChild(nameEl);
+
+ const closeEl = document.createElement('span');
+ closeEl.className = 'tab-close';
+ closeEl.innerHTML = '';
+ closeEl.addEventListener('click', (e) => { e.stopPropagation(); this.close(tab.id); });
+ el.appendChild(closeEl);
+
+ el.addEventListener('click', () => {
+ // Set this pane as active in PaneManager
+ if (window.PaneManager) window.PaneManager.setActivePane(this.paneId);
+ this.activate(tab.id);
+ });
+ el.addEventListener('dblclick', (e) => { e.preventDefault(); this.close(tab.id); });
+ el.addEventListener('mousedown', (e) => {
+ if (e.button === 1) { e.preventDefault(); this.close(tab.id); }
+ });
+ el.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ this._showTabContextMenu(e.clientX, e.clientY, tab.id);
+ });
+
+ // Drag within same pane
+ el.addEventListener('dragstart', (e) => {
+ e.dataTransfer.setData('application/obsigate-tab', JSON.stringify({
+ paneId: this.paneId, tabId: tab.id, index: idx
+ }));
+ el.classList.add('dragging');
+ });
+ el.addEventListener('dragend', () => {
+ el.classList.remove('dragging');
+ document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove());
+ });
+ el.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ const rect = el.getBoundingClientRect();
+ const mid = rect.left + rect.width / 2;
+ document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove());
+ const indicator = document.createElement('div');
+ indicator.className = 'tab-drop-indicator';
+ if (e.clientX < mid) el.before(indicator);
+ else el.after(indicator);
+ });
+ el.addEventListener('drop', (e) => {
+ e.preventDefault();
+ document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove());
+ try {
+ const raw = e.dataTransfer.getData('application/obsigate-tab');
+ if (!raw) return;
+ const dragData = JSON.parse(raw);
+ // Same pane — reorder
+ if (dragData.paneId === this.paneId) {
+ const rect = el.getBoundingClientRect();
+ const mid = rect.left + rect.width / 2;
+ const toIdx = e.clientX < mid ? idx : idx + 1;
+ if (dragData.index !== toIdx && dragData.index !== toIdx - 1)
+ this.moveTab(dragData.index, toIdx);
+ } else {
+ // Cross-pane transfer
+ if (window.PaneManager) {
+ window.PaneManager.transferTab(dragData.paneId, dragData.tabId, this.paneId);
+ }
+ }
+ } catch (e2) { /* ignore */ }
+ });
+
+ this._tabList.appendChild(el);
+ });
+ safeCreateIcons();
+ },
+
+ _showTabContextMenu(x, y, tabId) {
+ const existing = document.getElementById('tab-context-menu');
+ if (existing) existing.remove();
+ const menu = document.createElement('div');
+ menu.id = 'tab-context-menu';
+ menu.className = 'context-menu active';
+ menu.style.left = x + 'px';
+ menu.style.top = y + 'px';
+ const isSplit = window.PaneManager && window.PaneManager.isSplit();
+ let closePaneHtml = '';
+ if (isSplit) {
+ closePaneHtml = ``;
+ }
+ menu.innerHTML = `
+
+
+
+
+
+
+
+
+ ${closePaneHtml}
+ `;
+ document.body.appendChild(menu);
+ safeCreateIcons();
+
+ menu.addEventListener('click', (e) => {
+ const action = e.target.closest('.context-menu-item')?.dataset.action;
+ if (action === 'splitRight') { if (window.PaneManager) { window.PaneManager.splitRight(); menu.remove(); return; } }
+ else if (action === 'splitDown') { if (window.PaneManager) { window.PaneManager.splitDown(); menu.remove(); return; } }
+ else if (action === 'closePane') { if (window.PaneManager) { window.PaneManager.closeActivePane(); menu.remove(); return; } }
+ else if (action === 'close') this.close(tabId);
+ else if (action === 'closeOthers') this.closeOthers(tabId);
+ else if (action === 'closeRight') this.closeRight(tabId);
+ else if (action === 'closeAll') this.closeAll();
+ menu.remove();
+ });
+ const closeMenu = () => menu.remove();
+ document.addEventListener('click', closeMenu, { once: true });
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') menu.remove(); }, { once: true });
+ },
+ };
+ return tm;
+}
+
+// =============================================================================
+// PaneManager — orchestrates panes and their TabManagers
+// =============================================================================
const PaneManager = {
panes: [],
+ tabManagers: {}, // { paneId: PaneTabManager instance }
activePaneId: 0,
- _paneCounter: 1,
layout: 'horizontal',
+ _paneCounter: 1,
- // ------------------------------------------------------------------
- // Init / Restore
- // ------------------------------------------------------------------
init() {
+ // Create default pane-0 TabManager (will bind to existing DOM)
+ this.tabManagers[0] = createPaneTabManager(0);
+ this.tabManagers[0].init();
+
const saved = localStorage.getItem('obsigate-panes');
if (saved) {
try {
@@ -31,218 +475,110 @@ const PaneManager = {
if (data.panes && data.panes.length >= 2) {
this.layout = data.layout || 'horizontal';
this._paneCounter = data.panes.length;
- this._buildGrid(data.panes.length);
+ this._buildGrid(data.panes.length, data.panes);
+ this.activePaneId = 0;
+ this._highlightActive();
+ this._applyResize();
+ // Restore per-pane state
data.panes.forEach((p, i) => {
- if (this.panes[i]) {
- this.panes[i].activeTab = p.activeTab;
- this.panes[i].width = p.width;
+ if (this.tabManagers[i] && p.tabs) {
+ const tm = this.tabManagers[i];
+ tm._tabs = p.tabs.map(t => ({ ...t }));
+ tm._activeTabId = p.activeTab;
+ tm._previewTabId = p.previewTab || null;
+ // Rebuild tab cache from tabs
+ p.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 };
+ }
+ });
+ tm._renderTabs();
+ if (tm._activeTabId) tm.activate(tm._activeTabId);
}
});
- 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;
+ setActivePane(paneId) {
+ if (this.activePaneId === paneId) return;
+ this.activePaneId = paneId;
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);
- }
+ // Sync global state from this pane's active tab
+ const tm = this.tabManagers[paneId];
+ if (tm && tm._activeTabId) {
+ const cache = tm._tabCache[tm._activeTabId];
+ if (cache) {
+ state.currentVault = cache.vault;
+ state.currentPath = cache.path;
}
- });
-
- grid.appendChild(container);
- return { id: idx, element: container, width: null, activeTab: null };
+ }
+ this._persist();
},
- _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);
+ getActiveTabManager() {
+ return this.tabManagers[this.activePaneId] || this.tabManagers[0];
},
+ getTabManager(paneId) {
+ return this.tabManagers[paneId] || this.tabManagers[0];
+ },
+
+ getActiveContentArea() {
+ const tm = this.getActiveTabManager();
+ return tm ? tm.getContentArea() : document.getElementById('content-area');
+ },
+
+ transferTab(fromPaneId, tabId, toPaneId) {
+ const fromTm = this.tabManagers[fromPaneId];
+ const toTm = this.tabManagers[toPaneId];
+ if (!fromTm || !toTm) return false;
+ const ok = fromTm.transferTabTo(tabId, toTm);
+ if (ok) {
+ toTm._renderTabs();
+ toTm.activate(tabId);
+ this._persist();
+ }
+ return ok;
+ },
+
+ isSplit() { return this.panes.length >= 2; },
+
// ------------------------------------------------------------------
// Split actions
// ------------------------------------------------------------------
splitRight() {
if (this.panes.length >= PANE_MAX) return;
this.layout = 'horizontal';
- if (this.panes.length === 0) {
- this._buildGrid(2);
- } else {
- this._addPane();
- }
+ 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();
+ if (this.panes.length === 0) this._buildGrid(2);
+ else this._addPane();
this._persist();
},
// ------------------------------------------------------------------
- // Close / collapse
+ // Close pane
// ------------------------------------------------------------------
closePane(paneId) {
if (this.panes.length <= 1) return;
const idx = this.panes.findIndex(p => p.id === paneId);
if (idx < 0) return;
+ // Clean up tab manager
+ const tm = this.tabManagers[paneId];
+ if (tm) {
+ tm._tabs = [];
+ tm._tabCache = {};
+ tm._activeTabId = null;
+ }
+ delete this.tabManagers[paneId];
this.panes[idx].element.remove();
const handles = document.querySelectorAll('.pane-resize-handle');
if (handles[idx]) handles[idx].remove();
@@ -266,9 +602,164 @@ const PaneManager = {
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);
+ for (const p of toClose.reverse()) this.closePane(p.id);
+ },
+
+ // ------------------------------------------------------------------
+ // Build / Destroy pane grid
+ // ------------------------------------------------------------------
+ _buildGrid(n, savedPanes) {
+ const wrapper = document.querySelector('.content-wrapper');
+ const origTabBar = document.getElementById('tab-bar');
+ const origContent = document.getElementById('content-area');
+ if (!wrapper || !origContent || n < 2) return;
+
+ 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';
}
+
+ 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);
+ if (dashboard && i === 0 && pane) {
+ const ca = document.getElementById('content-area-0') || origContent;
+ if (ca && !ca.querySelector('#dashboard-home')) {
+ ca.innerHTML = '';
+ ca.appendChild(dashboard);
+ dashboard.style.display = '';
+ }
+ }
+ 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.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) {
+ container.appendChild(reuseTabBar);
+ container.appendChild(reuseContent);
+ reuseContent.id = 'content-area-0';
+ reuseTabBar.id = 'tab-bar-0';
+ 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;
+ const tabList = document.createElement('div');
+ tabList.className = 'tab-list';
+ tabList.id = 'tab-list-' + idx;
+ tabBar.appendChild(tabList);
+ 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', (e) => {
+ // Don't capture clicks on tab items (they have their own handler)
+ if (e.target.closest('.tab-item')) return;
+ this.setActivePane(idx);
+ });
+
+ grid.appendChild(container);
+ return { id: idx, element: container, width: null };
+ },
+
+ _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'; }
+ this._addResizeHandle(grid, n - 2, n - 1);
+ const pane = this._createPane(n - 1, grid, null, null);
+ this.panes.push(pane);
+
+ // Create TabManager for new pane
+ const tm = createPaneTabManager(n - 1);
+ const tabBar = document.getElementById('tab-bar-' + (n - 1));
+ const tabList = document.getElementById('tab-list-' + (n - 1));
+ const contentArea = document.getElementById('content-area-' + (n - 1));
+ tm.init(tabBar, tabList, contentArea);
+ this.tabManagers[n - 1] = tm;
+
+ // Copy active tab from previous pane to new pane
+ const prevTm = this.tabManagers[this.activePaneId];
+ if (prevTm && prevTm._activeTabId) {
+ const prevTabId = prevTm._activeTabId;
+ const prevCache = prevTm._tabCache[prevTabId];
+ if (prevCache) {
+ tm.open(prevCache.vault, prevCache.path);
+ }
+ }
+
+ this.activePaneId = n - 1;
+ this._highlightActive();
+ this._persist();
+ },
+
+ _addResizeHandle(grid, leftIdx, rightIdx) {
+ 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, leftIdx, rightIdx));
+ handle.addEventListener('dblclick', () => this._resetResize(leftIdx, rightIdx));
+ grid.appendChild(handle);
},
_collapseToSingle() {
@@ -277,21 +768,27 @@ const PaneManager = {
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);
+ let tabBar = null, content = null;
+ if (pane0 && pane0.element) {
+ tabBar = pane0.element.querySelector('.tab-bar');
+ content = pane0.element.querySelector('.content-area');
}
- if (content) {
- content.id = 'content-area';
- wrapper.appendChild(content);
- }
- // Restore tab-list ID
+ if (!tabBar) tabBar = document.getElementById('tab-bar-0');
+ if (!content) content = document.getElementById('content-area-0');
+ if (tabBar) { tabBar.id = 'tab-bar'; wrapper.appendChild(tabBar); }
+ if (content) { content.id = 'content-area'; wrapper.appendChild(content); }
const tl = wrapper.querySelector('.tab-list');
if (tl) tl.id = 'tab-list';
+ // Re-init pane-0 TabManager to point to restored DOM
+ if (this.tabManagers[0]) {
+ this.tabManagers[0]._tabBar = tabBar;
+ this.tabManagers[0]._tabList = tl;
+ this.tabManagers[0]._contentArea = content;
+ }
+ // Clean up other tab managers
+ for (const key of Object.keys(this.tabManagers)) {
+ if (Number(key) !== 0) delete this.tabManagers[key];
+ }
this.panes = [];
this.activePaneId = 0;
this._persist();
@@ -306,7 +803,6 @@ const PaneManager = {
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;
@@ -321,13 +817,11 @@ const PaneManager = {
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);
},
@@ -339,14 +833,9 @@ const PaneManager = {
},
_applyResize() {
- this.panes.forEach(p => {
- if (p.width && p.element) p.element.style.flexBasis = p.width;
- });
+ 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);
@@ -354,7 +843,7 @@ const PaneManager = {
},
// ------------------------------------------------------------------
- // Persistence
+ // Persistence — save per-pane tab state
// ------------------------------------------------------------------
_persist() {
if (this.panes.length <= 1) {
@@ -363,30 +852,48 @@ const PaneManager = {
}
const data = {
layout: this.layout,
- panes: this.panes.map(p => ({ id: p.id, activeTab: p.activeTab, width: p.width })),
+ panes: this.panes.map((p) => {
+ const tm = this.tabManagers[p.id];
+ return {
+ id: p.id,
+ width: p.width,
+ activeTab: tm ? tm._activeTabId : null,
+ previewTab: tm ? tm._previewTabId : null,
+ tabs: tm ? tm._tabs.map(t => ({
+ id: t.id, vault: t.vault, path: t.path, name: t.name, icon: t.icon, preview: t.preview
+ })) : [],
+ };
+ }),
};
localStorage.setItem('obsigate-panes', JSON.stringify(data));
},
-
- // ------------------------------------------------------------------
- // Queries
- // ------------------------------------------------------------------
- isSplit() {
- return this.panes.length >= 2;
- },
-
- getPaneCount() {
- return Math.max(this.panes.length, 1);
- },
};
-// ------------------------------------------------------------------
+// =============================================================================
+// Global helpers
+// =============================================================================
+function getActiveTabManager() {
+ if (window.PaneManager && window.PaneManager.isSplit())
+ return window.PaneManager.getActiveTabManager();
+ return window._singletonTabManager;
+}
+
+function getActiveContentArea() {
+ if (window.PaneManager && window.PaneManager.isSplit())
+ return window.PaneManager.getActiveContentArea();
+ return document.getElementById('content-area');
+}
+
+// =============================================================================
// Export + auto-init
-// ------------------------------------------------------------------
+// =============================================================================
if (typeof window !== 'undefined') {
window.PaneManager = PaneManager;
+ window.getActiveTabManager = getActiveTabManager;
+ window.getActiveContentArea = getActiveContentArea;
+
document.addEventListener('DOMContentLoaded', () => {
- // Keyboard shortcuts — Ctrl+Alt+\ for split right, Ctrl+Alt+Shift+\ for split down
+ // Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.altKey && !e.shiftKey && e.key === '\\') {
e.preventDefault();
@@ -396,15 +903,20 @@ if (typeof window !== 'undefined') {
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);
+ // Init — wait for DOM + singleton TabManager to be ready
+ setTimeout(() => {
+ // Store singleton TabManager reference
+ if (typeof TabManager !== 'undefined')
+ window._singletonTabManager = TabManager;
+ PaneManager.init();
+ }, 150);
});
}
+export { PaneManager, getActiveTabManager, getActiveContentArea, createPaneTabManager };
export default PaneManager;
diff --git a/frontend/js/ui.js b/frontend/js/ui.js
index f76086e..b30262f 100644
--- a/frontend/js/ui.js
+++ b/frontend/js/ui.js
@@ -1877,70 +1877,19 @@ export const TabManager = {
_tabList: null,
_dirtyTabs: new Set(),
- // Expose on window for PaneManager cross-module communication
- _expose() {
- window.TabManager = this;
- },
-
- // Pane-aware helpers
- _getContentArea() {
- if (window.PaneManager && window.PaneManager.isSplit()) {
- return window.PaneManager.getActiveContentArea() || document.getElementById('content-area');
- }
- return document.getElementById('content-area');
- },
- _getTabBar() {
- if (window.PaneManager && window.PaneManager.isSplit()) {
- return window.PaneManager.getActiveTabBar() || document.getElementById('tab-bar');
- }
- return document.getElementById('tab-bar');
- },
- _getTabList() {
- if (window.PaneManager && window.PaneManager.isSplit()) {
- return window.PaneManager.getActiveTabList() || document.getElementById('tab-list');
- }
- return document.getElementById('tab-list');
- },
-
init() {
- this._tabBar = this._getTabBar();
- this._tabList = this._getTabList();
- this._expose();
- },
-
- /** Called by PaneManager when pane focus changes — updates DOM references */
- activatePaneTab(paneId) {
- // Update DOM references to the newly active pane
- this._tabBar = this._getTabBar();
- this._tabList = this._getTabList();
- // If this pane has a saved active tab, activate it
- const pane = window.PaneManager && window.PaneManager.panes
- ? window.PaneManager.panes.find(p => p.id === paneId)
- : null;
- if (pane && pane.activeTab && pane.activeTab !== this._activeTabId) {
- this.activate(pane.activeTab);
- }
- if (!pane || !pane.activeTab) {
- // New pane with no tab — show dashboard
- this._showDashboard();
- }
- },
-
- /** Track which pane the active tab belongs to */
- _updatePaneActiveTab() {
- if (window.PaneManager && window.PaneManager.isSplit() && this._activeTabId) {
- const pane = window.PaneManager.panes.find(p => p.id === window.PaneManager.activePaneId);
- if (pane) {
- pane.activeTab = this._activeTabId;
- window.PaneManager._persist();
- }
- }
+ this._tabBar = document.getElementById("tab-bar");
+ this._tabList = document.getElementById("tab-list");
},
/** Open a file as a preview tab (single-click).
* Replaces any existing preview tab. If the file is already
* open as a persistent tab, just activates it. */
async openPreview(vault, path) {
+ // Delegate to active pane's TabManager when split
+ if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
+ return window.getActiveTabManager().openPreview(vault, path);
+ }
const tabId = `${vault}::${path}`;
// If already open as persistent tab, just activate it
@@ -1977,6 +1926,10 @@ export const TabManager = {
/** Convert a preview tab to a persistent tab (double-click).
* If already persistent, opens a new duplicate (same file, different tab). */
async openPersistent(vault, path) {
+ // Delegate to active pane's TabManager when split
+ if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
+ return window.getActiveTabManager().openPersistent(vault, path);
+ }
const tabId = `${vault}::${path}`;
// If it's already a preview tab, convert it to persistent
@@ -2041,7 +1994,7 @@ export const TabManager = {
this._activeTabId = tabId;
this._renderTabs();
- this._updatePaneActiveTab();
+
// Load tab content
const cache = this._tabCache[tabId];
if (!cache) return;
@@ -2051,7 +2004,7 @@ export const TabManager = {
state.currentPath = cache.path;
syncActiveFileTreeItem(cache.vault, cache.path);
- const area = this._getContentArea();
+ const area = document.getElementById("content-area");
if (cache.data) {
// Use cached data
@@ -2089,6 +2042,10 @@ export const TabManager = {
/** Close a tab */
close(tabId) {
+ // Delegate to active pane's TabManager when split
+ if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
+ return window.getActiveTabManager().close(tabId);
+ }
const idx = this._tabs.findIndex(t => t.id === tabId);
if (idx === -1) return;
@@ -2111,6 +2068,9 @@ export const TabManager = {
/** Close all tabs */
closeAll() {
+ if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
+ return window.getActiveTabManager().closeAll();
+ }
this._tabs = [];
this._tabCache = {};
this._dirtyTabs.clear();
@@ -2121,6 +2081,9 @@ export const TabManager = {
/** Close tabs to the right */
closeRight(tabId) {
+ if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
+ return window.getActiveTabManager().closeRight(tabId);
+ }
const idx = this._tabs.findIndex(t => t.id === tabId);
if (idx === -1) return;
const toClose = this._tabs.slice(idx + 1);
@@ -2137,6 +2100,9 @@ export const TabManager = {
/** Close other tabs */
closeOthers(tabId) {
+ if (window.getActiveTabManager && window.PaneManager && window.PaneManager.isSplit()) {
+ return window.getActiveTabManager().closeOthers(tabId);
+ }
const tab = this._tabs.find(t => t.id === tabId);
if (!tab) return;
for (const t of this._tabs) {
@@ -2163,7 +2129,7 @@ export const TabManager = {
const cache = this._tabCache[this._activeTabId];
if (!cache) return;
- const area = this._getContentArea();
+ const area = document.getElementById("content-area");
const rendered = document.getElementById("file-rendered-content");
cache.scrollTop = area.scrollTop;
@@ -2208,7 +2174,7 @@ export const TabManager = {
},
_showDashboard() {
- const area = this._getContentArea();
+ const area = document.getElementById("content-area");
// Save dashboard DOM before clearing (it may have been removed from DOM by renderFile)
let dashboard = document.getElementById("dashboard-home");
if (!dashboard) {
diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js
index 187fef3..a98c6f8 100644
--- a/frontend/js/viewer.js
+++ b/frontend/js/viewer.js
@@ -12,10 +12,12 @@ import { openShareDialog } from './config.js';
import { cacheViewedFile, getCachedFile } from './offline.js';
import { t } from './i18n.js';
-// Pane-aware content area helper
+// Pane-aware content area helper — delegates to PaneManager when split
function getContentArea() {
- if (window.PaneManager && window.PaneManager.isSplit()) {
- return window.PaneManager.getActiveContentArea();
+ // Override set by PaneTabManager._renderFileInPane for cross-module rendering
+ if (window._activePaneContentArea) return window._activePaneContentArea;
+ if (window.PaneManager && window.PaneManager.isSplit() && window.getActiveContentArea) {
+ return window.getActiveContentArea();
}
return document.getElementById('content-area');
}
diff --git a/tests/test_pane_manager.py b/tests/test_pane_manager.py
index bd1c357..7fa72c1 100644
--- a/tests/test_pane_manager.py
+++ b/tests/test_pane_manager.py
@@ -76,14 +76,16 @@ class TestPaneManagerStaticAssets:
# Only the fallback inside getContentArea should remain
assert len(direct_refs) <= 2, f"Too many direct content-area refs: {direct_refs}"
- def test_ui_tab_manager_has_pane_helpers(self):
- """TabManager in ui.js has _getContentArea, _getTabBar, _getTabList."""
+ def test_ui_tab_manager_has_delegation(self):
+ """TabManager in ui.js delegates to getActiveTabManager() when split."""
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
content = path.read_text()
- assert "_getContentArea()" in content
- assert "_getTabBar()" in content
- assert "_getTabList()" in content
- assert "activatePaneTab(" in content
+ assert "getActiveTabManager" in content
+ assert "PaneManager.isSplit()" in content
+ # Must have delegation in entry-point methods
+ assert "window.getActiveTabManager().openPreview" in content
+ assert "window.getActiveTabManager().openPersistent" in content
+ assert "window.getActiveTabManager().close(" in content
def test_ui_context_menu_has_split_actions(self):
"""Tab context menu includes 'Diviser à droite' and 'Diviser en bas'."""