@mintplayer/ng-bootstrap 20.5.0 → 20.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dock/index.d.ts CHANGED
@@ -150,6 +150,11 @@ declare class MintDockManagerElement extends HTMLElement {
150
150
  private onGlobalDragEnd;
151
151
  private updateDraggedFloatingPositionFromPoint;
152
152
  private updatePaneDragDropTargetFromPoint;
153
+ private isPointerOverSourceHeader;
154
+ private isPointWithinBounds;
155
+ private ensureHeaderDragPlaceholder;
156
+ private updateHeaderDragPlaceholderPosition;
157
+ private clearHeaderDragPlaceholder;
153
158
  private startDragPointerTracking;
154
159
  private stopDragPointerTracking;
155
160
  private onDragMouseMove;
@@ -159,6 +164,7 @@ declare class MintDockManagerElement extends HTMLElement {
159
164
  private computeHeaderInsertIndex;
160
165
  private reorderPaneInLocationAtIndex;
161
166
  private onDragTouchEnd;
167
+ private finalizeDropFromPoint;
162
168
  private clearPendingDragEndTimeout;
163
169
  private scheduleDeferredDragEnd;
164
170
  private onDrop;
@@ -1222,7 +1222,6 @@ class MintDockManagerElement extends HTMLElement {
1222
1222
  this.handleFloatingResizeMove(event);
1223
1223
  }
1224
1224
  if (this.floatingDragState && event.pointerId === this.floatingDragState.pointerId) {
1225
- console.warn('state', this.floatingDragState);
1226
1225
  this.handleFloatingDragMove(event);
1227
1226
  }
1228
1227
  }
@@ -1265,6 +1264,8 @@ class MintDockManagerElement extends HTMLElement {
1265
1264
  top,
1266
1265
  width,
1267
1266
  height,
1267
+ startClientX: event.clientX,
1268
+ startClientY: event.clientY,
1268
1269
  };
1269
1270
  }
1270
1271
  clearPendingTabDragMetrics() {
@@ -1285,7 +1286,9 @@ class MintDockManagerElement extends HTMLElement {
1285
1286
  this.shadowRoot?.appendChild(ghost);
1286
1287
  // Use the ghost element as the drag image.
1287
1288
  // The offset is set to where the user's cursor is on the original element.
1288
- event.dataTransfer.setDragImage(ghost, event.offsetX, event.offsetY);
1289
+ const dragImgOffsetX = Number.isFinite(event.offsetX) ? event.offsetX : 0;
1290
+ const dragImgOffsetY = Number.isFinite(event.offsetY) ? event.offsetY : 0;
1291
+ event.dataTransfer.setDragImage(ghost, dragImgOffsetX, dragImgOffsetY);
1289
1292
  // The ghost element is no longer needed after the drag image is set.
1290
1293
  // We defer its removal to ensure the browser has captured it.
1291
1294
  setTimeout(() => ghost.remove(), 0);
@@ -1296,6 +1299,7 @@ class MintDockManagerElement extends HTMLElement {
1296
1299
  const headerBounds = headerRect
1297
1300
  ? { left: headerRect.left, top: headerRect.top, right: headerRect.right, bottom: headerRect.bottom }
1298
1301
  : null;
1302
+ const metrics = this.pendingTabDragMetrics;
1299
1303
  this.dragState = {
1300
1304
  pane,
1301
1305
  sourcePath: this.clonePath(sourcePath),
@@ -1305,9 +1309,27 @@ class MintDockManagerElement extends HTMLElement {
1305
1309
  dropHandled: false,
1306
1310
  sourceStackEl: stackEl,
1307
1311
  sourceHeaderBounds: headerBounds,
1308
- startClientX: Number.isFinite(event.clientX) ? event.clientX : undefined,
1309
- startClientY: Number.isFinite(event.clientY) ? event.clientY : undefined,
1312
+ startClientX: metrics && Number.isFinite(metrics.startClientX)
1313
+ ? metrics.startClientX
1314
+ : Number.isFinite(event.clientX)
1315
+ ? event.clientX
1316
+ : undefined,
1317
+ startClientY: metrics && Number.isFinite(metrics.startClientY)
1318
+ ? metrics.startClientY
1319
+ : Number.isFinite(event.clientY)
1320
+ ? event.clientY
1321
+ : undefined,
1310
1322
  };
1323
+ // Seed last known pointer position from pointerdown metrics to avoid (0,0) glitches in Firefox
1324
+ if (this.dragState.startClientX !== undefined &&
1325
+ this.dragState.startClientY !== undefined &&
1326
+ Number.isFinite(this.dragState.startClientX) &&
1327
+ Number.isFinite(this.dragState.startClientY)) {
1328
+ this.lastDragPointerPosition = {
1329
+ x: this.dragState.startClientX,
1330
+ y: this.dragState.startClientY,
1331
+ };
1332
+ }
1311
1333
  // Prefer the pointer offset relative to the dragged tab to avoid jumps on conversion
1312
1334
  if (Number.isFinite(event.offsetX)) {
1313
1335
  this.dragState.pointerOffsetX = event.offsetX;
@@ -1319,6 +1341,28 @@ class MintDockManagerElement extends HTMLElement {
1319
1341
  this.startDragPointerTracking();
1320
1342
  event.dataTransfer.effectAllowed = 'move';
1321
1343
  event.dataTransfer.setData('text/plain', pane);
1344
+ // Preferred UX: if the dragged tab is the only one in its stack,
1345
+ // immediately convert to a floating window unless it is already the
1346
+ // only pane in a floating window (this case is handled by reuse logic).
1347
+ if (this.dragState && this.dragState.floatingIndex !== null && this.dragState.floatingIndex < 0) {
1348
+ const loc = this.resolveStackLocation(this.dragState.sourcePath);
1349
+ if (loc && Array.isArray(loc.node.panes) && loc.node.panes.length === 1) {
1350
+ let shouldConvert = false;
1351
+ if (loc.context === "docked") {
1352
+ shouldConvert = true;
1353
+ }
1354
+ else if (loc.context === "floating") {
1355
+ const floating = this.floatingLayouts[loc.index];
1356
+ const totalPanes = floating && floating.root ? this.countPanesInTree(floating.root) : 0;
1357
+ shouldConvert = totalPanes > 1; // not the only pane in this floating window
1358
+ }
1359
+ if (shouldConvert) {
1360
+ const startX = Number.isFinite(event.clientX) ? event.clientX : (this.dragState.startClientX ?? 0);
1361
+ const startY = Number.isFinite(event.clientY) ? event.clientY : (this.dragState.startClientY ?? 0);
1362
+ this.convertPendingTabDragToFloating(startX, startY);
1363
+ }
1364
+ }
1365
+ }
1322
1366
  }
1323
1367
  preparePaneDragSource(path, pane, stackEl, event) {
1324
1368
  const location = this.resolveStackLocation(path);
@@ -1400,6 +1444,7 @@ class MintDockManagerElement extends HTMLElement {
1400
1444
  const state = this.dragState;
1401
1445
  this.dragState = null;
1402
1446
  this.hideDropIndicator();
1447
+ this.clearHeaderDragPlaceholder();
1403
1448
  this.stopDragPointerTracking();
1404
1449
  this.lastDragPointerPosition = null;
1405
1450
  if (state && state.floatingIndex !== null && !state.dropHandled) {
@@ -1435,6 +1480,21 @@ class MintDockManagerElement extends HTMLElement {
1435
1480
  return;
1436
1481
  }
1437
1482
  const path = this.parsePath(stack.dataset['path']);
1483
+ // While reordering within the same header, suppress the joystick/indicator entirely
1484
+ if (this.dragState &&
1485
+ this.dragState.floatingIndex !== null &&
1486
+ this.dragState.floatingIndex < 0 &&
1487
+ path &&
1488
+ this.pathsEqual(path, this.dragState.sourcePath)) {
1489
+ const px = (point ? point.clientX : event.clientX);
1490
+ const py = (point ? point.clientY : event.clientY);
1491
+ if (Number.isFinite(px) && Number.isFinite(py) && this.isPointerOverSourceHeader(px, py)) {
1492
+ // Drive live reorder using the unified path so we update instantly.
1493
+ this.updatePaneDragDropTargetFromPoint(px, py);
1494
+ this.hideDropIndicator();
1495
+ return;
1496
+ }
1497
+ }
1438
1498
  // If the hovered stack changed, clear any sticky zone from the previous
1439
1499
  // target before computing the new zone.
1440
1500
  if (this.dropJoystickTarget && this.dropJoystickTarget !== stack) {
@@ -1450,10 +1510,10 @@ class MintDockManagerElement extends HTMLElement {
1450
1510
  if (!this.dragState) {
1451
1511
  return;
1452
1512
  }
1453
- const { clientX, clientY, screenX, screenY } = event;
1513
+ const { clientX, clientY } = event;
1454
1514
  const hasValidCoordinates = Number.isFinite(clientX) &&
1455
1515
  Number.isFinite(clientY) &&
1456
- !(clientX === 0 && clientY === 0 && screenX === 0 && screenY === 0);
1516
+ !(clientX === 0 && clientY === 0);
1457
1517
  if (hasValidCoordinates) {
1458
1518
  this.lastDragPointerPosition = { x: clientX, y: clientY };
1459
1519
  this.updateDraggedFloatingPositionFromPoint(clientX, clientY);
@@ -1477,7 +1537,31 @@ class MintDockManagerElement extends HTMLElement {
1477
1537
  this.updateDraggedFloatingPosition(event);
1478
1538
  }
1479
1539
  onGlobalDragEnd() {
1480
- this.hideDropIndicator();
1540
+ // Attempt to finalize a drop even if the drop event doesn't reach us (Firefox/edge cases)
1541
+ const state = this.dragState;
1542
+ const pos = this.lastDragPointerPosition;
1543
+ if (state && pos) {
1544
+ const stack = this.findStackAtPoint(pos.x, pos.y);
1545
+ const joystickVisible = this.dropJoystick.dataset['visible'] === 'true';
1546
+ const joystickPath = this.parsePath(this.dropJoystick.dataset['path']);
1547
+ const joystickTarget = this.dropJoystickTarget;
1548
+ const joystickTargetPath = joystickTarget ? this.parsePath(joystickTarget.dataset['path']) : null;
1549
+ const path = stack ? this.parsePath(stack.dataset['path']) : (joystickPath ?? joystickTargetPath);
1550
+ const joystickZone = this.dropJoystick.dataset['zone'];
1551
+ const zone = this.isDropZone(joystickZone)
1552
+ ? joystickZone
1553
+ : (stack ? this.computeDropZone(stack, { clientX: pos.x, clientY: pos.y }, null) : null);
1554
+ if (path && this.isDropZone(zone)) {
1555
+ this.handleDrop(path, zone);
1556
+ this.hideDropIndicator();
1557
+ if (this.dragState) {
1558
+ this.dragState.dropHandled = true;
1559
+ }
1560
+ }
1561
+ }
1562
+ else {
1563
+ this.hideDropIndicator();
1564
+ }
1481
1565
  if (!this.dragState) {
1482
1566
  this.clearPendingTabDragMetrics();
1483
1567
  return;
@@ -1492,17 +1576,22 @@ class MintDockManagerElement extends HTMLElement {
1492
1576
  if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) {
1493
1577
  return;
1494
1578
  }
1495
- // If we are still dragging a tab inside its header, only convert to floating once we leave the header bounds.
1579
+ // Ignore obviously bogus coordinates sometimes seen during HTML5 drag
1580
+ if (clientX === 0 && clientY === 0) {
1581
+ return;
1582
+ }
1583
+ // If still dragging a tab inside its header, only convert to floating once we leave the header.
1496
1584
  if (this.dragState.floatingIndex !== null && this.dragState.floatingIndex < 0) {
1497
1585
  const b = this.dragState.sourceHeaderBounds;
1498
1586
  const sx = this.dragState.startClientX ?? clientX;
1499
1587
  const sy = this.dragState.startClientY ?? clientY;
1500
1588
  const dist = Math.hypot(clientX - sx, clientY - sy);
1501
- const threshold = 4; // pixels to move before converting
1502
- let insideHeader = false;
1503
- if (b) {
1504
- insideHeader = clientX >= b.left && clientX <= b.right && clientY >= b.top && clientY <= b.bottom;
1505
- }
1589
+ const threshold = 8; // pixels to move before converting (tuned up)
1590
+ // Default to inside while bounds are unknown to avoid premature floating
1591
+ let insideHeader = true;
1592
+ const insideByBounds = b ? this.isPointWithinBounds(b, clientX, clientY) : true;
1593
+ const insideByHitTest = this.isPointerOverSourceHeader(clientX, clientY);
1594
+ insideHeader = insideByBounds || insideByHitTest;
1506
1595
  if (!insideHeader && dist > threshold) {
1507
1596
  // Convert to floating now using current pointer position
1508
1597
  this.convertPendingTabDragToFloating(clientX, clientY);
@@ -1540,6 +1629,8 @@ class MintDockManagerElement extends HTMLElement {
1540
1629
  if (this.dropJoystick.dataset['visible'] !== 'true') {
1541
1630
  this.hideDropIndicator();
1542
1631
  }
1632
+ // Also ensure any in-header placeholder is cleared when not over a stack
1633
+ this.clearHeaderDragPlaceholder();
1543
1634
  return;
1544
1635
  }
1545
1636
  // If we moved to a different target stack, reset any sticky zone so
@@ -1548,10 +1639,122 @@ class MintDockManagerElement extends HTMLElement {
1548
1639
  delete this.dropJoystick.dataset['zone'];
1549
1640
  this.updateDropJoystickActiveZone(null);
1550
1641
  }
1642
+ // Previous behavior hid the indicator and returned early here; instead,
1643
+ // allow the live-reorder branch below to handle in-header drags.
1644
+ // While dragging within the same header, show a placeholder and suppress joystick/indicator
1645
+ if (this.dragState &&
1646
+ this.dragState.floatingIndex !== null &&
1647
+ this.dragState.floatingIndex < 0 &&
1648
+ path &&
1649
+ this.pathsEqual(path, this.dragState.sourcePath)) {
1650
+ const inHeaderByBounds = !!this.dragState.sourceHeaderBounds && this.isPointWithinBounds(this.dragState.sourceHeaderBounds, clientX, clientY);
1651
+ const inHeaderByHitTest = this.isPointerOverSourceHeader(clientX, clientY);
1652
+ if (inHeaderByBounds || inHeaderByHitTest) {
1653
+ const header = stack.querySelector('.dock-stack__header');
1654
+ if (header) {
1655
+ // Ensure placeholder exists and move it as the pointer moves
1656
+ this.ensureHeaderDragPlaceholder(header, this.dragState.pane);
1657
+ const idx = this.computeHeaderInsertIndex(header, clientX);
1658
+ if (this.dragState.liveReorderIndex !== idx) {
1659
+ this.updateHeaderDragPlaceholderPosition(header, idx);
1660
+ // Keep model reordering until drop; only move the placeholder now
1661
+ this.dragState.liveReorderIndex = idx;
1662
+ }
1663
+ }
1664
+ this.hideDropIndicator();
1665
+ return;
1666
+ }
1667
+ }
1668
+ // Leaving the header: ensure any placeholder is removed immediately
1669
+ this.clearHeaderDragPlaceholder();
1551
1670
  const zoneHint = this.findDropZoneByPoint(clientX, clientY);
1552
1671
  const zone = this.computeDropZone(stack, { clientX, clientY }, zoneHint);
1553
1672
  this.showDropIndicator(stack, zone);
1554
1673
  }
1674
+ // Returns true when the pointer is currently over the source stack's header (tab strip)
1675
+ isPointerOverSourceHeader(clientX, clientY) {
1676
+ const state = this.dragState;
1677
+ if (!state) {
1678
+ return false;
1679
+ }
1680
+ const stackEl = state.sourceStackEl ?? null;
1681
+ const header = stackEl?.querySelector('.dock-stack__header');
1682
+ if (!header) {
1683
+ // Be conservative: if we cannot resolve the header, treat as inside
1684
+ return true;
1685
+ }
1686
+ const sr = this.shadowRoot;
1687
+ const elements = sr ? sr.elementsFromPoint(clientX, clientY) : [];
1688
+ for (const el of elements) {
1689
+ if (el instanceof HTMLElement && header.contains(el)) {
1690
+ return true;
1691
+ }
1692
+ }
1693
+ return false;
1694
+ }
1695
+ isPointWithinBounds(bounds, x, y) {
1696
+ return x >= bounds.left && x <= bounds.right && y >= bounds.top && y <= bounds.bottom;
1697
+ }
1698
+ // Ensure a placeholder tab exists during in-header drag and hide the real dragged tab visually
1699
+ ensureHeaderDragPlaceholder(header, pane) {
1700
+ if (this.dragState?.placeholderHeader === header && this.dragState.placeholderEl) {
1701
+ return;
1702
+ }
1703
+ const dragged = Array.from(header.querySelectorAll('.dock-tab')).find((t) => t.dataset['pane'] === pane) ?? null;
1704
+ if (!dragged) {
1705
+ return;
1706
+ }
1707
+ // Create placeholder
1708
+ const placeholder = this.documentRef.createElement('button');
1709
+ placeholder.type = 'button';
1710
+ placeholder.classList.add('dock-tab');
1711
+ placeholder.dataset['placeholder'] = 'true';
1712
+ // Keep the placeholder visually empty but reserving the same width
1713
+ placeholder.textContent = '';
1714
+ placeholder.setAttribute('aria-hidden', 'true');
1715
+ placeholder.style.width = `${dragged.offsetWidth}px`;
1716
+ // Hide the original dragged tab so it doesn't duplicate visually and free up its slot
1717
+ dragged.style.display = 'none';
1718
+ // Insert placeholder in the original position of the dragged tab
1719
+ header.insertBefore(placeholder, dragged);
1720
+ if (this.dragState) {
1721
+ this.dragState.placeholderHeader = header;
1722
+ this.dragState.placeholderEl = placeholder;
1723
+ }
1724
+ }
1725
+ // Move the placeholder to the computed target index within the header
1726
+ updateHeaderDragPlaceholderPosition(header, targetIndex) {
1727
+ const placeholder = this.dragState?.placeholderEl ?? null;
1728
+ if (!placeholder) {
1729
+ return;
1730
+ }
1731
+ const draggedPane = this.dragState?.pane ?? null;
1732
+ const tabs = Array.from(header.querySelectorAll('.dock-tab'))
1733
+ .filter((t) => t !== placeholder && (!draggedPane || t.dataset['pane'] !== draggedPane));
1734
+ const clampedTarget = Math.max(0, Math.min(targetIndex, tabs.length));
1735
+ const ref = tabs[clampedTarget] ?? null;
1736
+ header.insertBefore(placeholder, ref);
1737
+ }
1738
+ // Remove placeholder and restore original tab visibility
1739
+ clearHeaderDragPlaceholder() {
1740
+ const ph = this.dragState?.placeholderEl ?? null;
1741
+ const header = this.dragState?.placeholderHeader ?? null;
1742
+ if (header) {
1743
+ const dragged = this.dragState?.pane
1744
+ ? (Array.from(header.querySelectorAll('.dock-tab')).find((t) => t.dataset['pane'] === this.dragState?.pane) ?? null)
1745
+ : null;
1746
+ if (dragged) {
1747
+ dragged.style.display = '';
1748
+ }
1749
+ }
1750
+ if (ph && ph.parentElement) {
1751
+ ph.parentElement.removeChild(ph);
1752
+ }
1753
+ if (this.dragState) {
1754
+ this.dragState.placeholderEl = null;
1755
+ this.dragState.placeholderHeader = null;
1756
+ }
1757
+ }
1555
1758
  startDragPointerTracking() {
1556
1759
  if (this.dragPointerTrackingActive) {
1557
1760
  return;
@@ -1606,6 +1809,13 @@ class MintDockManagerElement extends HTMLElement {
1606
1809
  this.updateDraggedFloatingPositionFromPoint(touch.clientX, touch.clientY);
1607
1810
  }
1608
1811
  onDragMouseUp() {
1812
+ // Prefer committing a drop from pointer-up since some browsers suppress drop events
1813
+ if (this.dragState) {
1814
+ const pos = this.lastDragPointerPosition;
1815
+ if (pos) {
1816
+ this.finalizeDropFromPoint(pos.x, pos.y);
1817
+ }
1818
+ }
1609
1819
  this.handleDragPointerUpCommon();
1610
1820
  }
1611
1821
  // Convert a currently in-header tab drag into a floating window
@@ -1617,6 +1827,8 @@ class MintDockManagerElement extends HTMLElement {
1617
1827
  if (state.floatingIndex !== null && state.floatingIndex >= 0) {
1618
1828
  return; // already floating
1619
1829
  }
1830
+ // Clean up any placeholder before converting
1831
+ this.clearHeaderDragPlaceholder();
1620
1832
  const location = this.resolveStackLocation(state.sourcePath);
1621
1833
  if (!location) {
1622
1834
  return;
@@ -1686,19 +1898,41 @@ class MintDockManagerElement extends HTMLElement {
1686
1898
  this.dispatchLayoutChanged();
1687
1899
  }
1688
1900
  // Compute the intended tab insert index within a header based on pointer X
1901
+ // Adds a slight rightward bias and uses the placeholder rect (if present)
1902
+ // to ensure offsets are correct even when the dragged tab is display:none.
1689
1903
  computeHeaderInsertIndex(header, clientX) {
1690
- const tabs = Array.from(header.querySelectorAll('.dock-tab'));
1691
- if (tabs.length === 0) {
1904
+ const allTabs = Array.from(header.querySelectorAll('.dock-tab'));
1905
+ if (allTabs.length === 0) {
1906
+ return 0;
1907
+ }
1908
+ const draggedPane = this.dragState?.pane ?? null;
1909
+ const draggedEl = draggedPane
1910
+ ? (allTabs.find((t) => t.dataset['pane'] === draggedPane) ?? null)
1911
+ : null;
1912
+ const placeholderEl = header.querySelector('.dock-tab[data-placeholder="true"]');
1913
+ const targets = allTabs.filter((t) => t !== draggedEl && t !== placeholderEl);
1914
+ if (targets.length === 0) {
1692
1915
  return 0;
1693
1916
  }
1694
- for (let i = 0; i < tabs.length; i += 1) {
1695
- const rect = tabs[i].getBoundingClientRect();
1696
- const mid = rect.left + rect.width / 2;
1917
+ const rightBias = 12;
1918
+ const leftBias = 0;
1919
+ const baseRect = placeholderEl
1920
+ ? placeholderEl.getBoundingClientRect()
1921
+ : draggedEl
1922
+ ? draggedEl.getBoundingClientRect()
1923
+ : null;
1924
+ const rectValid = !!baseRect && Number.isFinite(baseRect.width) && baseRect.width > 0;
1925
+ const draggedCenter = rectValid && baseRect ? baseRect.left + baseRect.width / 2 : null;
1926
+ for (let i = 0; i < targets.length; i += 1) {
1927
+ const rect = targets[i].getBoundingClientRect();
1928
+ const baseMid = rect.left + rect.width / 2;
1929
+ const isRightOfDragged = draggedCenter !== null ? baseMid >= draggedCenter : false;
1930
+ const mid = isRightOfDragged ? baseMid + rightBias : baseMid - leftBias;
1697
1931
  if (clientX < mid) {
1698
1932
  return i;
1699
1933
  }
1700
1934
  }
1701
- return tabs.length; // insert at end
1935
+ return targets.length;
1702
1936
  }
1703
1937
  reorderPaneInLocationAtIndex(location, pane, targetIndex) {
1704
1938
  const panes = location.node.panes;
@@ -1723,6 +1957,49 @@ class MintDockManagerElement extends HTMLElement {
1723
1957
  onDragTouchEnd() {
1724
1958
  this.handleDragPointerUpCommon();
1725
1959
  }
1960
+ // Commit a drop using current pointer coordinates and joystick state
1961
+ finalizeDropFromPoint(clientX, clientY) {
1962
+ if (!this.dragState) {
1963
+ return;
1964
+ }
1965
+ const stack = this.findStackAtPoint(clientX, clientY);
1966
+ const stackPath = stack ? this.parsePath(stack.dataset['path']) : null;
1967
+ const joystickVisible = this.dropJoystick.dataset['visible'] === 'true';
1968
+ const joystickStoredPath = this.parsePath(this.dropJoystick.dataset['path']);
1969
+ const joystickTarget = this.dropJoystickTarget;
1970
+ const joystickTargetPath = joystickTarget ? this.parsePath(joystickTarget.dataset['path']) : null;
1971
+ const path = (joystickVisible ? (joystickStoredPath ?? joystickTargetPath) : null) ?? stackPath;
1972
+ const joystickZone = this.dropJoystick.dataset['zone'];
1973
+ const zone = this.isDropZone(joystickZone)
1974
+ ? joystickZone
1975
+ : (stack ? this.computeDropZone(stack, { clientX, clientY }, null) : null);
1976
+ // Same-header reorder case when no side zone is chosen
1977
+ if (this.dragState &&
1978
+ this.dragState.floatingIndex !== null &&
1979
+ this.dragState.floatingIndex < 0 &&
1980
+ stack &&
1981
+ path &&
1982
+ stackPath &&
1983
+ this.pathsEqual(stackPath, this.dragState.sourcePath) &&
1984
+ (!zone || zone === 'center')) {
1985
+ const header = stack.querySelector('.dock-stack__header');
1986
+ if (header) {
1987
+ const location = this.resolveStackLocation(path);
1988
+ if (location) {
1989
+ const idx = this.computeHeaderInsertIndex(header, clientX);
1990
+ this.reorderPaneInLocationAtIndex(location, this.dragState.pane, idx);
1991
+ this.render();
1992
+ this.dispatchLayoutChanged();
1993
+ this.dragState.dropHandled = true;
1994
+ return;
1995
+ }
1996
+ }
1997
+ }
1998
+ if (path && this.isDropZone(zone)) {
1999
+ this.handleDrop(path, zone);
2000
+ this.dragState.dropHandled = true;
2001
+ }
2002
+ }
1726
2003
  clearPendingDragEndTimeout() {
1727
2004
  if (this.pendingDragEndTimeout !== null) {
1728
2005
  const win = this.windowRef;
@@ -1771,18 +2048,37 @@ class MintDockManagerElement extends HTMLElement {
1771
2048
  : null);
1772
2049
  const stack = this.findStackElement(event) ??
1773
2050
  (point ? this.findStackAtPoint(point.clientX, point.clientY) : null);
1774
- if (!stack) {
1775
- this.hideDropIndicator();
1776
- this.endPaneDrag();
1777
- return;
1778
- }
1779
- const path = this.parsePath(stack.dataset['path']);
1780
- // Allow reordering within the same stack header without selecting a zone
2051
+ // Prefer joystick's stored target path when the joystick is visible (drop over buttons)
2052
+ const joystickVisible = this.dropJoystick.dataset['visible'] === 'true';
2053
+ const joystickPath = this.parsePath(this.dropJoystick.dataset['path']);
2054
+ const joystickTarget = this.dropJoystickTarget;
2055
+ const joystickTargetPath = joystickTarget ? this.parsePath(joystickTarget.dataset['path']) : null;
2056
+ let path = stack
2057
+ ? this.parsePath(stack.dataset['path'])
2058
+ : (joystickPath ?? joystickTargetPath);
2059
+ if (!path && joystickVisible) {
2060
+ // As a last resort, target the main dock surface only when empty
2061
+ const dockPath = this.parsePath(this.dockedEl.dataset['path']);
2062
+ path = (!this.rootLayout ? dockPath : null);
2063
+ }
2064
+ // Defer same-header reorder decision until after zone resolution below
2065
+ // Prefer joystick's active zone if available, else infer from event/point
2066
+ const joystickZone = this.dropJoystick.dataset['zone'];
2067
+ const eventZoneHint = this.extractDropZoneFromEvent(event);
2068
+ const pointZoneHint = point ? this.findDropZoneByPoint(point.clientX, point.clientY) : null;
2069
+ const zone = this.isDropZone(joystickZone)
2070
+ ? joystickZone
2071
+ : stack
2072
+ ? this.computeDropZone(stack, point ?? event, pointZoneHint ?? eventZoneHint)
2073
+ : (this.isDropZone(pointZoneHint ?? eventZoneHint) ? (pointZoneHint ?? eventZoneHint) : null);
2074
+ // If still in same header and no side zone chosen, treat as in-header reorder
1781
2075
  if (this.dragState &&
1782
2076
  this.dragState.floatingIndex !== null &&
1783
2077
  this.dragState.floatingIndex < 0 &&
2078
+ stack &&
1784
2079
  path &&
1785
- this.pathsEqual(path, this.dragState.sourcePath)) {
2080
+ this.pathsEqual(path, this.dragState.sourcePath) &&
2081
+ (!zone || zone === 'center')) {
1786
2082
  const header = stack.querySelector('.dock-stack__header');
1787
2083
  if (header) {
1788
2084
  const x = (point ? point.clientX : event.clientX);
@@ -1800,9 +2096,12 @@ class MintDockManagerElement extends HTMLElement {
1800
2096
  }
1801
2097
  }
1802
2098
  }
1803
- const eventZoneHint = this.extractDropZoneFromEvent(event);
1804
- const pointZoneHint = point ? this.findDropZoneByPoint(point.clientX, point.clientY) : null;
1805
- const zone = this.computeDropZone(stack, point ?? event, pointZoneHint ?? eventZoneHint);
2099
+ // If joystick is visible and both path and zone are resolved, force using joystick as authoritative
2100
+ if (joystickVisible && path && this.isDropZone(joystickZone)) {
2101
+ this.handleDrop(path, joystickZone);
2102
+ this.endPaneDrag();
2103
+ return;
2104
+ }
1806
2105
  if (!zone) {
1807
2106
  this.hideDropIndicator();
1808
2107
  this.endPaneDrag();