@dashfoo/react 0.5.1 → 0.6.0

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/dist/index.js CHANGED
@@ -2,12 +2,48 @@
2
2
 
3
3
  import { canRedo, canUndo, dashfooMachine, dragDockMachine, findTabset, fromJSON, normalize, reducer, resolveDockTarget, snapEnabled, snapSizes, stackModel, tabNodeSchema, toJSON, zoneRect } from "@dashfoo/core";
4
4
  import { Fragment, createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
- import { Accessibility, DragDropManager, Draggable, Feedback, PointerSensor } from "@dnd-kit/dom";
5
+ import { Accessibility, DragDropManager, Draggable, Droppable, Feedback, PointerSensor } from "@dnd-kit/dom";
6
6
  import { createStore, useStore } from "zustand";
7
+ import { CollisionPriority, CollisionType } from "@dnd-kit/abstract";
7
8
  import { useActorRef, useSelector } from "@xstate/react";
8
9
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
10
  import { Group, Panel as Panel$1, Separator } from "react-resizable-panels";
10
11
 
12
+ //#region src/lib/topmost-collision.ts
13
+ const createTopmostPointerIntersection = (topElementAt = (x, y) => document.elementFromPoint(x, y)) => {
14
+ let cached = null;
15
+ const topAt = (x, y) => {
16
+ if (cached && cached.x === x && cached.y === y) return cached.top;
17
+ const top = topElementAt(x, y);
18
+ cached = {
19
+ top,
20
+ x,
21
+ y
22
+ };
23
+ queueMicrotask(() => {
24
+ cached = null;
25
+ });
26
+ return top;
27
+ };
28
+ return ({ dragOperation, droppable }) => {
29
+ const { element } = droppable;
30
+ if (!element) return null;
31
+ const point = dragOperation.position.current;
32
+ const top = topAt(point.x, point.y);
33
+ if (!top || !element.contains(top)) return null;
34
+ let depth = 0;
35
+ for (let node = element; node; node = node.parentElement) depth += 1;
36
+ return {
37
+ id: droppable.id,
38
+ priority: CollisionPriority.High,
39
+ type: CollisionType.PointerIntersection,
40
+ value: depth
41
+ };
42
+ };
43
+ };
44
+ const topmostPointerIntersection = createTopmostPointerIntersection();
45
+
46
+ //#endregion
11
47
  //#region src/hooks/drag-hooks.tsx
12
48
  const INTERACTIVE_SELECTOR = `
13
49
  input:not([disabled]),
@@ -24,7 +60,7 @@ const preventActivation = (event, source) => {
24
60
  return interactive !== null && interactive !== source.element;
25
61
  };
26
62
  const createDragManager = () => new DragDropManager({
27
- plugins: (defaults) => defaults.filter((plugin) => plugin !== Accessibility && plugin !== Feedback),
63
+ plugins: (defaults) => [...defaults.filter((plugin) => plugin !== Accessibility && plugin !== Feedback), Feedback.configure({ dropAnimation: null })],
28
64
  sensors: () => [PointerSensor.configure({ preventActivation })]
29
65
  });
30
66
  const DragContext = createContext(null);
@@ -109,9 +145,37 @@ const useTabsetDraggable = (tabsetId, disabled = false, label = "") => {
109
145
  };
110
146
  const useTabsetDroppable = (tabsetId) => {
111
147
  const context = useContext(DragContext);
112
- return { ref: useCallback((element) => {
113
- context?.registerTabset(tabsetId, element);
114
- }, [context, tabsetId]) };
148
+ const elementRef = useRef(null);
149
+ const droppableRef = useRef(null);
150
+ const ref = useCallback((element) => {
151
+ elementRef.current = element;
152
+ if (droppableRef.current) droppableRef.current.element = element ?? void 0;
153
+ }, []);
154
+ const layerId = context?.layerId;
155
+ const manager = context?.manager;
156
+ useEffect(() => {
157
+ if (!manager || layerId === void 0) return;
158
+ const droppable = new Droppable({
159
+ collisionDetector: topmostPointerIntersection,
160
+ data: {
161
+ layerId,
162
+ tabsetId,
163
+ type: "tabset"
164
+ },
165
+ id: `${layerId}:${tabsetId}`
166
+ }, manager);
167
+ droppable.element = elementRef.current ?? void 0;
168
+ droppableRef.current = droppable;
169
+ return () => {
170
+ droppable.destroy();
171
+ droppableRef.current = null;
172
+ };
173
+ }, [
174
+ layerId,
175
+ manager,
176
+ tabsetId
177
+ ]);
178
+ return { ref };
115
179
  };
116
180
  const useExternalTabSource = ({ createTab, disabled = false, label = "" }) => {
117
181
  const id = useId();
@@ -422,31 +486,6 @@ const useDashfooStore = (options) => {
422
486
  ]);
423
487
  };
424
488
 
425
- //#endregion
426
- //#region src/components/drag-root.tsx
427
- const DashfooDragProvider = ({ children }) => {
428
- const [manager] = useState(createDragManager);
429
- useInsertionEffect(() => () => manager.destroy(), [manager]);
430
- const [subjectStore] = useState(createDragSubjectStore);
431
- return /* @__PURE__ */ jsx(SharedDragManagerContext.Provider, {
432
- value: manager,
433
- children: /* @__PURE__ */ jsx(DragSubjectStoreContext.Provider, {
434
- value: subjectStore,
435
- children
436
- })
437
- });
438
- };
439
-
440
- //#endregion
441
- //#region src/hooks/layout-store.ts
442
- const createLayoutStore = (initial) => createStore(() => initial);
443
- const LayoutStoreContext = createContext(null);
444
- const useLayout = (selector) => {
445
- const store = useContext(LayoutStoreContext);
446
- if (store === null) throw new Error("dashfoo components must be rendered inside <DashfooLayout> or <Layout.Root>.");
447
- return useStore(store, selector);
448
- };
449
-
450
489
  //#endregion
451
490
  //#region src/lib/tab-insertion.ts
452
491
  const pointInRect = (point, rect) => point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
@@ -472,15 +511,6 @@ const shouldAllowDrop = (draggedId, targetId, tabIds) => {
472
511
  return true;
473
512
  };
474
513
 
475
- //#endregion
476
- //#region src/lib/warn-once.ts
477
- const warned = /* @__PURE__ */ new Set();
478
- const warnOnce = (key, message) => {
479
- if (warned.has(key)) return;
480
- warned.add(key);
481
- console.warn(`[dashfoo] ${message}`);
482
- };
483
-
484
514
  //#endregion
485
515
  //#region src/components/drag-overlays.tsx
486
516
  const overlayBase = {
@@ -511,37 +541,107 @@ const DockIndicator = ({ actorRef, getTabsetElement }) => {
511
541
  if (!intent) return null;
512
542
  const element = getTabsetElement(intent.targetId);
513
543
  if (!element) return null;
544
+ const zIndex = element.closest("[data-dashfoo=\"float\"]") ? overlayBase.zIndex : "auto";
514
545
  if (intent.location === "center") {
515
546
  const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
516
547
  if (strip) return /* @__PURE__ */ jsx("div", {
517
548
  "data-dashfoo": "dock-indicator",
518
- style: lineStyle(insertionLineRect(strip.getBoundingClientRect(), tabItemRects(strip, draggedId), intent.index ?? 0))
549
+ style: {
550
+ ...lineStyle(insertionLineRect(strip.getBoundingClientRect(), tabItemRects(strip, draggedId), intent.index ?? 0)),
551
+ zIndex
552
+ }
519
553
  });
520
554
  }
521
555
  return /* @__PURE__ */ jsx("div", {
522
556
  "data-dashfoo": "dock-indicator",
523
- style: paneStyle(zoneRect(element.getBoundingClientRect(), intent.location))
557
+ style: {
558
+ ...paneStyle(zoneRect(element.getBoundingClientRect(), intent.location)),
559
+ zIndex
560
+ }
524
561
  });
525
562
  };
526
- const previewStyle = {
527
- left: 0,
528
- position: "fixed",
529
- top: 0,
530
- zIndex: 9999
563
+ const PREVIEW_OFFSET = {
564
+ x: 12,
565
+ y: 8
531
566
  };
532
- const DragPreview = ({ overlayRef, preview }) => {
533
- if (!preview) return null;
567
+ const labelOf = (source) => {
568
+ const raw = source?.data?.label;
569
+ return typeof raw === "string" ? raw : "";
570
+ };
571
+ const DragPreviewOverlay = ({ manager }) => {
572
+ const [chip, setChip] = useState(null);
573
+ useEffect(() => {
574
+ const offStart = manager.monitor.addEventListener("dragstart", (event) => {
575
+ const source = event.operation.source;
576
+ if (!source) return;
577
+ const rect = source.element?.getBoundingClientRect();
578
+ const point = event.operation.position.current;
579
+ setChip({
580
+ label: labelOf(source),
581
+ x: PREVIEW_OFFSET.x + (rect ? point.x - rect.left : 0),
582
+ y: PREVIEW_OFFSET.y + (rect ? point.y - rect.top : 0)
583
+ });
584
+ });
585
+ const offEnd = manager.monitor.addEventListener("dragend", () => setChip(null));
586
+ return () => {
587
+ offStart();
588
+ offEnd();
589
+ };
590
+ }, [manager]);
534
591
  return /* @__PURE__ */ jsx("div", {
535
- "data-dashfoo": "drag-preview",
536
- ref: overlayRef,
537
- style: {
538
- ...previewStyle,
539
- transform: `translate(${preview.x}px, ${preview.y}px)`
540
- },
541
- children: preview.label
592
+ "data-dnd-overlay": "",
593
+ ref: useCallback((element) => {
594
+ const feedback = manager.registry.plugins.get(Feedback);
595
+ if (feedback) feedback.overlay = element ?? void 0;
596
+ }, [manager]),
597
+ children: chip === null ? null : /* @__PURE__ */ jsx("div", {
598
+ "data-dashfoo": "drag-preview",
599
+ style: {
600
+ left: 0,
601
+ position: "absolute",
602
+ top: 0,
603
+ transform: `translate(${chip.x}px, ${chip.y}px)`,
604
+ width: "max-content"
605
+ },
606
+ children: chip.label
607
+ })
542
608
  });
543
609
  };
544
610
 
611
+ //#endregion
612
+ //#region src/components/drag-root.tsx
613
+ const DashfooDragProvider = ({ children }) => {
614
+ const [manager] = useState(createDragManager);
615
+ useInsertionEffect(() => () => manager.destroy(), [manager]);
616
+ const [subjectStore] = useState(createDragSubjectStore);
617
+ return /* @__PURE__ */ jsx(SharedDragManagerContext.Provider, {
618
+ value: manager,
619
+ children: /* @__PURE__ */ jsxs(DragSubjectStoreContext.Provider, {
620
+ value: subjectStore,
621
+ children: [children, /* @__PURE__ */ jsx(DragPreviewOverlay, { manager })]
622
+ })
623
+ });
624
+ };
625
+
626
+ //#endregion
627
+ //#region src/hooks/layout-store.ts
628
+ const createLayoutStore = (initial) => createStore(() => initial);
629
+ const LayoutStoreContext = createContext(null);
630
+ const useLayout = (selector) => {
631
+ const store = useContext(LayoutStoreContext);
632
+ if (store === null) throw new Error("dashfoo components must be rendered inside <DashfooLayout> or <Layout.Root>.");
633
+ return useStore(store, selector);
634
+ };
635
+
636
+ //#endregion
637
+ //#region src/lib/warn-once.ts
638
+ const warned = /* @__PURE__ */ new Set();
639
+ const warnOnce = (key, message) => {
640
+ if (warned.has(key)) return;
641
+ warned.add(key);
642
+ console.warn(`[dashfoo] ${message}`);
643
+ };
644
+
545
645
  //#endregion
546
646
  //#region src/components/drag-adapter.tsx
547
647
  const tabRects = (strip, excludeId) => [...strip.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => tab.dataset.tabId === excludeId ? [] : [tab.getBoundingClientRect()]);
@@ -564,14 +664,6 @@ const intentForTabset = (id, element, point, draggedId) => {
564
664
  targetId: id
565
665
  };
566
666
  };
567
- const PREVIEW_OFFSET = {
568
- x: 12,
569
- y: 8
570
- };
571
- const labelOf = (source) => {
572
- const raw = source?.data?.label;
573
- return typeof raw === "string" ? raw : "";
574
- };
575
667
  const isTabFactory = (value) => typeof value === "function";
576
668
  const subjectFor = (source) => {
577
669
  const data = source.data;
@@ -610,9 +702,6 @@ const subjectFor = (source) => {
610
702
  const DragProvider = ({ children, onCommit, splitDock }) => {
611
703
  const actorRef = useActorRef(dragDockMachine);
612
704
  const layoutStore = useContext(LayoutStoreContext);
613
- const [tabsets] = useState(() => /* @__PURE__ */ new Map());
614
- const overlayRef = useRef(null);
615
- const [preview, setPreview] = useState(null);
616
705
  const sharedManager = useContext(SharedDragManagerContext);
617
706
  const [ownManager] = useState(() => sharedManager ? null : createDragManager());
618
707
  useInsertionEffect(() => () => ownManager?.destroy(), [ownManager]);
@@ -650,6 +739,10 @@ const DragProvider = ({ children, onCommit, splitDock }) => {
650
739
  onCommitRef.current = onCommit;
651
740
  splitDockRef.current = splitDock;
652
741
  });
742
+ const getTabsetElement = useCallback((id) => {
743
+ const element = manager.registry.droppables.get(`${ownerId}:${id}`)?.element;
744
+ return element instanceof HTMLElement ? element : void 0;
745
+ }, [manager, ownerId]);
653
746
  useEffect(() => {
654
747
  const subscription = actorRef.on("COMMIT", (emitted) => {
655
748
  if (layoutStore?.getState().editable === false) return;
@@ -664,23 +757,7 @@ const DragProvider = ({ children, onCommit, splitDock }) => {
664
757
  subscription.unsubscribe();
665
758
  };
666
759
  }, [actorRef, layoutStore]);
667
- const registerTabset = useCallback((id, element) => {
668
- if (element) tabsets.set(id, element);
669
- else tabsets.delete(id);
670
- }, [tabsets]);
671
- const getTabsetElement = useCallback((id) => tabsets.get(id), [tabsets]);
672
- const attachOverlay = useCallback((element) => {
673
- overlayRef.current = element;
674
- }, []);
675
760
  useEffect(() => {
676
- const tabsetAt = (point) => {
677
- const top = document.elementFromPoint(point.x, point.y);
678
- if (!top) return;
679
- for (const [id, element] of tabsets) if (element.contains(top)) return {
680
- element,
681
- id
682
- };
683
- };
684
761
  const resolveIntent = (targetId, element, point, draggedId) => {
685
762
  if (layoutStore?.getState().editable === false) return null;
686
763
  if (!shouldAllowDrop(draggedId, targetId, [...element.querySelectorAll("[data-dashfoo=\"tab\"]")].map((tab) => tab.dataset.tabId ?? ""))) return null;
@@ -691,71 +768,62 @@ const DragProvider = ({ children, onCommit, splitDock }) => {
691
768
  };
692
769
  return intent;
693
770
  };
694
- const positionOverlay = (point) => {
695
- const element = overlayRef.current;
696
- if (element) element.style.transform = `translate(${point.x + PREVIEW_OFFSET.x}px, ${point.y + PREVIEW_OFFSET.y}px)`;
697
- };
698
771
  const handleStart = (event) => {
699
772
  const source = event.operation.source;
700
773
  if (!source) return;
701
774
  const subject = subjectFor(source);
702
- if (!subject) return;
703
- const point = event.operation.position.current;
704
- setPreview({
705
- label: labelOf(source),
706
- x: point.x + PREVIEW_OFFSET.x,
707
- y: point.y + PREVIEW_OFFSET.y
708
- });
775
+ if (!subject) {
776
+ manager.actions.stop({ canceled: true });
777
+ return;
778
+ }
709
779
  actorRef.send({
710
780
  subject,
711
781
  type: "START"
712
782
  });
713
783
  };
714
- const handleMove = (event) => {
715
- const op = event.operation;
716
- const point = op.position.current;
717
- positionOverlay(point);
718
- const draggedId = op.source ? String(op.source.id) : void 0;
719
- const hit = tabsetAt(point);
784
+ const syncIntent = () => {
785
+ const operation = manager.dragOperation;
786
+ const point = operation.position.current;
787
+ const target = operation.target;
788
+ const data = target?.data;
789
+ const tabsetId = data?.layerId === ownerId ? data.tabsetId : void 0;
790
+ const element = tabsetId === void 0 ? void 0 : target?.element;
791
+ const draggedId = operation.source ? String(operation.source.id) : void 0;
720
792
  actorRef.send({
721
- intent: hit ? resolveIntent(hit.id, hit.element, point, draggedId) : null,
793
+ intent: tabsetId !== void 0 && element instanceof HTMLElement ? resolveIntent(tabsetId, element, point, draggedId) : null,
722
794
  type: "OVER"
723
795
  });
724
796
  };
797
+ const handleMove = () => syncIntent();
798
+ const handleCollision = () => syncIntent();
725
799
  const handleEnd = (event) => {
726
- setPreview(null);
727
800
  if (event.canceled) {
728
801
  actorRef.send({ type: "CANCEL" });
729
802
  return;
730
803
  }
731
- const op = event.operation;
732
- const point = op.position.current;
733
- const draggedId = op.source ? String(op.source.id) : void 0;
734
- const hit = tabsetAt(point);
735
- if (hit) actorRef.send({
736
- intent: resolveIntent(hit.id, hit.element, point, draggedId),
737
- type: "OVER"
738
- });
804
+ syncIntent();
739
805
  actorRef.send({ type: "DROP" });
740
806
  };
741
807
  const offStart = manager.monitor.addEventListener("dragstart", handleStart);
742
808
  const offMove = manager.monitor.addEventListener("dragmove", handleMove);
809
+ const offCollision = manager.monitor.addEventListener("collision", handleCollision);
743
810
  const offEnd = manager.monitor.addEventListener("dragend", handleEnd);
744
811
  return () => {
745
812
  offStart();
746
813
  offMove();
814
+ offCollision();
747
815
  offEnd();
748
816
  };
749
817
  }, [
750
818
  actorRef,
751
819
  layoutStore,
752
820
  manager,
753
- tabsets
821
+ ownerId
754
822
  ]);
755
823
  const contextValue = useMemo(() => ({
756
- manager,
757
- registerTabset
758
- }), [manager, registerTabset]);
824
+ layerId: ownerId,
825
+ manager
826
+ }), [manager, ownerId]);
759
827
  return /* @__PURE__ */ jsx(DragContext.Provider, {
760
828
  value: contextValue,
761
829
  children: /* @__PURE__ */ jsxs(DragSubjectStoreContext.Provider, {
@@ -766,10 +834,7 @@ const DragProvider = ({ children, onCommit, splitDock }) => {
766
834
  actorRef,
767
835
  getTabsetElement
768
836
  }),
769
- /* @__PURE__ */ jsx(DragPreview, {
770
- overlayRef: attachOverlay,
771
- preview
772
- })
837
+ ownManager === null ? null : /* @__PURE__ */ jsx(DragPreviewOverlay, { manager: ownManager })
773
838
  ]
774
839
  })
775
840
  });
@@ -825,6 +890,125 @@ const resizeRect = (start, edges, dx, dy) => {
825
890
  };
826
891
  };
827
892
 
893
+ //#endregion
894
+ //#region src/components/float-resize-handles.ts
895
+ const EDGE_THICKNESS = 12;
896
+ const EDGE_OFFSET = -6;
897
+ const CORNER_SIZE = 20;
898
+ const CORNER_OFFSET = -8;
899
+ const EDGE_INSET = 12;
900
+ const RESIZE_HANDLES = [
901
+ {
902
+ edges: {
903
+ x: 0,
904
+ y: -1
905
+ },
906
+ key: "n",
907
+ style: {
908
+ cursor: "ns-resize",
909
+ height: EDGE_THICKNESS,
910
+ insetInline: EDGE_INSET,
911
+ top: EDGE_OFFSET
912
+ }
913
+ },
914
+ {
915
+ edges: {
916
+ x: 0,
917
+ y: 1
918
+ },
919
+ key: "s",
920
+ style: {
921
+ bottom: EDGE_OFFSET,
922
+ cursor: "ns-resize",
923
+ height: EDGE_THICKNESS,
924
+ insetInline: EDGE_INSET
925
+ }
926
+ },
927
+ {
928
+ edges: {
929
+ x: 1,
930
+ y: 0
931
+ },
932
+ key: "e",
933
+ style: {
934
+ cursor: "ew-resize",
935
+ insetBlock: EDGE_INSET,
936
+ right: EDGE_OFFSET,
937
+ width: EDGE_THICKNESS
938
+ }
939
+ },
940
+ {
941
+ edges: {
942
+ x: -1,
943
+ y: 0
944
+ },
945
+ key: "w",
946
+ style: {
947
+ cursor: "ew-resize",
948
+ insetBlock: EDGE_INSET,
949
+ left: EDGE_OFFSET,
950
+ width: EDGE_THICKNESS
951
+ }
952
+ },
953
+ {
954
+ edges: {
955
+ x: 1,
956
+ y: -1
957
+ },
958
+ key: "ne",
959
+ style: {
960
+ cursor: "nesw-resize",
961
+ height: CORNER_SIZE,
962
+ right: CORNER_OFFSET,
963
+ top: CORNER_OFFSET,
964
+ width: CORNER_SIZE
965
+ }
966
+ },
967
+ {
968
+ edges: {
969
+ x: -1,
970
+ y: -1
971
+ },
972
+ key: "nw",
973
+ style: {
974
+ cursor: "nwse-resize",
975
+ height: CORNER_SIZE,
976
+ left: CORNER_OFFSET,
977
+ top: CORNER_OFFSET,
978
+ width: CORNER_SIZE
979
+ }
980
+ },
981
+ {
982
+ edges: {
983
+ x: 1,
984
+ y: 1
985
+ },
986
+ key: "se",
987
+ style: {
988
+ bottom: CORNER_OFFSET,
989
+ cursor: "nwse-resize",
990
+ height: CORNER_SIZE,
991
+ right: CORNER_OFFSET,
992
+ width: CORNER_SIZE
993
+ }
994
+ },
995
+ {
996
+ edges: {
997
+ x: -1,
998
+ y: 1
999
+ },
1000
+ key: "sw",
1001
+ style: {
1002
+ bottom: CORNER_OFFSET,
1003
+ cursor: "nesw-resize",
1004
+ height: CORNER_SIZE,
1005
+ left: CORNER_OFFSET,
1006
+ width: CORNER_SIZE
1007
+ }
1008
+ }
1009
+ ];
1010
+ const EDGE_BY_KEY = new Map(RESIZE_HANDLES.map((h) => [h.key, h.edges]));
1011
+
828
1012
  //#endregion
829
1013
  //#region src/components/layout-root.tsx
830
1014
  const DEFAULT_TABSET_MIN_SIZE = 320;
@@ -1148,6 +1332,7 @@ const TabOverflowMenu = ({ items, onSelect }) => {
1148
1332
  "data-dashfoo": "tab-overflow",
1149
1333
  onClick: handleToggle,
1150
1334
  ref: triggerRef,
1335
+ title: "More tabs",
1151
1336
  type: "button",
1152
1337
  children: /* @__PURE__ */ jsx(OverflowIcon, {})
1153
1338
  }), open ? /* @__PURE__ */ jsx("div", {
@@ -1547,6 +1732,7 @@ const TabsetCloseButton = ({ children, onClick, ...props }) => {
1547
1732
  };
1548
1733
  return /* @__PURE__ */ jsx("button", {
1549
1734
  "aria-label": `Close ${tab.name}`,
1735
+ title: `Close ${tab.name}`,
1550
1736
  ...props,
1551
1737
  "data-dashfoo": "tab-close",
1552
1738
  onClick: handleClick,
@@ -1638,6 +1824,7 @@ const TabsetGrip = ({ children, ref: userRef, ...props }) => {
1638
1824
  if (hidden) return null;
1639
1825
  return /* @__PURE__ */ jsx("button", {
1640
1826
  "aria-label": "Move tabset",
1827
+ title: "Move tabset",
1641
1828
  ...props,
1642
1829
  "data-dashfoo": "tabset-grip",
1643
1830
  ref: refCallback,
@@ -1656,6 +1843,7 @@ const TabsetMaximizeButton = ({ children, onClick, ...props }) => {
1656
1843
  };
1657
1844
  return /* @__PURE__ */ jsx("button", {
1658
1845
  "aria-label": isMaximized ? "Restore" : "Maximize",
1846
+ title: isMaximized ? "Restore tabset" : "Maximize tabset",
1659
1847
  ...props,
1660
1848
  "aria-pressed": isMaximized,
1661
1849
  "data-dashfoo": "tabset-maximize",
@@ -1685,6 +1873,7 @@ const TabsetFloatButton = ({ children, onClick, ...props }) => {
1685
1873
  };
1686
1874
  return /* @__PURE__ */ jsx("button", {
1687
1875
  "aria-label": "Float panel",
1876
+ title: "Float panel",
1688
1877
  ...props,
1689
1878
  "data-dashfoo": "tabset-float",
1690
1879
  onClick: handleClick,
@@ -1917,117 +2106,6 @@ const bodyStyle = {
1917
2106
  minHeight: 0,
1918
2107
  position: "relative"
1919
2108
  };
1920
- const RESIZE_HANDLES = [
1921
- {
1922
- edges: {
1923
- x: 0,
1924
- y: -1
1925
- },
1926
- key: "n",
1927
- style: {
1928
- cursor: "ns-resize",
1929
- height: 6,
1930
- insetInline: 10,
1931
- top: -3
1932
- }
1933
- },
1934
- {
1935
- edges: {
1936
- x: 0,
1937
- y: 1
1938
- },
1939
- key: "s",
1940
- style: {
1941
- bottom: -3,
1942
- cursor: "ns-resize",
1943
- height: 6,
1944
- insetInline: 10
1945
- }
1946
- },
1947
- {
1948
- edges: {
1949
- x: 1,
1950
- y: 0
1951
- },
1952
- key: "e",
1953
- style: {
1954
- cursor: "ew-resize",
1955
- insetBlock: 10,
1956
- right: -3,
1957
- width: 6
1958
- }
1959
- },
1960
- {
1961
- edges: {
1962
- x: -1,
1963
- y: 0
1964
- },
1965
- key: "w",
1966
- style: {
1967
- cursor: "ew-resize",
1968
- insetBlock: 10,
1969
- left: -3,
1970
- width: 6
1971
- }
1972
- },
1973
- {
1974
- edges: {
1975
- x: 1,
1976
- y: -1
1977
- },
1978
- key: "ne",
1979
- style: {
1980
- cursor: "nesw-resize",
1981
- height: 14,
1982
- right: -4,
1983
- top: -4,
1984
- width: 14
1985
- }
1986
- },
1987
- {
1988
- edges: {
1989
- x: -1,
1990
- y: -1
1991
- },
1992
- key: "nw",
1993
- style: {
1994
- cursor: "nwse-resize",
1995
- height: 14,
1996
- left: -4,
1997
- top: -4,
1998
- width: 14
1999
- }
2000
- },
2001
- {
2002
- edges: {
2003
- x: 1,
2004
- y: 1
2005
- },
2006
- key: "se",
2007
- style: {
2008
- bottom: -4,
2009
- cursor: "nwse-resize",
2010
- height: 14,
2011
- right: -4,
2012
- width: 14
2013
- }
2014
- },
2015
- {
2016
- edges: {
2017
- x: -1,
2018
- y: 1
2019
- },
2020
- key: "sw",
2021
- style: {
2022
- bottom: -4,
2023
- cursor: "nesw-resize",
2024
- height: 14,
2025
- left: -4,
2026
- width: 14
2027
- }
2028
- }
2029
- ];
2030
- const EDGE_BY_KEY = new Map(RESIZE_HANDLES.map((h) => [h.key, h.edges]));
2031
2109
  const CHIP_SIZE = {
2032
2110
  height: 34,
2033
2111
  width: 168
@@ -2120,10 +2198,12 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2120
2198
  const handlePointerDown = (event) => {
2121
2199
  const panel = panelRef.current;
2122
2200
  if (!panel || !editable) return;
2201
+ const prior = gestureRef.current;
2202
+ if (prior && panel.hasPointerCapture?.(prior.pointerId)) panel.releasePointerCapture(prior.pointerId);
2123
2203
  movedRef.current = false;
2124
2204
  const edgeKey = event.currentTarget.dataset.edge;
2125
2205
  const parent = panel.offsetParent;
2126
- gestureRef.current = {
2206
+ const gesture = {
2127
2207
  bounds: {
2128
2208
  height: parent?.clientHeight ?? 0,
2129
2209
  width: parent?.clientWidth ?? 0
@@ -2134,18 +2214,40 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2134
2214
  startX: event.clientX,
2135
2215
  startY: event.clientY
2136
2216
  };
2217
+ gestureRef.current = gesture;
2137
2218
  latestRef.current = node.geometry;
2219
+ if (gesture.edges) panel.setPointerCapture(event.pointerId);
2220
+ };
2221
+ const handlePointerUp = (event) => {
2222
+ const gesture = gestureRef.current;
2223
+ if (!gesture || event.pointerId !== gesture.pointerId) return;
2224
+ gestureRef.current = null;
2225
+ const panel = panelRef.current;
2226
+ if (panel?.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
2227
+ if (!movedRef.current) {
2228
+ if (node.minimized) restore();
2229
+ return;
2230
+ }
2231
+ dispatch({
2232
+ floatId: node.id,
2233
+ geometry: latestRef.current,
2234
+ type: "moveFloat"
2235
+ });
2138
2236
  };
2139
2237
  const handlePointerMove = (event) => {
2140
2238
  const gesture = gestureRef.current;
2141
2239
  const panel = panelRef.current;
2142
2240
  if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
2241
+ if (event.buttons === 0) {
2242
+ handlePointerUp(event);
2243
+ return;
2244
+ }
2143
2245
  const dx = event.clientX - gesture.startX;
2144
2246
  const dy = event.clientY - gesture.startY;
2145
2247
  if (!movedRef.current) {
2146
2248
  if (Math.hypot(dx, dy) <= TAP_SLOP) return;
2147
2249
  movedRef.current = true;
2148
- panel.setPointerCapture(gesture.pointerId);
2250
+ if (!gesture.edges) panel.setPointerCapture(gesture.pointerId);
2149
2251
  }
2150
2252
  const next = gesture.edges ? resizeRect(gesture.start, gesture.edges, dx, dy) : clampToBounds({
2151
2253
  ...gesture.start,
@@ -2160,20 +2262,19 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2160
2262
  panel.style.height = `${next.height}px`;
2161
2263
  }
2162
2264
  };
2163
- const handlePointerUp = (event) => {
2265
+ const handlePointerCancel = (event) => {
2164
2266
  const gesture = gestureRef.current;
2165
- if (!gesture || event.pointerId !== gesture.pointerId) return;
2267
+ const panel = panelRef.current;
2268
+ if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
2166
2269
  gestureRef.current = null;
2167
- if (!movedRef.current) {
2168
- if (node.minimized) restore();
2169
- return;
2270
+ if (panel.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
2271
+ if (!movedRef.current) return;
2272
+ panel.style.left = `${gesture.start.left}px`;
2273
+ panel.style.top = `${gesture.start.top}px`;
2274
+ if (!node.minimized) {
2275
+ panel.style.width = `${gesture.start.width}px`;
2276
+ panel.style.height = `${gesture.start.height}px`;
2170
2277
  }
2171
- panelRef.current?.releasePointerCapture(event.pointerId);
2172
- dispatch({
2173
- floatId: node.id,
2174
- geometry: latestRef.current,
2175
- type: "moveFloat"
2176
- });
2177
2278
  };
2178
2279
  if (node.minimized) return /* @__PURE__ */ jsxs("button", {
2179
2280
  "aria-label": `Restore ${floatTitle(node)} panel`,
@@ -2184,6 +2285,7 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2184
2285
  restore();
2185
2286
  }
2186
2287
  },
2288
+ onPointerCancel: handlePointerCancel,
2187
2289
  onPointerDown: handlePointerDown,
2188
2290
  onPointerDownCapture: onFocus,
2189
2291
  onPointerMove: handlePointerMove,
@@ -2210,6 +2312,7 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2210
2312
  };
2211
2313
  return /* @__PURE__ */ jsxs("div", {
2212
2314
  "data-dashfoo": "float",
2315
+ onPointerCancel: handlePointerCancel,
2213
2316
  onPointerDownCapture: onFocus,
2214
2317
  onPointerMove: handlePointerMove,
2215
2318
  onPointerUp: handlePointerUp,
@@ -2252,6 +2355,7 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2252
2355
  }),
2253
2356
  onPointerDown: (event) => event.stopPropagation(),
2254
2357
  style: dockButtonStyle,
2358
+ title: "Minimize panel",
2255
2359
  type: "button",
2256
2360
  children: /* @__PURE__ */ jsx(MinimizeIcon, {})
2257
2361
  }), /* @__PURE__ */ jsx("button", {
@@ -2263,6 +2367,7 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2263
2367
  }),
2264
2368
  onPointerDown: (event) => event.stopPropagation(),
2265
2369
  style: dockButtonStyle,
2370
+ title: "Dock panel back",
2266
2371
  type: "button",
2267
2372
  children: /* @__PURE__ */ jsx(DockIcon, {})
2268
2373
  })] })
@@ -2310,7 +2415,8 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2310
2415
  const overlayStyle = {
2311
2416
  inset: 0,
2312
2417
  pointerEvents: "none",
2313
- position: "fixed"
2418
+ position: "fixed",
2419
+ zIndex: 1
2314
2420
  };
2315
2421
  const FloatOverlay = ({ floats, global }) => {
2316
2422
  const [topId, setTopId] = useState(null);