/**
* PaneManager + PaneTabManager — Multi-pane split view for ObsiGate (v2.0.0-dev)
*
* Architecture:
* 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, detachInlineEditor } 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);
// Add split buttons to the tab bar (right side)
this._ensureSplitButtons();
},
_ensureSplitButtons() {
if (!this._tabBar) return;
// Remove existing split buttons if any
const existing = this._tabBar.querySelector('.tab-split-buttons');
if (existing) existing.remove();
const btns = document.createElement('div');
btns.className = 'tab-split-buttons';
btns.innerHTML = `
`;
btns.querySelector('[data-action="split-right"]').addEventListener('click', (e) => {
e.stopPropagation();
if (window.PaneManager && window.PaneManager._hasAnyTab()) window.PaneManager.splitRight();
});
btns.querySelector('[data-action="split-down"]').addEventListener('click', (e) => {
e.stopPropagation();
if (window.PaneManager && window.PaneManager._hasAnyTab()) window.PaneManager.splitDown();
});
this._tabBar.appendChild(btns);
if (typeof safeCreateIcons === 'function') safeCreateIcons();
},
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 openHome(name = "Home") {
const tabId = "home";
const idx = this._tabs.findIndex((x) => x.id === tabId);
if (idx > 0) {
const [tab] = this._tabs.splice(idx, 1);
this._tabs.unshift(tab);
} else if (idx === -1) {
this._tabs.unshift({ id: tabId, vault: null, path: "", name, icon: "home", home: true });
this._tabCache[tabId] = { home: true, vault: null, path: "", title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: "home" };
}
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;
}
// Check if same file is open in another pane (editor lock)
if (window.PaneManager && window.PaneManager.isSplit()) {
for (const [pid, tm] of Object.entries(window.PaneManager.tabManagers)) {
if (Number(pid) === this.paneId) continue;
if (tm._tabs.find(t => t.id === tabId)) {
// File already open elsewhere — focus that pane instead
window.PaneManager.setActivePane(Number(pid));
tm.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;
// #93 — an inline edition session owns this pane's content area: release
// it before the load placeholder / read view wipes it off the DOM.
detachInlineEditor();
if (this._activeTabId && this._tabCache[this._activeTabId])
this._saveCurrentTabState();
this._activeTabId = tabId;
this._renderTabs();
const cache = this._tabCache[tabId];
if (!cache) return;
// Onglet Accueil (#158) — la page d'accueil vit dans l'onglet
if (cache.home) {
state.currentVault = null;
state.currentPath = null;
this._showDashboard();
if (history.pushState) history.pushState(null, '', '#');
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;
const closingTab = this._tabs[idx];
this._tabs.splice(idx, 1);
delete this._tabCache[tabId];
this._dirtyTabs.delete(tabId);
if (window.NowPlaying && closingTab) {
window.NowPlaying.notifyTabClosed(closingTab.vault, closingTab.path);
}
if (this._tabs.length === 0) {
this._activeTabId = null;
// If this pane has no more tabs and isn't the last pane, close it
if (window.PaneManager && window.PaneManager.isSplit() && window.PaneManager.panes.length > 1) {
this._renderTabs();
window.PaneManager.closePane(this.paneId);
return;
}
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 };
// If source is now empty and not the last pane, close it
if (this._tabs.length === 0) {
this._activeTabId = null;
if (window.PaneManager && window.PaneManager.isSplit() && window.PaneManager.panes.length > 1) {
this._renderTabs();
window.PaneManager.closePane(this.paneId);
return true;
}
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;
// Use dynamic import to call viewer.js renderFile in the right context
import('./viewer.js').then(m => {
m.renderFile(data);
window._activePaneContentArea = prev;
}).catch(() => {
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 (+ cross-pane via container drop zone)
el.addEventListener('dragstart', (e) => {
const payload = JSON.stringify({
paneId: this.paneId, tabId: tab.id, index: idx
});
// text/plain is universally supported across all browsers for drag-drop
e.dataTransfer.setData('text/plain', payload);
e.dataTransfer.effectAllowed = 'move';
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();
e.stopPropagation(); // Prevent container handler from double-firing
document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove());
const raw = e.dataTransfer.getData('text/plain');
if (!raw) return;
try {
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 pm = window.PaneManager;
const isSplit = pm && pm.isSplit();
// Build "Déplacer vers..." submenu items
let moveToHtml = '';
if (isSplit) {
const otherPanes = pm.panes.filter(p => p.id !== this.paneId);
if (otherPanes.length > 0) {
moveToHtml = '';
}
}
let closePaneHtml = '';
if (isSplit) {
closePaneHtml = ``;
}
menu.innerHTML = `
${moveToHtml}
${closePaneHtml}
`;
document.body.appendChild(menu);
safeCreateIcons();
// "Déplacer vers..." flyout submenu
const moveToEl = menu.querySelector('[data-action="moveTo"]');
if (moveToEl && isSplit) {
const otherPanes = pm.panes.filter(p => p.id !== this.paneId);
let submenu = null;
moveToEl.addEventListener('mouseenter', () => {
if (submenu) submenu.remove();
submenu = document.createElement('div');
submenu.className = 'context-menu active context-submenu';
const parentRect = moveToEl.getBoundingClientRect();
submenu.style.left = (parentRect.right + 4) + 'px';
submenu.style.top = parentRect.top + 'px';
submenu.innerHTML = otherPanes.map((p, i) =>
``
).join('');
document.body.appendChild(submenu);
safeCreateIcons();
submenu.addEventListener('click', (se) => {
const targetPane = parseInt(se.target.closest('.context-menu-item')?.dataset.pane);
if (!isNaN(targetPane)) {
pm.transferTab(this.paneId, tabId, targetPane);
menu.remove(); submenu.remove();
}
});
});
moveToEl.addEventListener('mouseleave', (e) => {
setTimeout(() => {
if (submenu && !submenu.matches(':hover')) { submenu.remove(); submenu = null; }
}, 100);
});
}
menu.addEventListener('click', (e) => {
const action = e.target.closest('.context-menu-item')?.dataset.action;
if (action === 'moveTo') return; // handled by hover
if (action === 'splitRight') {
if (pm && pm._hasAnyTab()) {
pm.splitRight();
menu.remove(); return;
}
}
else if (action === 'splitDown') {
if (pm && pm._hasAnyTab()) {
pm.splitDown();
menu.remove(); return;
}
}
else if (action === 'closePane') { if (pm) { pm.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,
layout: 'horizontal',
_paneCounter: 1,
init() {
// Create default pane-0 TabManager (will bind to existing DOM)
this.tabManagers[0] = createPaneTabManager(0);
this.tabManagers[0].init();
// Setup drop zone on content-wrapper for single-pane drag-to-split
const wrapper = document.querySelector('.content-wrapper');
if (wrapper && !wrapper.dataset.dropZoneSetup) {
wrapper.dataset.dropZoneSetup = '1';
this._setupPaneDropZone(wrapper, 0);
}
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);
this.activePaneId = 0;
this._highlightActive();
this._applyResize();
// Restore per-pane state
data.panes.forEach((p, i) => {
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);
}
});
}
} catch (e) { /* ignore corrupt state */ }
}
},
setActivePane(paneId) {
if (this.activePaneId === paneId) return;
this.activePaneId = paneId;
this._highlightActive();
// 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;
}
}
this._persist();
},
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;
// Track state BEFORE transfer (closePane may rebuild DOM)
const paneCountBefore = this.panes.length;
const ok = fromTm.transferTabTo(tabId, toTm);
if (!ok) return false;
// If source pane was auto-closed, DOM was rebuilt — re-lookup target
let actualTm = toTm;
if (this.panes.length < paneCountBefore) {
// Find which pane manager now holds the tab
for (const [pid, tm] of Object.entries(this.tabManagers)) {
if (tm._tabs.find(t => t.id === tabId)) {
actualTm = tm;
console.log('[PaneManager] source closed, tab now in pane', pid);
break;
}
}
}
actualTm._renderTabs();
actualTm.activate(tabId);
this._persist();
return ok;
},
isSplit() { return this.panes.length >= 2; },
// ------------------------------------------------------------------
// Split actions
// ------------------------------------------------------------------
splitRight() {
if (this.panes.length >= PANE_MAX) return;
// Don't split if no tab is open anywhere
if (!this._hasAnyTab()) 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;
if (!this._hasAnyTab()) return;
this.layout = 'vertical';
if (this.panes.length === 0) {
this._buildGrid(2);
} else {
this._addPane();
}
this._persist();
},
/** Check if any pane has at least one tab */
_hasAnyTab() {
if (!this.isSplit()) {
// Check singleton in single-pane mode
if (window._singletonTabManager && window._singletonTabManager._tabs.length > 0)
return true;
return false;
}
for (const tm of Object.values(this.tabManagers)) {
if (tm._tabs && tm._tabs.length > 0) return true;
}
return false;
},
/** Copy the active tab to the newly created last pane */
_copyActiveTabToNewPane() {
const newPaneId = this.panes.length - 1;
const srcTm = this.tabManagers[this.activePaneId];
const dstTm = this.tabManagers[newPaneId];
if (!srcTm || !dstTm || !srcTm._activeTabId) return;
const cache = srcTm._tabCache[srcTm._activeTabId];
if (cache) {
setTimeout(() => {
dstTm.open(cache.vault, cache.path);
}, 150);
}
},
// ------------------------------------------------------------------
// 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();
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();
}
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);
},
// ------------------------------------------------------------------
// 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';
this._applyGridTemplate(grid, n);
// #110 — keep playing media alive across a pane-grid rebuild.
if (window.NowPlaying) window.NowPlaying.handleRender(wrapper, null);
wrapper.innerHTML = '';
wrapper.appendChild(grid);
this.panes = [];
// Keep pane-0 TabManager (already created in init)
for (const key of Object.keys(this.tabManagers)) {
if (Number(key) >= n) delete this.tabManagers[key];
}
// 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')) {
ca.innerHTML = '';
ca.appendChild(dashboard);
dashboard.style.display = '';
}
}
this.panes.push(pane);
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
if (!savedPanes && window._singletonTabManager && window._singletonTabManager._tabs.length > 0) {
const stm = window._singletonTabManager;
const ptm = this.tabManagers[0];
if (ptm && ptm._tabs.length === 0) {
ptm._tabs = stm._tabs.map(t => ({ ...t }));
ptm._tabCache = { ...stm._tabCache };
ptm._activeTabId = stm._activeTabId;
ptm._previewTabId = stm._previewTabId;
stm._tabs = []; stm._tabCache = {};
stm._activeTabId = null; stm._previewTabId = null;
setTimeout(() => {
ptm._renderTabs();
if (ptm._activeTabId) ptm.activate(ptm._activeTabId);
}, 100);
}
}
},
/** 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);
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) => {
if (e.target.closest('.tab-item')) return;
this.setActivePane(idx);
});
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;
container.addEventListener('dragenter', (e) => {
_dragCounter++;
e.preventDefault(); // Accept the drop unconditionally
});
container.addEventListener('dragover', (e) => {
e.preventDefault(); // Required for drop to work
e.dataTransfer.dropEffect = 'move';
// Detect edge zones for drag-to-split (only in single-pane mode)
const rect = container.getBoundingClientRect();
const relX = e.clientX - rect.left;
const relY = e.clientY - rect.top;
const isSingle = !this.isSplit();
// Clean up previous indicators
container.querySelectorAll('.pane-drop-zone-indicator').forEach(d => d.remove());
const tabBar = container.querySelector('.tab-bar');
if (tabBar) tabBar.classList.remove('drop-target');
if (isSingle && relX > rect.width * 0.72) {
const indicator = document.createElement('div');
indicator.className = 'pane-drop-zone-indicator pane-drop-zone-right';
container.appendChild(indicator);
} else if (isSingle && relY > rect.height * 0.72) {
const indicator = document.createElement('div');
indicator.className = 'pane-drop-zone-indicator pane-drop-zone-bottom';
container.appendChild(indicator);
} else {
// Show tab-bar highlight for cross-pane transfer
if (tabBar) tabBar.classList.add('drop-target');
}
});
container.addEventListener('dragleave', (e) => {
_dragCounter--;
if (_dragCounter > 0) return;
container.querySelectorAll('.pane-drop-zone-indicator').forEach(d => d.remove());
const tabBar = container.querySelector('.tab-bar');
if (tabBar) tabBar.classList.remove('drop-target');
});
container.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
_dragCounter = 0;
container.querySelectorAll('.pane-drop-zone-indicator').forEach(d => d.remove());
const tabBar = container.querySelector('.tab-bar');
if (tabBar) tabBar.classList.remove('drop-target');
// Get drag data (text/plain is universally supported)
const raw = e.dataTransfer.getData('text/plain');
console.log('[PaneManager] drop fired, raw data:', raw ? raw.substring(0, 80) : 'null');
if (!raw) return;
// --- Determine drop zone ---
const rect = container.getBoundingClientRect();
const relX = e.clientX - rect.left;
const relY = e.clientY - rect.top;
const isSingle = !this.isSplit();
const onRightEdge = isSingle && relX > rect.width * 0.72;
const onBottomEdge = isSingle && relY > rect.height * 0.72;
console.log('[PaneManager] drop at', relX.toFixed(0), relY.toFixed(0),
'rect', rect.width.toFixed(0), 'x', rect.height.toFixed(0),
'isSingle', isSingle, 'paneId', paneId,
'onRightEdge', onRightEdge, 'onBottomEdge', onBottomEdge);
// --- Tree drag (file from sidebar) ---
if (raw.startsWith('tree:')) {
try {
const treeData = JSON.parse(raw.substring(5));
if (!treeData.vault || !treeData.path) return;
console.log('[PaneManager] tree drop vault=', treeData.vault, 'path=', treeData.path);
if (onRightEdge) {
console.log('[PaneManager] → tree splitRight');
this.splitRight();
setTimeout(() => {
const newPaneId = this.panes.length - 1;
const tm = this.getTabManager(newPaneId);
if (tm) {
this.setActivePane(newPaneId);
tm.openPreview(treeData.vault, treeData.path);
}
}, 200);
} else if (onBottomEdge) {
console.log('[PaneManager] → tree splitDown');
this.splitDown();
setTimeout(() => {
const newPaneId = this.panes.length - 1;
const tm = this.getTabManager(newPaneId);
if (tm) {
this.setActivePane(newPaneId);
tm.openPreview(treeData.vault, treeData.path);
}
}, 200);
} else {
console.log('[PaneManager] → tree open in pane', paneId);
const tm = this.getTabManager(paneId);
if (tm) {
this.setActivePane(paneId);
tm.openPreview(treeData.vault, treeData.path);
}
}
} catch (e2) { console.debug('[PaneManager] tree drop parse error:', e2); }
return;
}
// --- Tab drag ---
let dragData;
try { dragData = JSON.parse(raw); } catch (e2) { console.log('[PaneManager] JSON parse error:', e2); return; }
if (dragData.paneId == null || dragData.tabId == null) { console.log('[PaneManager] missing paneId/tabId'); return; }
if (onRightEdge) {
console.log('[PaneManager] → splitRight');
this.splitRight();
setTimeout(() => {
const newPaneId = this.panes.length - 1;
console.log('[PaneManager] transferring tab', dragData.tabId, 'to pane', newPaneId);
this.transferTab(dragData.paneId, dragData.tabId, newPaneId);
}, 200);
} else if (onBottomEdge) {
console.log('[PaneManager] → splitDown');
this.splitDown();
setTimeout(() => {
const newPaneId = this.panes.length - 1;
this.transferTab(dragData.paneId, dragData.tabId, newPaneId);
}, 200);
} else if (dragData.paneId !== paneId) {
console.log('[PaneManager] → cross-pane transfer', dragData.paneId, '→', paneId);
this.transferTab(dragData.paneId, dragData.tabId, paneId);
} else {
console.log('[PaneManager] → same pane, no edge — letting tab handler deal');
}
});
},
_addPane() {
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'; }
// 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
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 active 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);
}
}
// 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';
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() {
const grid = document.querySelector('.pane-grid');
if (!grid) return;
const wrapper = grid.parentElement;
const pane0 = this.panes[0];
// #110 — keep playing media alive across a pane collapse.
if (window.NowPlaying) window.NowPlaying.handleRender(wrapper, null);
wrapper.innerHTML = '';
let tabBar = null, content = null;
if (pane0 && pane0.element) {
tabBar = pane0.element.querySelector('.tab-bar');
content = pane0.element.querySelector('.content-area');
}
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';
// Migrate tabs back to singleton TabManager
const ptm = this.tabManagers[0];
if (ptm && window._singletonTabManager) {
const stm = window._singletonTabManager;
stm._tabs = ptm._tabs.map(t => ({ ...t }));
stm._tabCache = { ...ptm._tabCache };
stm._activeTabId = ptm._activeTabId;
stm._previewTabId = ptm._previewTabId;
stm._tabBar = tabBar;
stm._tabList = tl;
stm._renderTabs();
}
// 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();
},
// ------------------------------------------------------------------
// 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; });
},
_highlightActive() {
this.panes.forEach((p, i) => {
if (p.element) p.element.classList.toggle('pane-active', i === this.activePaneId);
});
},
// ------------------------------------------------------------------
// Persistence — save per-pane tab state
// ------------------------------------------------------------------
_persist() {
if (this.panes.length <= 1) {
localStorage.removeItem('obsigate-panes');
return;
}
const data = {
layout: this.layout,
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));
},
};
// =============================================================================
// 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
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.altKey && !e.shiftKey && e.key === '\\') {
e.preventDefault();
if (PaneManager._hasAnyTab()) {
PaneManager.splitRight();
}
}
if (e.ctrlKey && e.altKey && e.shiftKey && e.key === '\\') {
e.preventDefault();
if (PaneManager._hasAnyTab()) {
PaneManager.splitDown();
}
}
// Close active pane
if (e.ctrlKey && e.altKey && !e.shiftKey && (e.key === 'w' || e.key === 'W') && PaneManager.isSplit()) {
e.preventDefault();
PaneManager.closeActivePane();
}
// Close all other panes
if (e.ctrlKey && e.shiftKey && e.altKey && (e.key === 'w' || e.key === 'W') && PaneManager.isSplit()) {
e.preventDefault();
PaneManager.closeOtherPanes();
}
// Navigate between panes
if (e.ctrlKey && e.altKey && PaneManager.isSplit()) {
if (e.key === 'ArrowRight') {
e.preventDefault();
const next = (PaneManager.activePaneId + 1) % PaneManager.panes.length;
PaneManager.setActivePane(next);
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
const prev = (PaneManager.activePaneId - 1 + PaneManager.panes.length) % PaneManager.panes.length;
PaneManager.setActivePane(prev);
} else if (e.key === 'ArrowDown' && PaneManager.layout === 'vertical') {
e.preventDefault();
const next = (PaneManager.activePaneId + 1) % PaneManager.panes.length;
PaneManager.setActivePane(next);
} else if (e.key === 'ArrowUp' && PaneManager.layout === 'vertical') {
e.preventDefault();
const prev = (PaneManager.activePaneId - 1 + PaneManager.panes.length) % PaneManager.panes.length;
PaneManager.setActivePane(prev);
}
}
});
// Init — wait for DOM + singleton TabManager to be ready
setTimeout(() => {
// Use window.TabManager (set by ui.js init) as the singleton fallback
if (!window._singletonTabManager && window.TabManager)
window._singletonTabManager = window.TabManager;
PaneManager.init();
}, 150);
});
}
export { PaneManager, getActiveTabManager, getActiveContentArea, createPaneTabManager };
export default PaneManager;