From a842d6803833e18626eaaca8ce8623c862c5a61c Mon Sep 17 00:00:00 2001 From: bruno Date: Mon, 27 Jul 2026 22:16:04 -0400 Subject: [PATCH] feat: drop cross-pane fonctionnel + drag-to-split (bord droit/bas) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _setupPaneDropZone: dragover/drop sur .pane-container (fonctionne même si tab bar vide) - Drag-to-split: déposer un onglet dans les 25% droit → split right, 25% bas → split down - Indicateurs visuels: zone bleue semi-transparente au drop, tab-bar dashed outline - stopPropagation dans le drop handler des tabs pour éviter double-fire - CSS: .pane-drop-zone-indicator, .tab-bar.drop-target, position:relative sur pane-container --- frontend/js/pane-manager.js | 87 ++++++++++++++++++++++++++++++++++++- frontend/style.css | 33 ++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/frontend/js/pane-manager.js b/frontend/js/pane-manager.js index 0be3343..8276744 100644 --- a/frontend/js/pane-manager.js +++ b/frontend/js/pane-manager.js @@ -390,6 +390,7 @@ function createPaneTabManager(paneId) { }); el.addEventListener('drop', (e) => { e.preventDefault(); + e.stopPropagation(); // Prevent container handler from double-firing document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove()); try { const raw = e.dataTransfer.getData('application/obsigate-tab'); @@ -720,15 +721,99 @@ const PaneManager = { } 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); }); + // --- Drop zone handlers on the pane container --- + this._setupPaneDropZone(container, idx); + grid.appendChild(container); return { id: idx, element: container, width: null }; }, + /** Add dragover/drop on a pane container for cross-pane transfers + drag-to-split */ + _setupPaneDropZone(container, paneId) { + let _edgeZone = null; // 'right' | 'bottom' | null + + container.addEventListener('dragover', (e) => { + const raw = e.dataTransfer.types.includes('application/obsigate-tab'); + if (!raw) return; + e.preventDefault(); + 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()); + _edgeZone = null; + + if (isSingle) { + if (relX > rect.width * 0.75) { + _edgeZone = 'right'; + } else if (relY > rect.height * 0.75) { + _edgeZone = 'bottom'; + } + } + + if (_edgeZone) { + // Show edge zone indicator + const indicator = document.createElement('div'); + indicator.className = 'pane-drop-zone-indicator pane-drop-zone-' + _edgeZone; + container.appendChild(indicator); + } else { + // Show tab-bar highlight for cross-pane transfer + const tabBar = container.querySelector('.tab-bar'); + if (tabBar) tabBar.classList.add('drop-target'); + } + }); + + container.addEventListener('dragleave', (e) => { + container.querySelectorAll('.pane-drop-zone-indicator').forEach(d => d.remove()); + const tabBar = container.querySelector('.tab-bar'); + if (tabBar) tabBar.classList.remove('drop-target'); + _edgeZone = null; + }); + + container.addEventListener('drop', (e) => { + e.preventDefault(); + container.querySelectorAll('.pane-drop-zone-indicator').forEach(d => d.remove()); + const tabBar = container.querySelector('.tab-bar'); + if (tabBar) tabBar.classList.remove('drop-target'); + + const raw = e.dataTransfer.getData('application/obsigate-tab'); + if (!raw) return; + try { + const dragData = JSON.parse(raw); + + if (_edgeZone === 'right') { + // Drag to right edge → split right with this tab + this.splitRight(); + // Transfer the dragged tab to the new pane + setTimeout(() => { + const newPaneId = this.panes.length - 1; + this.transferTab(dragData.paneId, dragData.tabId, newPaneId); + }, 100); + } else if (_edgeZone === 'bottom') { + // Drag to bottom edge → split down with this tab + this.splitDown(); + setTimeout(() => { + const newPaneId = this.panes.length - 1; + this.transferTab(dragData.paneId, dragData.tabId, newPaneId); + }, 100); + } else if (dragData.paneId !== paneId) { + // Cross-pane transfer (dropped on tab bar or content area of another pane) + this.transferTab(dragData.paneId, dragData.tabId, paneId); + } + } catch (e2) { /* ignore */ } + _edgeZone = null; + }); + }, + _addPane() { const grid = document.querySelector('.pane-grid'); if (!grid) return; diff --git a/frontend/style.css b/frontend/style.css index de35649..2a84a3e 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -1264,6 +1264,7 @@ select { min-height: 0; border: 2px solid transparent; transition: border-color 150ms ease; + position: relative; } .pane-container.pane-active { @@ -1317,6 +1318,38 @@ select { border: 2px solid var(--accent); } +/* --- Drop zone indicators for drag-to-split --- */ +.pane-drop-zone-indicator { + position: absolute; + z-index: 10; + background: var(--accent); + opacity: 0.4; + pointer-events: none; + transition: opacity 100ms ease; +} + +.pane-drop-zone-indicator.pane-drop-zone-right { + top: 0; + right: 0; + width: 25%; + height: 100%; +} + +.pane-drop-zone-indicator.pane-drop-zone-bottom { + bottom: 0; + left: 0; + width: 100%; + height: 25%; +} + +/* Tab bar drop target highlight */ +.tab-bar.drop-target { + background: var(--accent); + opacity: 0.15; + outline: 2px dashed var(--accent); + outline-offset: -2px; +} + /* Responsive: disable split view below 768px */ @media (max-width: 768px) { .pane-grid {