@deepseek-ai/dsh-client-ui-dockkit 0.2.1-rc.5 → 0.2.2-alpha.3
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 +12 -4
- package/README.zh.md +12 -4
- package/lib/components/dockkit.module.css +79 -27
- package/lib/index.js +504 -100
- package/lib/types/components/DockSurface.d.ts +9 -5
- 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 +8 -4
- 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",
|
|
@@ -2363,28 +2461,6 @@ function TabTitle({ children }) {
|
|
|
2363
2461
|
//#endregion
|
|
2364
2462
|
//#region lib/types/components/TabPanel.js
|
|
2365
2463
|
/**
|
|
2366
|
-
* One pane: its tab strip (drag source, drop target, split control) and the
|
|
2367
|
-
* active tab's body with the dock preview overlay. Presentational; every gesture
|
|
2368
|
-
* leaves through `PaneCallbacks`, and the body itself comes from `renderTab`.
|
|
2369
|
-
*
|
|
2370
|
-
* A chip is a capsule carrying one control, its close, shown over its right
|
|
2371
|
-
* end while the chip is active, hovered, or focused; the context menu
|
|
2372
|
-
* (secondary press) carries the same close plus whatever the embedder appends.
|
|
2373
|
-
* Both close routes draw only while the embedder's `canCloseTab` allows, and
|
|
2374
|
-
* a pane's lone chip whose close is withheld draws quiet — no capsule, no
|
|
2375
|
-
* hover fill — since there is nothing to select against and nothing to do to
|
|
2376
|
-
* it.
|
|
2377
|
-
* Between neighbouring chips sits a slot: a fixed-width box drawing a
|
|
2378
|
-
* hairline, blank beside the active chip, and the drop caret when a drag
|
|
2379
|
-
* targets that index, so a caret never widens the row; the two end slots
|
|
2380
|
-
* exist only while targeted. The chips sit in their own box, the strip's one
|
|
2381
|
-
* shrinking part: in a narrow pane their titles fade at the clipped edge down
|
|
2382
|
-
* to the chip's floor and then the chips scroll there, keeping the active one
|
|
2383
|
-
* in view, so the add control after them (drawn while the embedder's
|
|
2384
|
-
* `canAddTab` allows), the pane's split control, and the embedder's chrome keep
|
|
2385
|
-
* their width and their place at the strip's end.
|
|
2386
|
-
*/
|
|
2387
|
-
/**
|
|
2388
2464
|
* The ic_ds_panel_left_outline_16 frame alone: its outer and inner rounded
|
|
2389
2465
|
* rectangles as one even-odd ring, without the divider. The glyphs below draw
|
|
2390
2466
|
* inside it so they read as siblings of the panel controls beside them.
|
|
@@ -2525,28 +2601,68 @@ function useStripScrollFades(box, tabs) {
|
|
|
2525
2601
|
}, [box, tabs]);
|
|
2526
2602
|
}
|
|
2527
2603
|
/**
|
|
2604
|
+
* Move the chip box to `left` at once, overriding the stylesheet's smooth
|
|
2605
|
+
* `scroll-behavior` for this one assignment.
|
|
2606
|
+
*/
|
|
2607
|
+
function jumpTo(element, left) {
|
|
2608
|
+
element.style.scrollBehavior = "auto";
|
|
2609
|
+
element.scrollLeft = left;
|
|
2610
|
+
element.style.removeProperty("scroll-behavior");
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2528
2613
|
* Bring the active chip into the chip box's view whenever the active tab or
|
|
2529
2614
|
* the row of chips changes: a tab opened or selected past the box's edge, or
|
|
2530
2615
|
* moved there by a close or a reorder, scrolls the box to it, with the fade
|
|
2531
2616
|
* band (24px) cleared so the chip is not under it. A chip already in view
|
|
2532
2617
|
* moves nothing. Direct DOM, like the fades above: the box's scroll position
|
|
2533
2618
|
* renders nothing.
|
|
2534
|
-
|
|
2535
|
-
|
|
2619
|
+
*
|
|
2620
|
+
* A box newly mounted, or newly showing another pane, first jumps to that
|
|
2621
|
+
* pane's offset in `offsets`, so a strip that replaces another for the same
|
|
2622
|
+
* pane continues from where that one stood and glides only the remaining
|
|
2623
|
+
* distance. With no recorded offset the first placement jumps rather than
|
|
2624
|
+
* glides from the row's start. The box records its offset in `offsets` when
|
|
2625
|
+
* it unmounts or leaves its pane.
|
|
2626
|
+
*/
|
|
2627
|
+
function useActiveChipInView(box, chips, tabs, activeTabId, paneId, offsets) {
|
|
2628
|
+
const placed = useRef(void 0);
|
|
2629
|
+
useLayoutEffect(() => {
|
|
2630
|
+
if (offsets === void 0) return void 0;
|
|
2631
|
+
const element = box.current;
|
|
2632
|
+
/* v8 ignore next -- the box is rendered unconditionally with the strip. */
|
|
2633
|
+
if (element === null) return void 0;
|
|
2634
|
+
return () => {
|
|
2635
|
+
offsets.set(paneId, element.scrollLeft);
|
|
2636
|
+
};
|
|
2637
|
+
}, [
|
|
2638
|
+
box,
|
|
2639
|
+
paneId,
|
|
2640
|
+
offsets
|
|
2641
|
+
]);
|
|
2536
2642
|
useLayoutEffect(() => {
|
|
2537
2643
|
const element = box.current;
|
|
2538
2644
|
const chip = activeTabId === void 0 ? void 0 : chips.get(activeTabId);
|
|
2539
2645
|
/* v8 ignore next -- the box and the active tab's chip are rendered with the strip. */
|
|
2540
2646
|
if (element === null || chip === void 0) return;
|
|
2647
|
+
const first = placed.current !== paneId;
|
|
2648
|
+
placed.current = paneId;
|
|
2649
|
+
const recorded = first ? offsets?.get(paneId) : void 0;
|
|
2650
|
+
if (recorded !== void 0) jumpTo(element, recorded);
|
|
2541
2651
|
const bounds = element.getBoundingClientRect();
|
|
2542
2652
|
const rect = chip.getBoundingClientRect();
|
|
2543
|
-
|
|
2544
|
-
|
|
2653
|
+
let delta = 0;
|
|
2654
|
+
if (rect.left < bounds.left) delta = rect.left - bounds.left - STRIP_FADE;
|
|
2655
|
+
else if (rect.right > bounds.right) delta = rect.right - bounds.right + STRIP_FADE;
|
|
2656
|
+
if (delta === 0) return;
|
|
2657
|
+
if (first && recorded === void 0) jumpTo(element, element.scrollLeft + delta);
|
|
2658
|
+
else element.scrollLeft += delta;
|
|
2545
2659
|
}, [
|
|
2546
2660
|
box,
|
|
2547
2661
|
chips,
|
|
2548
2662
|
tabs,
|
|
2549
|
-
activeTabId
|
|
2663
|
+
activeTabId,
|
|
2664
|
+
paneId,
|
|
2665
|
+
offsets
|
|
2550
2666
|
]);
|
|
2551
2667
|
}
|
|
2552
2668
|
/** Width of the chip box's fade at a hidden side; mirrors the stylesheet's 24px. */
|
|
@@ -2558,13 +2674,13 @@ function splitBlockedTitle(labels, block) {
|
|
|
2558
2674
|
case "width": return labels.splitPaneNarrow;
|
|
2559
2675
|
}
|
|
2560
2676
|
}
|
|
2561
|
-
/** @param props - pane state and
|
|
2562
|
-
function TabStrip({ state, pane, callbacks }) {
|
|
2677
|
+
/** @param props - pane state, gestures and shared offsets. @returns its tab strip without a content container. */
|
|
2678
|
+
function TabStrip({ state, pane, callbacks, offsets }) {
|
|
2563
2679
|
const [menu, setMenu] = useState(void 0);
|
|
2564
2680
|
const [chips] = useState(() => /* @__PURE__ */ new Map());
|
|
2565
2681
|
const stripTabs = useRef(null);
|
|
2566
2682
|
useStripScrollFades(stripTabs, pane.tabs);
|
|
2567
|
-
useActiveChipInView(stripTabs, chips, pane.tabs, pane.activeTabId);
|
|
2683
|
+
useActiveChipInView(stripTabs, chips, pane.tabs, pane.activeTabId, pane.id, offsets);
|
|
2568
2684
|
const block = callbacks.splitBlock(pane.id);
|
|
2569
2685
|
const target = callbacks.dropTarget;
|
|
2570
2686
|
const stripIndex = target !== void 0 && target.kind === "strip" && target.paneId === pane.id ? target.index : void 0;
|
|
@@ -2595,6 +2711,20 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2595
2711
|
const selected = tabId === pane.activeTabId;
|
|
2596
2712
|
const closable = callbacks.canCloseTab(tabId);
|
|
2597
2713
|
const quiet = !closable && pane.tabs.length === 1;
|
|
2714
|
+
const onKeyDown = (event) => {
|
|
2715
|
+
if (event.target !== event.currentTarget) return;
|
|
2716
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.nativeEvent.isComposing) return;
|
|
2717
|
+
const next = chipToFocus(event.key, pane.tabs, tabId);
|
|
2718
|
+
if (next !== void 0) {
|
|
2719
|
+
event.preventDefault();
|
|
2720
|
+
focusChip(next);
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
if (selects(event.key)) {
|
|
2724
|
+
event.preventDefault();
|
|
2725
|
+
activate(tabId);
|
|
2726
|
+
}
|
|
2727
|
+
};
|
|
2598
2728
|
return jsxs(Fragment$1, { children: [(index > 0 || stripIndex === index) && jsx("div", {
|
|
2599
2729
|
className: clsx(css.slot, stripIndex === index && css.slotCaret),
|
|
2600
2730
|
"data-dockkit-caret": stripIndex === index ? index : void 0
|
|
@@ -2610,39 +2740,41 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2610
2740
|
else chips.set(tabId, element);
|
|
2611
2741
|
},
|
|
2612
2742
|
onPointerDown: (event) => {
|
|
2613
|
-
if (event.button
|
|
2743
|
+
if (event.button !== 0) return;
|
|
2614
2744
|
callbacks.onTabPressed(tabId, event);
|
|
2615
2745
|
},
|
|
2746
|
+
onMouseDown: (event) => {
|
|
2747
|
+
if (event.button === 1) event.preventDefault();
|
|
2748
|
+
},
|
|
2749
|
+
onAuxClick: (event) => {
|
|
2750
|
+
if (event.button !== 1) return;
|
|
2751
|
+
event.preventDefault();
|
|
2752
|
+
event.stopPropagation();
|
|
2753
|
+
if (closable && event.buttons === 0) callbacks.onCloseTab(tabId);
|
|
2754
|
+
},
|
|
2616
2755
|
onClick: (event) => {
|
|
2617
2756
|
event.stopPropagation();
|
|
2618
2757
|
activate(tabId);
|
|
2619
2758
|
},
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
focusChip(next);
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
if (selects(event.key)) {
|
|
2759
|
+
...callbacks.getTabTriggerProps?.(tab, pane.id, {
|
|
2760
|
+
onKeyDown,
|
|
2761
|
+
tabIndex: selected ? 0 : -1
|
|
2762
|
+
}) ?? {
|
|
2763
|
+
onKeyDown,
|
|
2764
|
+
onContextMenu: (event) => {
|
|
2630
2765
|
event.preventDefault();
|
|
2631
|
-
|
|
2766
|
+
event.stopPropagation();
|
|
2767
|
+
const anchor = event.currentTarget;
|
|
2768
|
+
setMenu((current) => current?.tabId === tabId ? void 0 : {
|
|
2769
|
+
tabId,
|
|
2770
|
+
anchor
|
|
2771
|
+
});
|
|
2632
2772
|
}
|
|
2633
2773
|
},
|
|
2634
|
-
onContextMenu: (event) => {
|
|
2635
|
-
event.preventDefault();
|
|
2636
|
-
const anchor = event.currentTarget;
|
|
2637
|
-
setMenu((current) => current?.tabId === tabId ? void 0 : {
|
|
2638
|
-
tabId,
|
|
2639
|
-
anchor
|
|
2640
|
-
});
|
|
2641
|
-
},
|
|
2642
2774
|
children: [
|
|
2643
2775
|
jsx(TabTitle, { children: callbacks.renderTabTitle?.(tab) ?? tab.title }),
|
|
2644
2776
|
closable && jsx(Tooltip, {
|
|
2645
|
-
disabled: menu !== void 0,
|
|
2777
|
+
disabled: menu !== void 0 || callbacks.tabMenuOpen === true,
|
|
2646
2778
|
label: callbacks.labels.closeTab,
|
|
2647
2779
|
shortcutKeys: callbacks.labels.closeTabKeys,
|
|
2648
2780
|
side: "bottom",
|
|
@@ -2709,9 +2841,14 @@ function TabStrip({ state, pane, callbacks }) {
|
|
|
2709
2841
|
shortcutKeys: callbacks.labels.splitPaneKeys,
|
|
2710
2842
|
side: "bottom",
|
|
2711
2843
|
delayMs: 500,
|
|
2844
|
+
openOnClick: block !== void 0,
|
|
2712
2845
|
children: jsx("span", {
|
|
2846
|
+
"data-dockkit-split-control": true,
|
|
2713
2847
|
tabIndex: block === void 0 ? void 0 : 0,
|
|
2714
2848
|
"aria-label": block === void 0 ? void 0 : splitBlockedTitle(callbacks.labels, block),
|
|
2849
|
+
onClick: (event) => {
|
|
2850
|
+
if (block !== void 0) event.stopPropagation();
|
|
2851
|
+
},
|
|
2715
2852
|
children: jsx("button", {
|
|
2716
2853
|
type: "button",
|
|
2717
2854
|
className: css.iconButton,
|
|
@@ -2781,7 +2918,10 @@ function TabPanel({ state, pane, callbacks }) {
|
|
|
2781
2918
|
children: [active === void 0 ? jsx("p", {
|
|
2782
2919
|
className: css.empty,
|
|
2783
2920
|
children: callbacks.labels.emptyPane
|
|
2784
|
-
}) :
|
|
2921
|
+
}) : jsx(TabBody, {
|
|
2922
|
+
tab: active,
|
|
2923
|
+
renderTab: callbacks.renderTab
|
|
2924
|
+
}), jsx(PaneDropHints, {
|
|
2785
2925
|
pane,
|
|
2786
2926
|
callbacks
|
|
2787
2927
|
})]
|
|
@@ -2828,6 +2968,72 @@ function PaneTree({ state, nodeId, callbacks, preview }) {
|
|
|
2828
2968
|
});
|
|
2829
2969
|
}
|
|
2830
2970
|
//#endregion
|
|
2971
|
+
//#region lib/types/engine/spring.js
|
|
2972
|
+
/**
|
|
2973
|
+
* The spring a floating panel follows back inside the viewport after a release
|
|
2974
|
+
* past an edge. Pure and clock-free: the caller supplies each frame's elapsed
|
|
2975
|
+
* time, so trajectories are deterministic for a given sequence of frames.
|
|
2976
|
+
*/
|
|
2977
|
+
/** Seconds the spring takes for one undamped period; sets its stiffness. */
|
|
2978
|
+
const RESPONSE = .32;
|
|
2979
|
+
/** Below 1, so the return overshoots slightly inward (about 3.5%), which always stays legal. */
|
|
2980
|
+
const DAMPING_RATIO = .72;
|
|
2981
|
+
const OMEGA = 2 * Math.PI / RESPONSE;
|
|
2982
|
+
/** Fixed integration step, so the motion does not depend on the frame rate. */
|
|
2983
|
+
const SUBSTEP = 1 / 480;
|
|
2984
|
+
/** Position and speed below which an axis counts as at rest. */
|
|
2985
|
+
const REST_DISTANCE = 1;
|
|
2986
|
+
const REST_SPEED = 60;
|
|
2987
|
+
/** Fastest release velocity a return starts with, in pixels per second. */
|
|
2988
|
+
const SPRING_MAX_VELOCITY = 1500;
|
|
2989
|
+
/**
|
|
2990
|
+
* Advance one axis by an elapsed time, in equal substeps no longer than `SUBSTEP`
|
|
2991
|
+
* (semi-implicit Euler).
|
|
2992
|
+
* @param axis - the axis state.
|
|
2993
|
+
* @param target - the position the spring pulls toward.
|
|
2994
|
+
* @param dtSeconds - elapsed time, already clamped by the caller.
|
|
2995
|
+
* @returns the advanced state.
|
|
2996
|
+
*/
|
|
2997
|
+
function stepSpring(axis, target, dtSeconds) {
|
|
2998
|
+
const steps = Math.max(1, Math.ceil(dtSeconds / SUBSTEP));
|
|
2999
|
+
const h = dtSeconds / steps;
|
|
3000
|
+
let { position, velocity } = axis;
|
|
3001
|
+
for (let step = 0; step < steps; step++) {
|
|
3002
|
+
velocity += (-OMEGA * OMEGA * (position - target) - 2 * DAMPING_RATIO * OMEGA * velocity) * h;
|
|
3003
|
+
position += velocity * h;
|
|
3004
|
+
}
|
|
3005
|
+
return {
|
|
3006
|
+
position,
|
|
3007
|
+
velocity
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* The velocity an axis starts its return with. An axis already at its target
|
|
3012
|
+
* keeps still. Outward speed is capped at `omega0 * (reach - |offset|)`: the
|
|
3013
|
+
* spring carries a start offset `d` and speed `v` at most `d + 0.46 * v / omega0`
|
|
3014
|
+
* from its target, so the whole return stays within `reach`.
|
|
3015
|
+
* @param position - where the axis is drawn at release.
|
|
3016
|
+
* @param target - the position the spring pulls toward.
|
|
3017
|
+
* @param velocity - the drawn release velocity, in pixels per second.
|
|
3018
|
+
* @param reach - the farthest the return may be from the target.
|
|
3019
|
+
* @returns the starting velocity.
|
|
3020
|
+
*/
|
|
3021
|
+
function launchVelocity(position, target, velocity, reach) {
|
|
3022
|
+
const offset = position - target;
|
|
3023
|
+
if (offset === 0) return 0;
|
|
3024
|
+
if (Math.sign(velocity) !== Math.sign(offset)) return velocity;
|
|
3025
|
+
return Math.sign(offset) * Math.min(Math.abs(velocity), OMEGA * Math.max(0, reach - Math.abs(offset)));
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Whether an axis has come to rest at its target.
|
|
3029
|
+
* @param axis - the axis state.
|
|
3030
|
+
* @param target - the spring's target.
|
|
3031
|
+
* @returns true within `REST_DISTANCE` of the target and slower than `REST_SPEED`.
|
|
3032
|
+
*/
|
|
3033
|
+
function springSettled(axis, target) {
|
|
3034
|
+
return Math.abs(axis.position - target) <= REST_DISTANCE && Math.abs(axis.velocity) <= REST_SPEED;
|
|
3035
|
+
}
|
|
3036
|
+
//#endregion
|
|
2831
3037
|
//#region lib/types/components/FloatLayer.js
|
|
2832
3038
|
/**
|
|
2833
3039
|
* The floating layer: one overlay panel per floating pane, bottom-to-top in the
|
|
@@ -2844,7 +3050,7 @@ function PaneTree({ state, nodeId, callbacks, preview }) {
|
|
|
2844
3050
|
* only `state` and the outward contracts. An embedder may portal it anywhere,
|
|
2845
3051
|
* and nothing here assumes the docked tree is an ancestor or even present.
|
|
2846
3052
|
*/
|
|
2847
|
-
/** The rectangle
|
|
3053
|
+
/** The rectangle the pointer travel asks for, before the viewport has its say. */
|
|
2848
3054
|
function draggedRect(drag, x, y) {
|
|
2849
3055
|
const dx = x - drag.originX;
|
|
2850
3056
|
const dy = y - drag.originY;
|
|
@@ -2859,57 +3065,244 @@ function raised(state, paneId) {
|
|
|
2859
3065
|
return state.activePaneId === paneId && state.floats.at(-1) === paneId;
|
|
2860
3066
|
}
|
|
2861
3067
|
/**
|
|
3068
|
+
* The viewport region panels stay inside. It reads the window size and the
|
|
3069
|
+
* root's caption attributes only, so starting a gesture never forces layout.
|
|
3070
|
+
*/
|
|
3071
|
+
function floatBounds() {
|
|
3072
|
+
const root = document.documentElement;
|
|
3073
|
+
const windows = root.hasAttribute("data-windows-titlebar");
|
|
3074
|
+
const caption = windows && !root.hasAttribute("data-fullscreen") ? Number.parseFloat(root.style.getPropertyValue("--dsh-windows-titlebar-height")) : 0;
|
|
3075
|
+
return {
|
|
3076
|
+
top: windows ? (Number.isFinite(caption) ? caption : 0) + 20 : 0,
|
|
3077
|
+
width: window.innerWidth,
|
|
3078
|
+
height: window.innerHeight
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
/** Whether the platform asks for reduced motion; environments without `matchMedia` (jsdom) report no preference. */
|
|
3082
|
+
function reducedMotion() {
|
|
3083
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3084
|
+
}
|
|
3085
|
+
/** How much recent drawing history the release velocity is measured over. */
|
|
3086
|
+
const VELOCITY_WINDOW_MS = 100;
|
|
3087
|
+
/** Append a drawn position and drop the samples more than `VELOCITY_WINDOW_MS` older than it. */
|
|
3088
|
+
function sampleDrawn(samples, t, rect) {
|
|
3089
|
+
samples.push({
|
|
3090
|
+
t,
|
|
3091
|
+
x: rect.x,
|
|
3092
|
+
y: rect.y
|
|
3093
|
+
});
|
|
3094
|
+
while (t - samples[0].t > VELOCITY_WINDOW_MS) samples.shift();
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Drawn panel velocity over the kept samples, capped per axis, in pixels per
|
|
3098
|
+
* second. It follows the drawn panel rather than the pointer, so the speed stays
|
|
3099
|
+
* continuous across a release where the resistance slows the panel.
|
|
3100
|
+
*/
|
|
3101
|
+
function releaseVelocity(samples) {
|
|
3102
|
+
const first = samples[0];
|
|
3103
|
+
const last = samples[samples.length - 1];
|
|
3104
|
+
if (last.t === first.t) return {
|
|
3105
|
+
x: 0,
|
|
3106
|
+
y: 0
|
|
3107
|
+
};
|
|
3108
|
+
const rate = (travel) => Math.max(-1500, Math.min(SPRING_MAX_VELOCITY, travel / (last.t - first.t) * 1e3));
|
|
3109
|
+
return {
|
|
3110
|
+
x: rate(last.x - first.x),
|
|
3111
|
+
y: rate(last.y - first.y)
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Inline position of a floating panel. A gesture or return draws it exactly at
|
|
3116
|
+
* `rect`. At rest the panel only publishes `rect` to the `.float` rule, which
|
|
3117
|
+
* clamps it into the viewport and below the caption floor, so a stale viewport
|
|
3118
|
+
* reading in `useFloatGestures` still leaves the panel inside.
|
|
3119
|
+
* @param rect - the rectangle to draw.
|
|
3120
|
+
* @param lifted - whether a gesture or return is drawing the panel.
|
|
3121
|
+
* @returns the panel's inline position and size.
|
|
3122
|
+
*/
|
|
3123
|
+
function floatStyle(rect, lifted) {
|
|
3124
|
+
if (lifted) return {
|
|
3125
|
+
left: rect.x,
|
|
3126
|
+
top: rect.y,
|
|
3127
|
+
width: rect.width,
|
|
3128
|
+
height: rect.height
|
|
3129
|
+
};
|
|
3130
|
+
return {
|
|
3131
|
+
"--dsh-dockkit-float-x": `${rect.x}px`,
|
|
3132
|
+
"--dsh-dockkit-float-y": `${rect.y}px`,
|
|
3133
|
+
"--dsh-dockkit-float-width": `${rect.width}px`,
|
|
3134
|
+
"--dsh-dockkit-float-height": `${rect.height}px`
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
2862
3138
|
* Shared move/resize gestures; changing layout mode never owns the content DOM.
|
|
3139
|
+
* Panels draw at their stored rectangle projected into the viewport, so a
|
|
3140
|
+
* shrinking window pushes them in without recording anything. A move past an
|
|
3141
|
+
* edge draws with resistance; its release records the legal rectangle and the
|
|
3142
|
+
* panel springs there from where it was drawn.
|
|
2863
3143
|
* @param state - committed layout and float stacking order.
|
|
2864
3144
|
* @param intents - gesture settlements.
|
|
2865
|
-
* @returns the current preview and focus/gesture callbacks.
|
|
3145
|
+
* @returns the current preview, the rectangle to draw per panel, and focus/gesture callbacks.
|
|
2866
3146
|
*/
|
|
2867
3147
|
function useFloatGestures(state, intents) {
|
|
2868
3148
|
const [preview, setPreview] = useState(void 0);
|
|
3149
|
+
const [bounds, setBounds] = useState(floatBounds);
|
|
3150
|
+
const area = useRef(bounds);
|
|
3151
|
+
const latest = useRef(state);
|
|
3152
|
+
latest.current = state;
|
|
3153
|
+
const settling = useRef(void 0);
|
|
2869
3154
|
const begin = useGesture(() => {
|
|
2870
3155
|
setPreview(void 0);
|
|
2871
3156
|
});
|
|
3157
|
+
/** Stop a running return where it is; reports whether one was running. */
|
|
3158
|
+
const cancelSettling = useRef(() => {
|
|
3159
|
+
const running = settling.current;
|
|
3160
|
+
if (running === void 0) return false;
|
|
3161
|
+
cancelAnimationFrame(running.frame);
|
|
3162
|
+
settling.current = void 0;
|
|
3163
|
+
return true;
|
|
3164
|
+
}).current;
|
|
3165
|
+
useEffect(() => {
|
|
3166
|
+
const resize = () => {
|
|
3167
|
+
if (cancelSettling()) setPreview(void 0);
|
|
3168
|
+
area.current = floatBounds();
|
|
3169
|
+
setBounds(area.current);
|
|
3170
|
+
};
|
|
3171
|
+
window.addEventListener("resize", resize);
|
|
3172
|
+
return () => {
|
|
3173
|
+
window.removeEventListener("resize", resize);
|
|
3174
|
+
cancelSettling();
|
|
3175
|
+
};
|
|
3176
|
+
}, [cancelSettling]);
|
|
3177
|
+
/**
|
|
3178
|
+
* One animation frame of a return: advance the spring, or end it at its
|
|
3179
|
+
* target. It also ends once the panel's record no longer projects to the
|
|
3180
|
+
* target, as after an undo, a dock or a close, so the release's `moveFloat`
|
|
3181
|
+
* or `resizeFloat` must reach `state` before the first frame.
|
|
3182
|
+
*/
|
|
3183
|
+
const frame = () => {
|
|
3184
|
+
const running = settling.current;
|
|
3185
|
+
const now = performance.now();
|
|
3186
|
+
const gap = now - running.last;
|
|
3187
|
+
running.last = now;
|
|
3188
|
+
const layout = latest.current;
|
|
3189
|
+
if (layout.floats.includes(running.paneId) && sameRect(legalFloatRect(floatRect(getPane(layout, running.paneId)), area.current), running.target) && gap <= 250 && now - running.started < 900) {
|
|
3190
|
+
const dt = Math.min(gap, 64) / 1e3;
|
|
3191
|
+
running.x = stepSpring(running.x, running.target.x, dt);
|
|
3192
|
+
running.y = stepSpring(running.y, running.target.y, dt);
|
|
3193
|
+
if (!springSettled(running.x, running.target.x) || !springSettled(running.y, running.target.y)) {
|
|
3194
|
+
setPreview({
|
|
3195
|
+
paneId: running.paneId,
|
|
3196
|
+
rect: {
|
|
3197
|
+
...running.target,
|
|
3198
|
+
x: running.x.position,
|
|
3199
|
+
y: running.y.position
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
running.frame = requestAnimationFrame(frame);
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
settling.current = void 0;
|
|
3207
|
+
setPreview(void 0);
|
|
3208
|
+
};
|
|
3209
|
+
/** Spring a panel from where it was drawn to its recorded rectangle, never farther than `FLOAT_RESISTANCE` outside it. */
|
|
3210
|
+
const settle = (paneId, from, target, velocity) => {
|
|
3211
|
+
const axis = (position, goal, speed) => ({
|
|
3212
|
+
position,
|
|
3213
|
+
velocity: launchVelocity(position, goal, speed, 120)
|
|
3214
|
+
});
|
|
3215
|
+
const now = performance.now();
|
|
3216
|
+
settling.current = {
|
|
3217
|
+
paneId,
|
|
3218
|
+
target,
|
|
3219
|
+
started: now,
|
|
3220
|
+
last: now,
|
|
3221
|
+
x: axis(from.x, target.x, velocity.x),
|
|
3222
|
+
y: axis(from.y, target.y, velocity.y),
|
|
3223
|
+
frame: requestAnimationFrame(frame)
|
|
3224
|
+
};
|
|
3225
|
+
setPreview({
|
|
3226
|
+
paneId,
|
|
3227
|
+
rect: from
|
|
3228
|
+
});
|
|
3229
|
+
};
|
|
2872
3230
|
/** Focus and raise a panel from a press or click on it, unless it is raised already. */
|
|
2873
3231
|
const raise = (paneId) => {
|
|
2874
3232
|
if (raised(state, paneId)) return;
|
|
2875
3233
|
intents.focusPane(paneId);
|
|
2876
3234
|
};
|
|
2877
|
-
/**
|
|
3235
|
+
/**
|
|
3236
|
+
* Where a panel draws: its gesture or return preview, otherwise its stored
|
|
3237
|
+
* rectangle projected into the viewport.
|
|
3238
|
+
* @param paneId - the floating pane.
|
|
3239
|
+
* @param stored - its recorded rectangle.
|
|
3240
|
+
* @returns the rectangle to draw.
|
|
3241
|
+
*/
|
|
3242
|
+
const rectOf = (paneId, stored) => preview?.paneId === paneId ? preview.rect : legalFloatRect(stored, bounds);
|
|
3243
|
+
/**
|
|
3244
|
+
* Start a move or resize from a press on the panel's grip or corner. A release
|
|
3245
|
+
* that reaches the same legal rectangle as the press is a click and only raises,
|
|
3246
|
+
* also for a panel drawn projected from its record.
|
|
3247
|
+
*/
|
|
2878
3248
|
const drag = (mode, paneId, event) => {
|
|
2879
3249
|
event.stopPropagation();
|
|
2880
|
-
const
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
const
|
|
2884
|
-
const
|
|
2885
|
-
|
|
2886
|
-
y: Math.max(saved.y, windows ? caption + 20 : 0)
|
|
2887
|
-
};
|
|
3250
|
+
const drawn = settling.current?.paneId === paneId ? preview?.rect : void 0;
|
|
3251
|
+
if (cancelSettling() && drawn === void 0) setPreview(void 0);
|
|
3252
|
+
area.current = floatBounds();
|
|
3253
|
+
const reduce = reducedMotion();
|
|
3254
|
+
const shown = drawn ?? legalFloatRect(floatRect(getPane(state, paneId)), area.current);
|
|
3255
|
+
const rect = mode === "move" && !reduce ? unresistedRect(shown, area.current) : shown;
|
|
2888
3256
|
const start = {
|
|
2889
3257
|
mode,
|
|
2890
3258
|
originX: event.clientX,
|
|
2891
3259
|
originY: event.clientY,
|
|
2892
3260
|
rect
|
|
2893
3261
|
};
|
|
3262
|
+
/** The rectangle to draw for a pointer position, and the legal one a release there records. */
|
|
3263
|
+
const reached = (x, y) => {
|
|
3264
|
+
const desired = draggedRect(start, x, y);
|
|
3265
|
+
if (mode === "resize") {
|
|
3266
|
+
const resized = boundedResize(desired, area.current, FLOAT_MIN_SIZE);
|
|
3267
|
+
const legal = legalFloatRect(resized, area.current);
|
|
3268
|
+
return {
|
|
3269
|
+
drawn: reduce ? legal : resized,
|
|
3270
|
+
legal
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
const legal = legalFloatRect(desired, area.current);
|
|
3274
|
+
return {
|
|
3275
|
+
drawn: reduce ? legal : resistedRect(desired, area.current),
|
|
3276
|
+
legal
|
|
3277
|
+
};
|
|
3278
|
+
};
|
|
3279
|
+
const pressed = reached(event.clientX, event.clientY);
|
|
3280
|
+
const samples = [];
|
|
3281
|
+
sampleDrawn(samples, performance.now(), pressed.drawn);
|
|
2894
3282
|
begin(event.currentTarget, event.pointerId, {
|
|
2895
3283
|
move: (moved) => {
|
|
3284
|
+
const { drawn } = reached(moved.clientX, moved.clientY);
|
|
3285
|
+
sampleDrawn(samples, performance.now(), drawn);
|
|
2896
3286
|
setPreview({
|
|
2897
3287
|
paneId,
|
|
2898
|
-
rect:
|
|
3288
|
+
rect: drawn
|
|
2899
3289
|
});
|
|
2900
3290
|
},
|
|
2901
3291
|
up: (released) => {
|
|
2902
|
-
const
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
else intents.
|
|
3292
|
+
const { drawn, legal } = reached(released.clientX, released.clientY);
|
|
3293
|
+
sampleDrawn(samples, performance.now(), drawn);
|
|
3294
|
+
if (sameRect(legal, pressed.legal)) raise(paneId);
|
|
3295
|
+
else if (mode === "move") intents.moveFloat(paneId, legal.x, legal.y);
|
|
3296
|
+
else intents.resizeFloat(paneId, legal);
|
|
3297
|
+
if (!sameRect(drawn, legal)) settle(paneId, drawn, legal, releaseVelocity(samples));
|
|
2906
3298
|
}
|
|
2907
3299
|
});
|
|
2908
3300
|
};
|
|
2909
3301
|
return {
|
|
2910
3302
|
preview,
|
|
2911
3303
|
raise,
|
|
2912
|
-
drag
|
|
3304
|
+
drag,
|
|
3305
|
+
rectOf
|
|
2913
3306
|
};
|
|
2914
3307
|
}
|
|
2915
3308
|
/**
|
|
@@ -2973,22 +3366,19 @@ function FloatHeader({ paneId, tab, labels, intents, renderTabTitle, canCloseTab
|
|
|
2973
3366
|
}
|
|
2974
3367
|
/** Every floating panel, in z order. */
|
|
2975
3368
|
function FloatLayer({ state, intents, labels, renderTab, renderTabTitle, canCloseTab }) {
|
|
2976
|
-
const { preview, raise, drag } = useFloatGestures(state, intents);
|
|
3369
|
+
const { preview, raise, drag, rectOf } = useFloatGestures(state, intents);
|
|
2977
3370
|
return jsx(Fragment, { children: state.floats.map((paneId, depth) => {
|
|
2978
3371
|
const pane = getPane(state, paneId);
|
|
2979
3372
|
const tab = getTab(state, onlyTabId(pane));
|
|
2980
3373
|
const lifted = preview?.paneId === paneId ? preview.rect : void 0;
|
|
2981
|
-
const live =
|
|
3374
|
+
const live = rectOf(paneId, floatRect(pane));
|
|
2982
3375
|
return jsxs("div", {
|
|
2983
3376
|
className: css.float,
|
|
2984
3377
|
"data-dockkit-float": paneId,
|
|
2985
3378
|
tabIndex: -1,
|
|
2986
3379
|
"data-dockkit-float-active": state.activePaneId === paneId || void 0,
|
|
2987
3380
|
style: {
|
|
2988
|
-
|
|
2989
|
-
top: `max(var(--dsh-dockkit-float-top, 0px), ${live.y}px)`,
|
|
2990
|
-
width: live.width,
|
|
2991
|
-
height: live.height,
|
|
3381
|
+
...floatStyle(live, lifted !== void 0),
|
|
2992
3382
|
zIndex: lifted === void 0 ? depth + 1 : state.floats.length + 1
|
|
2993
3383
|
},
|
|
2994
3384
|
onPointerDown: () => {
|
|
@@ -3006,7 +3396,10 @@ function FloatLayer({ state, intents, labels, renderTab, renderTabTitle, canClos
|
|
|
3006
3396
|
}),
|
|
3007
3397
|
jsx("div", {
|
|
3008
3398
|
className: css.floatBody,
|
|
3009
|
-
children:
|
|
3399
|
+
children: jsx(TabBody, {
|
|
3400
|
+
tab,
|
|
3401
|
+
renderTab
|
|
3402
|
+
})
|
|
3010
3403
|
}),
|
|
3011
3404
|
jsx("div", {
|
|
3012
3405
|
className: css.floatResize,
|
|
@@ -3023,7 +3416,7 @@ function FloatLayer({ state, intents, labels, renderTab, renderTabTitle, canClos
|
|
|
3023
3416
|
//#region lib/types/components/TabLayout.js
|
|
3024
3417
|
/** Stable tab siblings in one horizontal Grid, including viewport-positioned floats. */
|
|
3025
3418
|
/** 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 }) {
|
|
3419
|
+
function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRequest, offsets, keepMounted, active = true }) {
|
|
3027
3420
|
const floating = pane.host === "float";
|
|
3028
3421
|
const selected = floating || pane.activeTabId === tab.id;
|
|
3029
3422
|
const visible = active && selected && (floating || state.expanded);
|
|
@@ -3067,7 +3460,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3067
3460
|
callbacks
|
|
3068
3461
|
]);
|
|
3069
3462
|
const lifted = floating && floats.preview?.paneId === pane.id ? floats.preview.rect : void 0;
|
|
3070
|
-
const rect = floating ?
|
|
3463
|
+
const rect = floating ? floats.rectOf(pane.id, floatRect(pane)) : void 0;
|
|
3071
3464
|
const depth = lifted === void 0 ? state.floats.indexOf(pane.id) + 1 : state.floats.length + 1;
|
|
3072
3465
|
return jsx("div", {
|
|
3073
3466
|
className: clsx(css.tabCell, floating && css.floatingCell),
|
|
@@ -3090,12 +3483,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3090
3483
|
"data-dockkit-float": floating ? pane.id : void 0,
|
|
3091
3484
|
"data-dockkit-float-active": floating && state.activePaneId === pane.id || void 0,
|
|
3092
3485
|
"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
|
-
},
|
|
3486
|
+
style: rect === void 0 ? void 0 : floatStyle(rect, lifted !== void 0),
|
|
3099
3487
|
onPointerDown: () => {
|
|
3100
3488
|
if (floating) floats.raise(pane.id);
|
|
3101
3489
|
},
|
|
@@ -3116,13 +3504,17 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3116
3504
|
}) : jsx(TabStrip, {
|
|
3117
3505
|
state,
|
|
3118
3506
|
pane,
|
|
3119
|
-
callbacks
|
|
3507
|
+
callbacks,
|
|
3508
|
+
offsets
|
|
3120
3509
|
}))
|
|
3121
3510
|
}),
|
|
3122
3511
|
jsxs("div", {
|
|
3123
3512
|
ref: body,
|
|
3124
3513
|
className: clsx(css.tabHostBody, floating ? css.floatBody : css.paneBody),
|
|
3125
|
-
children: [visited && (retained || active && selected) ?
|
|
3514
|
+
children: [visited && (retained || active && selected) ? jsx(TabBody, {
|
|
3515
|
+
tab,
|
|
3516
|
+
renderTab: callbacks.renderTab
|
|
3517
|
+
}) : null, !floating && jsx(PaneDropHints, {
|
|
3126
3518
|
pane,
|
|
3127
3519
|
callbacks
|
|
3128
3520
|
})]
|
|
@@ -3147,6 +3539,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe
|
|
|
3147
3539
|
function TabLayout(props) {
|
|
3148
3540
|
const { state, callbacks, preview } = props;
|
|
3149
3541
|
const focusRequest = useRef();
|
|
3542
|
+
const [offsets] = useState(() => /* @__PURE__ */ new Map());
|
|
3150
3543
|
const tabCallbacks = {
|
|
3151
3544
|
...callbacks,
|
|
3152
3545
|
onFocusTab: (tabId) => {
|
|
@@ -3177,7 +3570,8 @@ function TabLayout(props) {
|
|
|
3177
3570
|
pane,
|
|
3178
3571
|
column: panes.findIndex((candidate) => candidate.id === pane.id),
|
|
3179
3572
|
floats,
|
|
3180
|
-
focusRequest
|
|
3573
|
+
focusRequest,
|
|
3574
|
+
offsets
|
|
3181
3575
|
}, tab.id);
|
|
3182
3576
|
}),
|
|
3183
3577
|
panes.filter((pane) => pane.tabs.length === 0).map((pane) => jsx("div", {
|
|
@@ -3216,8 +3610,8 @@ function TabLayout(props) {
|
|
|
3216
3610
|
*
|
|
3217
3611
|
* A gesture only previews until it ends, then leaves through one intent, so the
|
|
3218
3612
|
* embedder's operation sequence stays the single source of truth. Releasing a tab
|
|
3219
|
-
* clear of this surface floats it; releasing inside it
|
|
3220
|
-
*
|
|
3613
|
+
* clear of this surface floats it; releasing inside it without an effective
|
|
3614
|
+
* drop target changes nothing.
|
|
3221
3615
|
*/
|
|
3222
3616
|
const NO_PREVIEW = {
|
|
3223
3617
|
draggingTabId: void 0,
|
|
@@ -3231,18 +3625,26 @@ const ALWAYS = () => true;
|
|
|
3231
3625
|
/**
|
|
3232
3626
|
* Resolve where a pointer sits inside the docked surface. An edge zone is only
|
|
3233
3627
|
* offered where the split it would make is allowed: within the pane budget and
|
|
3234
|
-
* with room for two halves
|
|
3628
|
+
* with room for two halves. Strip slots that preserve the dragged tab's position
|
|
3629
|
+
* offer no caret or placement intent. An undefined target inside the surface
|
|
3630
|
+
* leaves the layout unchanged on release.
|
|
3235
3631
|
*/
|
|
3236
|
-
function hitTest(root, x, y, canSplit, fits, dropZones) {
|
|
3632
|
+
function hitTest(root, x, y, tabId, canSplit, fits, dropZones) {
|
|
3237
3633
|
for (const [paneId, pane] of paneElements(root)) {
|
|
3238
3634
|
const rect = pane.getBoundingClientRect();
|
|
3239
3635
|
if (!containsPoint(rect, x, y)) continue;
|
|
3240
3636
|
const strip = pane.querySelector("[data-dockkit-strip]");
|
|
3241
|
-
if (strip !== null && containsPoint(strip.getBoundingClientRect(), x, y))
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3637
|
+
if (strip !== null && containsPoint(strip.getBoundingClientRect(), x, y)) {
|
|
3638
|
+
const tabs = [...strip.querySelectorAll("[data-dockkit-tab]")];
|
|
3639
|
+
const index = insertionIndex(tabs.map((tab) => tab.getBoundingClientRect()), x);
|
|
3640
|
+
const sourceIndex = tabs.findIndex((tab) => tab.dataset.dockkitTab === tabId);
|
|
3641
|
+
if (sourceIndex >= 0 && (index === sourceIndex || index === sourceIndex + 1)) return void 0;
|
|
3642
|
+
return {
|
|
3643
|
+
kind: "strip",
|
|
3644
|
+
paneId,
|
|
3645
|
+
index
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3246
3648
|
const zone = dropZones === "horizontal" ? canSplit && fitOf(fits, paneId).row ? x < rect.x + rect.width / 2 ? "left" : "right" : "center" : zoneInRect(rect, x, y);
|
|
3247
3649
|
if (zone !== "center") {
|
|
3248
3650
|
const fit = fitOf(fits, paneId);
|
|
@@ -3272,7 +3674,7 @@ function sameSizes(a, b) {
|
|
|
3272
3674
|
});
|
|
3273
3675
|
}
|
|
3274
3676
|
/** The split tree and the gestures over it. */
|
|
3275
|
-
function Surface({ state, canSplit, canAddTab, canCloseTab, intents, labels, renderTab, renderTabTitle, renderTabMenuItems, chrome, onRoom, draw, dropZones = "edges", minPaneFraction = MIN_PANE_FRACTION, hideSplitWhenBlocked = false }) {
|
|
3677
|
+
function Surface({ state, canSplit, canAddTab, canCloseTab, intents, labels, renderTab, renderTabTitle, renderTabMenuItems, getTabTriggerProps, tabMenuOpen, chrome, onRoom, draw, dropZones = "edges", minPaneFraction = MIN_PANE_FRACTION, hideSplitWhenBlocked = false }) {
|
|
3276
3678
|
const surface = useRef(null);
|
|
3277
3679
|
const [preview, setPreview] = useState(NO_PREVIEW);
|
|
3278
3680
|
const [fits, setFits] = useState(NO_FITS);
|
|
@@ -3334,12 +3736,12 @@ function Surface({ state, canSplit, canAddTab, canCloseTab, intents, labels, ren
|
|
|
3334
3736
|
setPreview({
|
|
3335
3737
|
...NO_PREVIEW,
|
|
3336
3738
|
draggingTabId: tabId,
|
|
3337
|
-
dropTarget: hitTest(root, moved.clientX, moved.clientY, canSplit, fits, dropZones)
|
|
3739
|
+
dropTarget: hitTest(root, moved.clientX, moved.clientY, tabId, canSplit, fits, dropZones)
|
|
3338
3740
|
});
|
|
3339
3741
|
},
|
|
3340
3742
|
up: (released) => {
|
|
3341
3743
|
if (!dragging) return;
|
|
3342
|
-
const target = hitTest(root, released.clientX, released.clientY, canSplit, fits, dropZones);
|
|
3744
|
+
const target = hitTest(root, released.clientX, released.clientY, tabId, canSplit, fits, dropZones);
|
|
3343
3745
|
if (target === void 0) {
|
|
3344
3746
|
if (containsPoint(root.getBoundingClientRect(), released.clientX, released.clientY)) return;
|
|
3345
3747
|
intents.floatTab(tabId, floatRectAt(released.clientX, released.clientY, FLOAT_DEFAULT_SIZE));
|
|
@@ -3393,6 +3795,8 @@ function Surface({ state, canSplit, canAddTab, canCloseTab, intents, labels, ren
|
|
|
3393
3795
|
renderTab,
|
|
3394
3796
|
renderTabTitle,
|
|
3395
3797
|
renderTabMenuItems,
|
|
3798
|
+
getTabTriggerProps,
|
|
3799
|
+
tabMenuOpen,
|
|
3396
3800
|
chromePaneId: topRightPaneId(state),
|
|
3397
3801
|
chrome
|
|
3398
3802
|
};
|