feat: split view pro — TabManager indépendant par panneau avec drag & drop cross-pane
CI / lint (push) Failing after 10s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 17s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled

- PaneTabManager factory: chaque panneau a sa propre instance avec ses propres onglets
- Drag & drop d'onglets entre panneaux (application/obsigate-tab dataTransfer)
- Split copie l'onglet actif dans le nouveau panneau
- TabManager singleton: délégation automatique vers le panneau actif en mode split
- Persistance complète par panneau (tabs, activeTab, previewTab)
- viewer.js: getContentArea() priorise window._activePaneContentArea
- 307 tests verts, déployé sur 192.168.30.101:2020
This commit is contained in:
2026-07-27 21:37:04 -04:00
parent b27b38d32b
commit ed4c1dcf07
4 changed files with 783 additions and 301 deletions
File diff suppressed because it is too large Load Diff
+27 -61
View File
@@ -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) {
+5 -3
View File
@@ -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');
}
+8 -6
View File
@@ -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'."""