@deepseek-ai/dsh-client-ui-dockkit 0.2.1-rc.4 → 0.2.2-alpha.2
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/README.i18n.yaml +6 -6
- package/README.md +10 -4
- package/README.zh.md +10 -4
- package/lib/components/dockkit.module.css +79 -27
- package/lib/index.js +479 -61
- package/lib/types/components/DockSurface.d.ts +2 -2
- package/lib/types/components/FloatLayer.d.ts +17 -2
- package/lib/types/components/TabBody.d.ts +9 -0
- package/lib/types/components/TabPanel.d.ts +14 -3
- package/lib/types/components/render.d.ts +1 -1
- package/lib/types/contract/adapter.d.ts +5 -1
- package/lib/types/engine/geometry.d.ts +46 -0
- package/lib/types/engine/spring.d.ts +47 -0
- package/package.json +5 -5
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Children, Fragment as Fragment$1, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { Children, Fragment as Fragment$1, memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import clsx from "clsx";
|
|
4
4
|
import { IconCloseFillRegular, IconCloseOutlineRegular, IconPanelLeftOutlineRegular, IconPlusOutlineRegular, MenuSurface, Tooltip, focusWithoutRing, modalSelector, observeComposition } from "@deepseek-ai/dsh-client-ui-primitives";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
@@ -1328,6 +1328,89 @@ function resizedRect(rect, dx, dy, min) {
|
|
|
1328
1328
|
height: Math.max(min.height, rect.height + dy)
|
|
1329
1329
|
};
|
|
1330
1330
|
}
|
|
1331
|
+
/** One axis clamped into `[min, max]`; an empty span pins it at `min`. */
|
|
1332
|
+
function clampAxis(value, min, max) {
|
|
1333
|
+
return Math.max(min, Math.min(value, max));
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* The nearest rectangle that lies inside the bounds, per axis. A panel larger
|
|
1337
|
+
* than the bounds on an axis takes the bounds' size on it, so its header
|
|
1338
|
+
* controls and resize corner stay reachable.
|
|
1339
|
+
* @param rect - any rectangle, possibly outside the bounds.
|
|
1340
|
+
* @param bounds - the viewport region.
|
|
1341
|
+
* @returns the rectangle moved inside the bounds, its size capped by them.
|
|
1342
|
+
*/
|
|
1343
|
+
function legalFloatRect(rect, bounds) {
|
|
1344
|
+
const width = Math.min(rect.width, bounds.width);
|
|
1345
|
+
const height = Math.min(rect.height, bounds.height - bounds.top);
|
|
1346
|
+
return {
|
|
1347
|
+
x: clampAxis(rect.x, 0, bounds.width - width),
|
|
1348
|
+
y: clampAxis(rect.y, bounds.top, bounds.height - height),
|
|
1349
|
+
width,
|
|
1350
|
+
height
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
/** Displacement past an edge for a pointer overflow: slope 1 at the edge, approaching `FLOAT_RESISTANCE`. */
|
|
1354
|
+
function resist(overflow) {
|
|
1355
|
+
return Math.sign(overflow) * 120 * Math.abs(overflow) / (120 + Math.abs(overflow));
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Where a dragged panel draws when the pointer has taken it past an edge: the
|
|
1359
|
+
* legal rectangle plus a resisted share of the overflow on each axis.
|
|
1360
|
+
* @param desired - the rectangle the pointer travel asks for.
|
|
1361
|
+
* @param bounds - the viewport region.
|
|
1362
|
+
* @returns the drawn rectangle at the legal size; equal to `desired` while it is legal.
|
|
1363
|
+
*/
|
|
1364
|
+
function resistedRect(desired, bounds) {
|
|
1365
|
+
const legal = legalFloatRect(desired, bounds);
|
|
1366
|
+
return {
|
|
1367
|
+
...legal,
|
|
1368
|
+
x: legal.x + resist(desired.x - legal.x),
|
|
1369
|
+
y: legal.y + resist(desired.y - legal.y)
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
/** The largest displacement `unresistedRect` inverts; `resist` only approaches `FLOAT_RESISTANCE`. */
|
|
1373
|
+
const MAX_INVERTIBLE = 120 * .99;
|
|
1374
|
+
/**
|
|
1375
|
+
* Pointer overflow that `resist` maps to a displacement, the displacement capped
|
|
1376
|
+
* at `MAX_INVERTIBLE`. A panel drawn further out than that is picked up at the
|
|
1377
|
+
* cap, which moves it inward by less than 1.2 px.
|
|
1378
|
+
*/
|
|
1379
|
+
function unresist(displacement) {
|
|
1380
|
+
const shown = Math.min(Math.abs(displacement), MAX_INVERTIBLE);
|
|
1381
|
+
return Math.sign(displacement) * 120 * shown / (120 - shown);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* The rectangle a drag must start from so that zero pointer travel draws
|
|
1385
|
+
* `drawn`, the inverse of `resistedRect`: a press on a panel still outside the
|
|
1386
|
+
* bounds picks it up where it is.
|
|
1387
|
+
* @param drawn - where the panel is drawn now.
|
|
1388
|
+
* @param bounds - the viewport region.
|
|
1389
|
+
* @returns the start rectangle at the legal size; equal to `drawn` while it is legal.
|
|
1390
|
+
*/
|
|
1391
|
+
function unresistedRect(drawn, bounds) {
|
|
1392
|
+
const legal = legalFloatRect(drawn, bounds);
|
|
1393
|
+
return {
|
|
1394
|
+
...legal,
|
|
1395
|
+
x: legal.x + unresist(drawn.x - legal.x),
|
|
1396
|
+
y: legal.y + unresist(drawn.y - legal.y)
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* A resized rectangle whose bottom-right corner stays inside the bounds, never
|
|
1401
|
+
* below the minimum size.
|
|
1402
|
+
* @param rect - the resized rectangle; its origin may lie outside the bounds.
|
|
1403
|
+
* @param bounds - the viewport region.
|
|
1404
|
+
* @param min - smallest size the panel may take.
|
|
1405
|
+
* @returns the rectangle with its size capped by the bounds.
|
|
1406
|
+
*/
|
|
1407
|
+
function boundedResize(rect, bounds, min) {
|
|
1408
|
+
return {
|
|
1409
|
+
...rect,
|
|
1410
|
+
width: Math.max(min.width, Math.min(rect.width, bounds.width - rect.x)),
|
|
1411
|
+
height: Math.max(min.height, Math.min(rect.height, bounds.height - rect.y))
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1331
1414
|
/**
|
|
1332
1415
|
* Where a panel should appear when a tab is dropped outside the docked area.
|
|
1333
1416
|
* @param x - drop point x.
|
|
@@ -2222,6 +2305,18 @@ function useGesture(reset) {
|
|
|
2222
2305
|
};
|
|
2223
2306
|
}
|
|
2224
2307
|
//#endregion
|
|
2308
|
+
//#region lib/types/components/TabBody.js
|
|
2309
|
+
/**
|
|
2310
|
+
* A tab's body from the embedder's renderer, shared by every layout. A layout
|
|
2311
|
+
* re-renders for its own previews (a float gesture or return, a divider or tab
|
|
2312
|
+
* drag) on every frame; the body is memoized on the renderer and the tab
|
|
2313
|
+
* record, so such a frame leaves it untouched.
|
|
2314
|
+
*/
|
|
2315
|
+
/** One tab's body, re-rendered only when `renderTab` or `tab` is a new object. */
|
|
2316
|
+
const TabBody = memo(function TabBody({ tab, renderTab }) {
|
|
2317
|
+
return renderTab(tab);
|
|
2318
|
+
});
|
|
2319
|
+
//#endregion
|
|
2225
2320
|
//#region lib/types/components/TabMenu.js
|
|
2226
2321
|
/**
|
|
2227
2322
|
* The per-tab context menu, opened by a secondary press on the chip. It carries
|
|
@@ -2308,6 +2403,9 @@ function TabMenu({ labels, anchor, onClose, onDismiss, extras }) {
|
|
|
2308
2403
|
onClick: (event) => {
|
|
2309
2404
|
event.stopPropagation();
|
|
2310
2405
|
},
|
|
2406
|
+
onAuxClick: (event) => {
|
|
2407
|
+
event.stopPropagation();
|
|
2408
|
+
},
|
|
2311
2409
|
children: [onClose !== void 0 && jsx("button", {
|
|
2312
2410
|
type: "button",
|
|
2313
2411
|
role: "menuitem",
|
|
@@ -2370,10 +2468,12 @@ function TabTitle({ children }) {
|
|
|
2370
2468
|
* A chip is a capsule carrying one control, its close, shown over its right
|
|
2371
2469
|
* end while the chip is active, hovered, or focused; the context menu
|
|
2372
2470
|
* (secondary press) carries the same close plus whatever the embedder appends.
|
|
2373
|
-
*
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
2376
|
-
*
|
|
2471
|
+
* A middle click anywhere on the chip, including its close control, closes
|
|
2472
|
+
* without selecting the tab, unless another mouse button is still held.
|
|
2473
|
+
* The embedder's `canCloseTab`
|
|
2474
|
+
* governs all close routes. A pane's lone chip whose close is withheld draws
|
|
2475
|
+
* quiet — no capsule, no hover fill — since there is nothing to select against
|
|
2476
|
+
* and nothing to do to it.
|
|
2377
2477
|
* Between neighbouring chips sits a slot: a fixed-width box drawing a
|
|
2378
2478
|
* hairline, blank beside the active chip, and the drop caret when a drag
|
|
2379
2479
|
* targets that index, so a caret never widens the row; the two end slots
|
|
@@ -2525,28 +2625,68 @@ function useStripScrollFades(box, tabs) {
|
|
|
2525
2625
|
}, [box, tabs]);
|
|
2526
2626
|
}
|
|
2527
2627
|
/**
|
|
2628
|
+
* Move the chip box to `left` at once, overriding the stylesheet's smooth
|
|
2629
|
+
* `scroll-behavior` for this one assignment.
|
|
2630
|
+
*/
|
|
2631
|
+
function jumpTo(element, left) {
|
|
2632
|
+
element.style.scrollBehavior = "auto";
|
|
2633
|
+
element.scrollLeft = left;
|
|
2634
|
+
element.style.removeProperty("scroll-behavior");
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2528
2637
|
* Bring the active chip into the chip box's view whenever the active tab or
|
|
2529
2638
|
* the row of chips changes: a tab opened or selected past the box's edge, or
|
|
2530
2639
|
* moved there by a close or a reorder, scrolls the box to it, with the fade
|
|
2531
2640
|
* band (24px) cleared so the chip is not under it. A chip already in view
|
|
2532
2641
|
* moves nothing. Direct DOM, like the fades above: the box's scroll position
|
|
2533
2642
|
* renders nothing.
|
|
2534
|
-
|
|
2535
|
-
|
|
2643
|
+
*
|
|
2644
|
+
* A box newly mounted, or newly showing another pane, first jumps to that
|
|
2645
|
+
* pane's offset in `offsets`, so a strip that replaces another for the same
|
|
2646
|
+
* pane continues from where that one stood and glides only the remaining
|
|
2647
|
+
* distance. With no recorded offset the first placement jumps rather than
|
|
2648
|
+
* glides from the row's start. The box records its offset in `offsets` when
|
|
2649
|
+
* it unmounts or leaves its pane.
|
|
2650
|
+
*/
|
|
2651
|
+
function useActiveChipInView(box, chips, tabs, activeTabId, paneId, offsets) {
|
|
2652
|
+
const placed = useRef(void 0);
|
|
2653
|
+
useLayoutEffect(() => {
|
|
2654
|
+
if (offsets === void 0) return void 0;
|
|
2655
|
+
const element = box.current;
|
|
2656
|
+
/* v8 ignore next -- the box is rendered unconditionally with the strip. */
|
|
2657
|
+
if (element === null) return void 0;
|
|
2658
|
+
return () => {
|
|
2659
|
+
offsets.set(paneId, element.scrollLeft);
|
|
2660
|
+
};
|
|
2661
|
+
}, [
|
|
2662
|
+
box,
|
|
2663
|
+
paneId,
|
|
2664
|
+
offsets
|
|
2665
|
+
]);
|
|
2536
2666
|
useLayoutEffect(() => {
|
|
2537
2667
|
const element = box.current;
|
|
2538
2668
|
const chip = activeTabId === void 0 ? void 0 : chips.get(activeTabId);
|
|
2539
2669
|
/* v8 ignore next -- the box and the active tab's chip are rendered with the strip. */
|
|
2540
2670
|
if (element === null || chip === void 0) return;
|
|
2671
|
+
const first = placed.current !== paneId;
|
|
2672
|
+
placed.current = paneId;
|
|
2673
|
+
const recorded = first ? offsets?.get(paneId) : void 0;
|
|
2674
|
+
if (recorded !== void 0) jumpTo(element, recorded);
|
|
2541
2675
|
const bounds = element.getBoundingClientRect();
|
|
2542
2676
|
const rect = chip.getBoundingClientRect();
|
|
2543
|
-
|
|
2544
|
-
|
|
2677
|
+
let delta = 0;
|
|
2678
|
+
if (rect.left < bounds.left) delta = rect.left - bounds.left - STRIP_FADE;
|
|
2679
|
+
else if (rect.right > bounds.right) delta = rect.right - bounds.right + STRIP_FADE;
|
|
2680
|
+
if (delta === 0) return;
|
|
2681
|
+
if (first && recorded === void 0) jumpTo(element, element.scrollLeft + delta);
|
|
2682
|
+
else element.scrollLeft += delta;
|
|
2545
2683
|
}, [
|
|
2546
2684
|
box,
|
|
2547
2685
|
chips,
|
|
2548
2686
|
tabs,
|
|
2549
|
-
activeTabId
|
|
2687
|
+
activeTabId,
|
|
2688
|
+
paneId,
|
|
2689
|
+
offsets
|
|
2550
2690
|
]);
|
|
2551
2691
|
}
|
|
2552
2692
|
/** Width of the chip box's fade at a hidden side; mirrors the stylesheet's 24px. */
|
|
@@ -2558,13 +2698,13 @@ function splitBlockedTitle(labels, block) {
|
|
|
2558
2698
|
case "width": return labels.splitPaneNarrow;
|
|
2559
2699
|
}
|
|
2560
2700
|
}
|
|
2561
|
-
/** @param props - pane state and
|
|
2562
|
-
function TabStrip({ state, pane, callbacks }) {
|
|
2701
|
+
/** @param props - pane state, gestures and shared offsets. @returns its tab strip without a content container. */
|
|
2702
|
+
function TabStrip({ state, pane, callbacks, offsets }) {
|
|
2563
2703
|
const [menu, setMenu] = useState(void 0);
|
|
2564
2704
|
const [chips] = useState(() => /* @__PURE__ */ new Map());
|
|
2565
2705
|
const stripTabs = useRef(null);
|
|
2566
2706
|
useStripScrollFades(stripTabs, pane.tabs);
|
|
2567
|
-
useActiveChipInView(stripTabs, chips, pane.tabs, pane.activeTabId);
|
|
2707
|
+
useActiveChipInView(stripTabs, chips, pane.tabs, pane.activeTabId, pane.id, offsets);
|
|
2568
2708
|
const block = callbacks.splitBlock(pane.id);
|
|
2569
2709
|
const target = callbacks.dropTarget;
|
|
2570
2710
|
const stripIndex = target !== void 0 && target.kind === "strip" && target.paneId === pane.id ? target.index : void 0;
|
|
@@ -2610,9 +2750,18 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2610
2750
|
else chips.set(tabId, element);
|
|
2611
2751
|
},
|
|
2612
2752
|
onPointerDown: (event) => {
|
|
2613
|
-
if (event.button
|
|
2753
|
+
if (event.button !== 0) return;
|
|
2614
2754
|
callbacks.onTabPressed(tabId, event);
|
|
2615
2755
|
},
|
|
2756
|
+
onMouseDown: (event) => {
|
|
2757
|
+
if (event.button === 1) event.preventDefault();
|
|
2758
|
+
},
|
|
2759
|
+
onAuxClick: (event) => {
|
|
2760
|
+
if (event.button !== 1) return;
|
|
2761
|
+
event.preventDefault();
|
|
2762
|
+
event.stopPropagation();
|
|
2763
|
+
if (closable && event.buttons === 0) callbacks.onCloseTab(tabId);
|
|
2764
|
+
},
|
|
2616
2765
|
onClick: (event) => {
|
|
2617
2766
|
event.stopPropagation();
|
|
2618
2767
|
activate(tabId);
|
|
@@ -2709,9 +2858,13 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2709
2858
|
shortcutKeys: callbacks.labels.splitPaneKeys,
|
|
2710
2859
|
side: "bottom",
|
|
2711
2860
|
delayMs: 500,
|
|
2861
|
+
openOnClick: block !== void 0,
|
|
2712
2862
|
children: jsx("span", {
|
|
2713
2863
|
tabIndex: block === void 0 ? void 0 : 0,
|
|
2714
2864
|
"aria-label": block === void 0 ? void 0 : splitBlockedTitle(callbacks.labels, block),
|
|
2865
|
+
onClick: (event) => {
|
|
2866
|
+
if (block !== void 0) event.stopPropagation();
|
|
2867
|
+
},
|
|
2715
2868
|
children: jsx("button", {
|
|
2716
2869
|
type: "button",
|
|
2717
2870
|
className: css.iconButton,
|
|
@@ -2781,7 +2934,10 @@ function TabPanel({ state, pane, callbacks }) {
|
|
|
2781
2934
|
children: [active === void 0 ? jsx("p", {
|
|
2782
2935
|
className: css.empty,
|
|
2783
2936
|
children: callbacks.labels.emptyPane
|
|
2784
|
-
}) :
|
|
2937
|
+
}) : jsx(TabBody, {
|
|
2938
|
+
tab: active,
|
|
2939
|
+
renderTab: callbacks.renderTab
|
|
2940
|
+
}), jsx(PaneDropHints, {
|
|
2785
2941
|
pane,
|
|
2786
2942
|
callbacks
|
|
2787
2943
|
})]
|
|
@@ -2828,6 +2984,72 @@ function PaneTree({ state, nodeId, callbacks, preview }) {
|
|
|
2828
2984
|
});
|
|
2829
2985
|
}
|
|
2830
2986
|
//#endregion
|
|
2987
|
+
//#region lib/types/engine/spring.js
|
|
2988
|
+
/**
|
|
2989
|
+
* The spring a floating panel follows back inside the viewport after a release
|
|
2990
|
+
* past an edge. Pure and clock-free: the caller supplies each frame's elapsed
|
|
2991
|
+
* time, so trajectories are deterministic for a given sequence of frames.
|
|
2992
|
+
*/
|
|
2993
|
+
/** Seconds the spring takes for one undamped period; sets its stiffness. */
|
|
2994
|
+
const RESPONSE = .32;
|
|
2995
|
+
/** Below 1, so the return overshoots slightly inward (about 3.5%), which always stays legal. */
|
|
2996
|
+
const DAMPING_RATIO = .72;
|
|
2997
|
+
const OMEGA = 2 * Math.PI / RESPONSE;
|
|
2998
|
+
/** Fixed integration step, so the motion does not depend on the frame rate. */
|
|
2999
|
+
const SUBSTEP = 1 / 480;
|
|
3000
|
+
/** Position and speed below which an axis counts as at rest. */
|
|
3001
|
+
const REST_DISTANCE = 1;
|
|
3002
|
+
const REST_SPEED = 60;
|
|
3003
|
+
/** Fastest release velocity a return starts with, in pixels per second. */
|
|
3004
|
+
const SPRING_MAX_VELOCITY = 1500;
|
|
3005
|
+
/**
|
|
3006
|
+
* Advance one axis by an elapsed time, in equal substeps no longer than `SUBSTEP`
|
|
3007
|
+
* (semi-implicit Euler).
|
|
3008
|
+
* @param axis - the axis state.
|
|
3009
|
+
* @param target - the position the spring pulls toward.
|
|
3010
|
+
* @param dtSeconds - elapsed time, already clamped by the caller.
|
|
3011
|
+
* @returns the advanced state.
|
|
3012
|
+
*/
|
|
3013
|
+
function stepSpring(axis, target, dtSeconds) {
|
|
3014
|
+
const steps = Math.max(1, Math.ceil(dtSeconds / SUBSTEP));
|
|
3015
|
+
const h = dtSeconds / steps;
|
|
3016
|
+
let { position, velocity } = axis;
|
|
3017
|
+
for (let step = 0; step < steps; step++) {
|
|
3018
|
+
velocity += (-OMEGA * OMEGA * (position - target) - 2 * DAMPING_RATIO * OMEGA * velocity) * h;
|
|
3019
|
+
position += velocity * h;
|
|
3020
|
+
}
|
|
3021
|
+
return {
|
|
3022
|
+
position,
|
|
3023
|
+
velocity
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* The velocity an axis starts its return with. An axis already at its target
|
|
3028
|
+
* keeps still. Outward speed is capped at `omega0 * (reach - |offset|)`: the
|
|
3029
|
+
* spring carries a start offset `d` and speed `v` at most `d + 0.46 * v / omega0`
|
|
3030
|
+
* from its target, so the whole return stays within `reach`.
|
|
3031
|
+
* @param position - where the axis is drawn at release.
|
|
3032
|
+
* @param target - the position the spring pulls toward.
|
|
3033
|
+
* @param velocity - the drawn release velocity, in pixels per second.
|
|
3034
|
+
* @param reach - the farthest the return may be from the target.
|
|
3035
|
+
* @returns the starting velocity.
|
|
3036
|
+
*/
|
|
3037
|
+
function launchVelocity(position, target, velocity, reach) {
|
|
3038
|
+
const offset = position - target;
|
|
3039
|
+
if (offset === 0) return 0;
|
|
3040
|
+
if (Math.sign(velocity) !== Math.sign(offset)) return velocity;
|
|
3041
|
+
return Math.sign(offset) * Math.min(Math.abs(velocity), OMEGA * Math.max(0, reach - Math.abs(offset)));
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Whether an axis has come to rest at its target.
|
|
3045
|
+
* @param axis - the axis state.
|
|
3046
|
+
* @param target - the spring's target.
|
|
3047
|
+
* @returns true within `REST_DISTANCE` of the target and slower than `REST_SPEED`.
|
|
3048
|
+
*/
|
|
3049
|
+
function springSettled(axis, target) {
|
|
3050
|
+
return Math.abs(axis.position - target) <= REST_DISTANCE && Math.abs(axis.velocity) <= REST_SPEED;
|
|
3051
|
+
}
|
|
3052
|
+
//#endregion
|
|
2831
3053
|
//#region lib/types/components/FloatLayer.js
|
|
2832
3054
|
/**
|
|
2833
3055
|
* The floating layer: one overlay panel per floating pane, bottom-to-top in the
|
|
@@ -2844,7 +3066,7 @@ function PaneTree({ state, nodeId, callbacks, preview }) {
|
|
|
2844
3066
|
* only `state` and the outward contracts. An embedder may portal it anywhere,
|
|
2845
3067
|
* and nothing here assumes the docked tree is an ancestor or even present.
|
|
2846
3068
|
*/
|
|
2847
|
-
/** The rectangle
|
|
3069
|
+
/** The rectangle the pointer travel asks for, before the viewport has its say. */
|
|
2848
3070
|
function draggedRect(drag, x, y) {
|
|
2849
3071
|
const dx = x - drag.originX;
|
|
2850
3072
|
const dy = y - drag.originY;
|
|
@@ -2859,57 +3081,244 @@ function raised(state, paneId) {
|
|
|
2859
3081
|
return state.activePaneId === paneId && state.floats.at(-1) === paneId;
|
|
2860
3082
|
}
|
|
2861
3083
|
/**
|
|
3084
|
+
* The viewport region panels stay inside. It reads the window size and the
|
|
3085
|
+
* root's caption attributes only, so starting a gesture never forces layout.
|
|
3086
|
+
*/
|
|
3087
|
+
function floatBounds() {
|
|
3088
|
+
const root = document.documentElement;
|
|
3089
|
+
const windows = root.hasAttribute("data-windows-titlebar");
|
|
3090
|
+
const caption = windows && !root.hasAttribute("data-fullscreen") ? Number.parseFloat(root.style.getPropertyValue("--dsh-windows-titlebar-height")) : 0;
|
|
3091
|
+
return {
|
|
3092
|
+
top: windows ? (Number.isFinite(caption) ? caption : 0) + 20 : 0,
|
|
3093
|
+
width: window.innerWidth,
|
|
3094
|
+
height: window.innerHeight
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
/** Whether the platform asks for reduced motion; environments without `matchMedia` (jsdom) report no preference. */
|
|
3098
|
+
function reducedMotion() {
|
|
3099
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3100
|
+
}
|
|
3101
|
+
/** How much recent drawing history the release velocity is measured over. */
|
|
3102
|
+
const VELOCITY_WINDOW_MS = 100;
|
|
3103
|
+
/** Append a drawn position and drop the samples more than `VELOCITY_WINDOW_MS` older than it. */
|
|
3104
|
+
function sampleDrawn(samples, t, rect) {
|
|
3105
|
+
samples.push({
|
|
3106
|
+
t,
|
|
3107
|
+
x: rect.x,
|
|
3108
|
+
y: rect.y
|
|
3109
|
+
});
|
|
3110
|
+
while (t - samples[0].t > VELOCITY_WINDOW_MS) samples.shift();
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Drawn panel velocity over the kept samples, capped per axis, in pixels per
|
|
3114
|
+
* second. It follows the drawn panel rather than the pointer, so the speed stays
|
|
3115
|
+
* continuous across a release where the resistance slows the panel.
|
|
3116
|
+
*/
|
|
3117
|
+
function releaseVelocity(samples) {
|
|
3118
|
+
const first = samples[0];
|
|
3119
|
+
const last = samples[samples.length - 1];
|
|
3120
|
+
if (last.t === first.t) return {
|
|
3121
|
+
x: 0,
|
|
3122
|
+
y: 0
|
|
3123
|
+
};
|
|
3124
|
+
const rate = (travel) => Math.max(-1500, Math.min(SPRING_MAX_VELOCITY, travel / (last.t - first.t) * 1e3));
|
|
3125
|
+
return {
|
|
3126
|
+
x: rate(last.x - first.x),
|
|
3127
|
+
y: rate(last.y - first.y)
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Inline position of a floating panel. A gesture or return draws it exactly at
|
|
3132
|
+
* `rect`. At rest the panel only publishes `rect` to the `.float` rule, which
|
|
3133
|
+
* clamps it into the viewport and below the caption floor, so a stale viewport
|
|
3134
|
+
* reading in `useFloatGestures` still leaves the panel inside.
|
|
3135
|
+
* @param rect - the rectangle to draw.
|
|
3136
|
+
* @param lifted - whether a gesture or return is drawing the panel.
|
|
3137
|
+
* @returns the panel's inline position and size.
|
|
3138
|
+
*/
|
|
3139
|
+
function floatStyle(rect, lifted) {
|
|
3140
|
+
if (lifted) return {
|
|
3141
|
+
left: rect.x,
|
|
3142
|
+
top: rect.y,
|
|
3143
|
+
width: rect.width,
|
|
3144
|
+
height: rect.height
|
|
3145
|
+
};
|
|
3146
|
+
return {
|
|
3147
|
+
"--dsh-dockkit-float-x": `${rect.x}px`,
|
|
3148
|
+
"--dsh-dockkit-float-y": `${rect.y}px`,
|
|
3149
|
+
"--dsh-dockkit-float-width": `${rect.width}px`,
|
|
3150
|
+
"--dsh-dockkit-float-height": `${rect.height}px`
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
2862
3154
|
* Shared move/resize gestures; changing layout mode never owns the content DOM.
|
|
3155
|
+
* Panels draw at their stored rectangle projected into the viewport, so a
|
|
3156
|
+
* shrinking window pushes them in without recording anything. A move past an
|
|
3157
|
+
* edge draws with resistance; its release records the legal rectangle and the
|
|
3158
|
+
* panel springs there from where it was drawn.
|
|
2863
3159
|
* @param state - committed layout and float stacking order.
|
|
2864
3160
|
* @param intents - gesture settlements.
|
|
2865
|
-
* @returns the current preview and focus/gesture callbacks.
|
|
3161
|
+
* @returns the current preview, the rectangle to draw per panel, and focus/gesture callbacks.
|
|
2866
3162
|
*/
|
|
2867
3163
|
function useFloatGestures(state, intents) {
|
|
2868
3164
|
const [preview, setPreview] = useState(void 0);
|
|
3165
|
+
const [bounds, setBounds] = useState(floatBounds);
|
|
3166
|
+
const area = useRef(bounds);
|
|
3167
|
+
const latest = useRef(state);
|
|
3168
|
+
latest.current = state;
|
|
3169
|
+
const settling = useRef(void 0);
|
|
2869
3170
|
const begin = useGesture(() => {
|
|
2870
3171
|
setPreview(void 0);
|
|
2871
3172
|
});
|
|
3173
|
+
/** Stop a running return where it is; reports whether one was running. */
|
|
3174
|
+
const cancelSettling = useRef(() => {
|
|
3175
|
+
const running = settling.current;
|
|
3176
|
+
if (running === void 0) return false;
|
|
3177
|
+
cancelAnimationFrame(running.frame);
|
|
3178
|
+
settling.current = void 0;
|
|
3179
|
+
return true;
|
|
3180
|
+
}).current;
|
|
3181
|
+
useEffect(() => {
|
|
3182
|
+
const resize = () => {
|
|
3183
|
+
if (cancelSettling()) setPreview(void 0);
|
|
3184
|
+
area.current = floatBounds();
|
|
3185
|
+
setBounds(area.current);
|
|
3186
|
+
};
|
|
3187
|
+
window.addEventListener("resize", resize);
|
|
3188
|
+
return () => {
|
|
3189
|
+
window.removeEventListener("resize", resize);
|
|
3190
|
+
cancelSettling();
|
|
3191
|
+
};
|
|
3192
|
+
}, [cancelSettling]);
|
|
3193
|
+
/**
|
|
3194
|
+
* One animation frame of a return: advance the spring, or end it at its
|
|
3195
|
+
* target. It also ends once the panel's record no longer projects to the
|
|
3196
|
+
* target, as after an undo, a dock or a close, so the release's `moveFloat`
|
|
3197
|
+
* or `resizeFloat` must reach `state` before the first frame.
|
|
3198
|
+
*/
|
|
3199
|
+
const frame = () => {
|
|
3200
|
+
const running = settling.current;
|
|
3201
|
+
const now = performance.now();
|
|
3202
|
+
const gap = now - running.last;
|
|
3203
|
+
running.last = now;
|
|
3204
|
+
const layout = latest.current;
|
|
3205
|
+
if (layout.floats.includes(running.paneId) && sameRect(legalFloatRect(floatRect(getPane(layout, running.paneId)), area.current), running.target) && gap <= 250 && now - running.started < 900) {
|
|
3206
|
+
const dt = Math.min(gap, 64) / 1e3;
|
|
3207
|
+
running.x = stepSpring(running.x, running.target.x, dt);
|
|
3208
|
+
running.y = stepSpring(running.y, running.target.y, dt);
|
|
3209
|
+
if (!springSettled(running.x, running.target.x) || !springSettled(running.y, running.target.y)) {
|
|
3210
|
+
setPreview({
|
|
3211
|
+
paneId: running.paneId,
|
|
3212
|
+
rect: {
|
|
3213
|
+
...running.target,
|
|
3214
|
+
x: running.x.position,
|
|
3215
|
+
y: running.y.position
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
running.frame = requestAnimationFrame(frame);
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
settling.current = void 0;
|
|
3223
|
+
setPreview(void 0);
|
|
3224
|
+
};
|
|
3225
|
+
/** Spring a panel from where it was drawn to its recorded rectangle, never farther than `FLOAT_RESISTANCE` outside it. */
|
|
3226
|
+
const settle = (paneId, from, target, velocity) => {
|
|
3227
|
+
const axis = (position, goal, speed) => ({
|
|
3228
|
+
position,
|
|
3229
|
+
velocity: launchVelocity(position, goal, speed, 120)
|
|
3230
|
+
});
|
|
3231
|
+
const now = performance.now();
|
|
3232
|
+
settling.current = {
|
|
3233
|
+
paneId,
|
|
3234
|
+
target,
|
|
3235
|
+
started: now,
|
|
3236
|
+
last: now,
|
|
3237
|
+
x: axis(from.x, target.x, velocity.x),
|
|
3238
|
+
y: axis(from.y, target.y, velocity.y),
|
|
3239
|
+
frame: requestAnimationFrame(frame)
|
|
3240
|
+
};
|
|
3241
|
+
setPreview({
|
|
3242
|
+
paneId,
|
|
3243
|
+
rect: from
|
|
3244
|
+
});
|
|
3245
|
+
};
|
|
2872
3246
|
/** Focus and raise a panel from a press or click on it, unless it is raised already. */
|
|
2873
3247
|
const raise = (paneId) => {
|
|
2874
3248
|
if (raised(state, paneId)) return;
|
|
2875
3249
|
intents.focusPane(paneId);
|
|
2876
3250
|
};
|
|
2877
|
-
/**
|
|
3251
|
+
/**
|
|
3252
|
+
* Where a panel draws: its gesture or return preview, otherwise its stored
|
|
3253
|
+
* rectangle projected into the viewport.
|
|
3254
|
+
* @param paneId - the floating pane.
|
|
3255
|
+
* @param stored - its recorded rectangle.
|
|
3256
|
+
* @returns the rectangle to draw.
|
|
3257
|
+
*/
|
|
3258
|
+
const rectOf = (paneId, stored) => preview?.paneId === paneId ? preview.rect : legalFloatRect(stored, bounds);
|
|
3259
|
+
/**
|
|
3260
|
+
* Start a move or resize from a press on the panel's grip or corner. A release
|
|
3261
|
+
* that reaches the same legal rectangle as the press is a click and only raises,
|
|
3262
|
+
* also for a panel drawn projected from its record.
|
|
3263
|
+
*/
|
|
2878
3264
|
const drag = (mode, paneId, event) => {
|
|
2879
3265
|
event.stopPropagation();
|
|
2880
|
-
const
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
const
|
|
2884
|
-
const
|
|
2885
|
-
|
|
2886
|
-
y: Math.max(saved.y, windows ? caption + 20 : 0)
|
|
2887
|
-
};
|
|
3266
|
+
const drawn = settling.current?.paneId === paneId ? preview?.rect : void 0;
|
|
3267
|
+
if (cancelSettling() && drawn === void 0) setPreview(void 0);
|
|
3268
|
+
area.current = floatBounds();
|
|
3269
|
+
const reduce = reducedMotion();
|
|
3270
|
+
const shown = drawn ?? legalFloatRect(floatRect(getPane(state, paneId)), area.current);
|
|
3271
|
+
const rect = mode === "move" && !reduce ? unresistedRect(shown, area.current) : shown;
|
|
2888
3272
|
const start = {
|
|
2889
3273
|
mode,
|
|
2890
3274
|
originX: event.clientX,
|
|
2891
3275
|
originY: event.clientY,
|
|
2892
3276
|
rect
|
|
2893
3277
|
};
|
|
3278
|
+
/** The rectangle to draw for a pointer position, and the legal one a release there records. */
|
|
3279
|
+
const reached = (x, y) => {
|
|
3280
|
+
const desired = draggedRect(start, x, y);
|
|
3281
|
+
if (mode === "resize") {
|
|
3282
|
+
const resized = boundedResize(desired, area.current, FLOAT_MIN_SIZE);
|
|
3283
|
+
const legal = legalFloatRect(resized, area.current);
|
|
3284
|
+
return {
|
|
3285
|
+
drawn: reduce ? legal : resized,
|
|
3286
|
+
legal
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
const legal = legalFloatRect(desired, area.current);
|
|
3290
|
+
return {
|
|
3291
|
+
drawn: reduce ? legal : resistedRect(desired, area.current),
|
|
3292
|
+
legal
|
|
3293
|
+
};
|
|
3294
|
+
};
|
|
3295
|
+
const pressed = reached(event.clientX, event.clientY);
|
|
3296
|
+
const samples = [];
|
|
3297
|
+
sampleDrawn(samples, performance.now(), pressed.drawn);
|
|
2894
3298
|
begin(event.currentTarget, event.pointerId, {
|
|
2895
3299
|
move: (moved) => {
|
|
3300
|
+
const { drawn } = reached(moved.clientX, moved.clientY);
|
|
3301
|
+
sampleDrawn(samples, performance.now(), drawn);
|
|
2896
3302
|
setPreview({
|
|
2897
3303
|
paneId,
|
|
2898
|
-
rect:
|
|
3304
|
+
rect: drawn
|
|
2899
3305
|
});
|
|
2900
3306
|
},
|
|
2901
3307
|
up: (released) => {
|
|
2902
|
-
const
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
else intents.
|
|
3308
|
+
const { drawn, legal } = reached(released.clientX, released.clientY);
|
|
3309
|
+
sampleDrawn(samples, performance.now(), drawn);
|
|
3310
|
+
if (sameRect(legal, pressed.legal)) raise(paneId);
|
|
3311
|
+
else if (mode === "move") intents.moveFloat(paneId, legal.x, legal.y);
|
|
3312
|
+
else intents.resizeFloat(paneId, legal);
|
|
3313
|
+
if (!sameRect(drawn, legal)) settle(paneId, drawn, legal, releaseVelocity(samples));
|
|
2906
3314
|
}
|
|
2907
3315
|
});
|
|
2908
3316
|
};
|
|
2909
3317
|
return {
|
|
2910
3318
|
preview,
|
|
2911
3319
|
raise,
|
|
2912
|
-
drag
|
|
3320
|
+
drag,
|
|
3321
|
+
rectOf
|
|
2913
3322
|
};
|
|
2914
3323
|
}
|
|
2915
3324
|
/**
|
|
@@ -2973,22 +3382,19 @@ function FloatHeader({ paneId, tab, labels, intents, renderTabTitle, canCloseTab
|
|
|
2973
3382
|
}
|
|
2974
3383
|
/** Every floating panel, in z order. */
|
|
2975
3384
|
function FloatLayer({ state, intents, labels, renderTab, renderTabTitle, canCloseTab }) {
|
|
2976
|
-
const { preview, raise, drag } = useFloatGestures(state, intents);
|
|
3385
|
+
const { preview, raise, drag, rectOf } = useFloatGestures(state, intents);
|
|
2977
3386
|
return jsx(Fragment, { children: state.floats.map((paneId, depth) => {
|
|
2978
3387
|
const pane = getPane(state, paneId);
|
|
2979
3388
|
const tab = getTab(state, onlyTabId(pane));
|
|
2980
3389
|
const lifted = preview?.paneId === paneId ? preview.rect : void 0;
|
|
2981
|
-
const live =
|
|
3390
|
+
const live = rectOf(paneId, floatRect(pane));
|
|
2982
3391
|
return jsxs("div", {
|
|
2983
3392
|
className: css.float,
|
|
2984
3393
|
"data-dockkit-float": paneId,
|
|
2985
3394
|
tabIndex: -1,
|
|
2986
3395
|
"data-dockkit-float-active": state.activePaneId === paneId || void 0,
|
|
2987
3396
|
style: {
|
|
2988
|
-
|
|
2989
|
-
top: `max(var(--dsh-dockkit-float-top, 0px), ${live.y}px)`,
|
|
2990
|
-
width: live.width,
|
|
2991
|
-
height: live.height,
|
|
3397
|
+
...floatStyle(live, lifted !== void 0),
|
|
2992
3398
|
zIndex: lifted === void 0 ? depth + 1 : state.floats.length + 1
|
|
2993
3399
|
},
|
|
2994
3400
|
onPointerDown: () => {
|
|
@@ -3006,7 +3412,10 @@ function FloatLayer({ state, intents, labels, renderTab, renderTabTitle, canClos
|
|
|
3006
3412
|
}),
|
|
3007
3413
|
jsx("div", {
|
|
3008
3414
|
className: css.floatBody,
|
|
3009
|
-
children:
|
|
3415
|
+
children: jsx(TabBody, {
|
|
3416
|
+
tab,
|
|
3417
|
+
renderTab
|
|
3418
|
+
})
|
|
3010
3419
|
}),
|
|
3011
3420
|
jsx("div", {
|
|
3012
3421
|
className: css.floatResize,
|
|
@@ -3023,7 +3432,7 @@ function FloatLayer({ state, intents, labels, renderTab, renderTabTitle, canClos
|
|
|
3023
3432
|
//#region lib/types/components/TabLayout.js
|
|
3024
3433
|
/** Stable tab siblings in one horizontal Grid, including viewport-positioned floats. */
|
|
3025
3434
|
/** A tab's ancestors stay identical across selection, pane moves and floating. */
|
|
3026
|
-
function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRequest, keepMounted, active = true }) {
|
|
3435
|
+
function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRequest, offsets, keepMounted, active = true }) {
|
|
3027
3436
|
const floating = pane.host === "float";
|
|
3028
3437
|
const selected = floating || pane.activeTabId === tab.id;
|
|
3029
3438
|
const visible = active && selected && (floating || state.expanded);
|
|
@@ -3067,7 +3476,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3067
3476
|
callbacks
|
|
3068
3477
|
]);
|
|
3069
3478
|
const lifted = floating && floats.preview?.paneId === pane.id ? floats.preview.rect : void 0;
|
|
3070
|
-
const rect = floating ?
|
|
3479
|
+
const rect = floating ? floats.rectOf(pane.id, floatRect(pane)) : void 0;
|
|
3071
3480
|
const depth = lifted === void 0 ? state.floats.indexOf(pane.id) + 1 : state.floats.length + 1;
|
|
3072
3481
|
return jsx("div", {
|
|
3073
3482
|
className: clsx(css.tabCell, floating && css.floatingCell),
|
|
@@ -3090,12 +3499,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3090
3499
|
"data-dockkit-float": floating ? pane.id : void 0,
|
|
3091
3500
|
"data-dockkit-float-active": floating && state.activePaneId === pane.id || void 0,
|
|
3092
3501
|
"data-dockkit-column": !floating ? column : void 0,
|
|
3093
|
-
style: rect === void 0 ? void 0 :
|
|
3094
|
-
left: rect.x,
|
|
3095
|
-
top: `max(var(--dsh-dockkit-float-top, 0px), ${rect.y}px)`,
|
|
3096
|
-
width: rect.width,
|
|
3097
|
-
height: rect.height
|
|
3098
|
-
},
|
|
3502
|
+
style: rect === void 0 ? void 0 : floatStyle(rect, lifted !== void 0),
|
|
3099
3503
|
onPointerDown: () => {
|
|
3100
3504
|
if (floating) floats.raise(pane.id);
|
|
3101
3505
|
},
|
|
@@ -3116,13 +3520,17 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3116
3520
|
}) : jsx(TabStrip, {
|
|
3117
3521
|
state,
|
|
3118
3522
|
pane,
|
|
3119
|
-
callbacks
|
|
3523
|
+
callbacks,
|
|
3524
|
+
offsets
|
|
3120
3525
|
}))
|
|
3121
3526
|
}),
|
|
3122
3527
|
jsxs("div", {
|
|
3123
3528
|
ref: body,
|
|
3124
3529
|
className: clsx(css.tabHostBody, floating ? css.floatBody : css.paneBody),
|
|
3125
|
-
children: [visited && (retained || active && selected) ?
|
|
3530
|
+
children: [visited && (retained || active && selected) ? jsx(TabBody, {
|
|
3531
|
+
tab,
|
|
3532
|
+
renderTab: callbacks.renderTab
|
|
3533
|
+
}) : null, !floating && jsx(PaneDropHints, {
|
|
3126
3534
|
pane,
|
|
3127
3535
|
callbacks
|
|
3128
3536
|
})]
|
|
@@ -3147,6 +3555,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3147
3555
|
function TabLayout(props) {
|
|
3148
3556
|
const { state, callbacks, preview } = props;
|
|
3149
3557
|
const focusRequest = useRef();
|
|
3558
|
+
const [offsets] = useState(() => /* @__PURE__ */ new Map());
|
|
3150
3559
|
const tabCallbacks = {
|
|
3151
3560
|
...callbacks,
|
|
3152
3561
|
onFocusTab: (tabId) => {
|
|
@@ -3177,7 +3586,8 @@ function TabLayout(props) {
|
|
|
3177
3586
|
pane,
|
|
3178
3587
|
column: panes.findIndex((candidate) => candidate.id === pane.id),
|
|
3179
3588
|
floats,
|
|
3180
|
-
focusRequest
|
|
3589
|
+
focusRequest,
|
|
3590
|
+
offsets
|
|
3181
3591
|
}, tab.id);
|
|
3182
3592
|
}),
|
|
3183
3593
|
panes.filter((pane) => pane.tabs.length === 0).map((pane) => jsx("div", {
|
|
@@ -3216,8 +3626,8 @@ function TabLayout(props) {
|
|
|
3216
3626
|
*
|
|
3217
3627
|
* A gesture only previews until it ends, then leaves through one intent, so the
|
|
3218
3628
|
* embedder's operation sequence stays the single source of truth. Releasing a tab
|
|
3219
|
-
* clear of this surface floats it; releasing inside it
|
|
3220
|
-
*
|
|
3629
|
+
* clear of this surface floats it; releasing inside it without an effective
|
|
3630
|
+
* drop target changes nothing.
|
|
3221
3631
|
*/
|
|
3222
3632
|
const NO_PREVIEW = {
|
|
3223
3633
|
draggingTabId: void 0,
|
|
@@ -3231,18 +3641,26 @@ const ALWAYS = () => true;
|
|
|
3231
3641
|
/**
|
|
3232
3642
|
* Resolve where a pointer sits inside the docked surface. An edge zone is only
|
|
3233
3643
|
* offered where the split it would make is allowed: within the pane budget and
|
|
3234
|
-
* with room for two halves
|
|
3644
|
+
* with room for two halves. Strip slots that preserve the dragged tab's position
|
|
3645
|
+
* offer no caret or placement intent. An undefined target inside the surface
|
|
3646
|
+
* leaves the layout unchanged on release.
|
|
3235
3647
|
*/
|
|
3236
|
-
function hitTest(root, x, y, canSplit, fits, dropZones) {
|
|
3648
|
+
function hitTest(root, x, y, tabId, canSplit, fits, dropZones) {
|
|
3237
3649
|
for (const [paneId, pane] of paneElements(root)) {
|
|
3238
3650
|
const rect = pane.getBoundingClientRect();
|
|
3239
3651
|
if (!containsPoint(rect, x, y)) continue;
|
|
3240
3652
|
const strip = pane.querySelector("[data-dockkit-strip]");
|
|
3241
|
-
if (strip !== null && containsPoint(strip.getBoundingClientRect(), x, y))
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3653
|
+
if (strip !== null && containsPoint(strip.getBoundingClientRect(), x, y)) {
|
|
3654
|
+
const tabs = [...strip.querySelectorAll("[data-dockkit-tab]")];
|
|
3655
|
+
const index = insertionIndex(tabs.map((tab) => tab.getBoundingClientRect()), x);
|
|
3656
|
+
const sourceIndex = tabs.findIndex((tab) => tab.dataset.dockkitTab === tabId);
|
|
3657
|
+
if (sourceIndex >= 0 && (index === sourceIndex || index === sourceIndex + 1)) return void 0;
|
|
3658
|
+
return {
|
|
3659
|
+
kind: "strip",
|
|
3660
|
+
paneId,
|
|
3661
|
+
index
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3246
3664
|
const zone = dropZones === "horizontal" ? canSplit && fitOf(fits, paneId).row ? x < rect.x + rect.width / 2 ? "left" : "right" : "center" : zoneInRect(rect, x, y);
|
|
3247
3665
|
if (zone !== "center") {
|
|
3248
3666
|
const fit = fitOf(fits, paneId);
|
|
@@ -3334,12 +3752,12 @@ function Surface({ state, canSplit, canAddTab, canCloseTab, intents, labels, ren
|
|
|
3334
3752
|
setPreview({
|
|
3335
3753
|
...NO_PREVIEW,
|
|
3336
3754
|
draggingTabId: tabId,
|
|
3337
|
-
dropTarget: hitTest(root, moved.clientX, moved.clientY, canSplit, fits, dropZones)
|
|
3755
|
+
dropTarget: hitTest(root, moved.clientX, moved.clientY, tabId, canSplit, fits, dropZones)
|
|
3338
3756
|
});
|
|
3339
3757
|
},
|
|
3340
3758
|
up: (released) => {
|
|
3341
3759
|
if (!dragging) return;
|
|
3342
|
-
const target = hitTest(root, released.clientX, released.clientY, canSplit, fits, dropZones);
|
|
3760
|
+
const target = hitTest(root, released.clientX, released.clientY, tabId, canSplit, fits, dropZones);
|
|
3343
3761
|
if (target === void 0) {
|
|
3344
3762
|
if (containsPoint(root.getBoundingClientRect(), released.clientX, released.clientY)) return;
|
|
3345
3763
|
intents.floatTab(tabId, floatRectAt(released.clientX, released.clientY, FLOAT_DEFAULT_SIZE));
|