feat: drop cross-pane fonctionnel + drag-to-split (bord droit/bas)
CI / lint (push) Failing after 12s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 16s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled

- _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
This commit is contained in:
2026-07-27 22:16:04 -04:00
parent e5cd656c6e
commit a842d68038
2 changed files with 119 additions and 1 deletions
+86 -1
View File
@@ -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;
+33
View File
@@ -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 {