@dashfoo/react 0.6.3 → 0.7.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
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { canRedo, canUndo, dashfooMachine, dragDockMachine, findTabset, fromJSON, normalize, reducer, resolveDockTarget, snapEnabled, snapSizes, stackModel, tabNodeSchema, toJSON, zoneRect } from "@dashfoo/core";
3
+ import { canRedo, canUndo, dashfooMachine, decideSnap, dockLocationFor, dragDockMachine, findTabset, fromJSON, normalize, resolveDockTarget, settleSnap, snapEnabled, splitEdge, stackModel, tabNodeSchema, toJSON, zoneRect } from "@dashfoo/core";
4
4
  import { Fragment, createContext, createElement, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
5
  import { Accessibility, DragDropManager, Draggable, Droppable, Feedback, PointerSensor } from "@dnd-kit/dom";
6
- import { createStore, useStore } from "zustand";
7
- import { CollisionPriority, CollisionType } from "@dnd-kit/abstract";
8
6
  import { useActorRef, useSelector } from "@xstate/react";
7
+ import { CollisionPriority, CollisionType } from "@dnd-kit/abstract";
9
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
+ import { createStore, useStore } from "zustand";
10
10
  import { Group, Panel as Panel$1, Separator } from "react-resizable-panels";
11
11
 
12
12
  //#region src/lib/topmost-collision.ts
@@ -63,17 +63,15 @@ const createDragManager = () => new DragDropManager({
63
63
  plugins: (defaults) => [...defaults.filter((plugin) => plugin !== Accessibility && plugin !== Feedback), Feedback.configure({ dropAnimation: null })],
64
64
  sensors: () => [PointerSensor.configure({ preventActivation })]
65
65
  });
66
+ const DragRootContext = createContext(null);
66
67
  const DragContext = createContext(null);
67
68
  const SharedDragManagerContext = createContext(null);
68
- const createDragSubjectStore = () => createStore(() => ({
69
- intent: null,
70
- intentOwner: null,
71
- subject: null
72
- }));
69
+ /**
70
+ * The actor assigns a freshly built intent on every pointer move, so selecting
71
+ * `context.intent` by reference would re-render every reader of `useDropIntent`
72
+ * for each move that resolved to the same slot.
73
+ */
73
74
  const sameDropIntent = (a, b) => a === b || a !== null && b !== null && a.index === b.index && a.location === b.location && a.targetId === b.targetId;
74
- const sameDragSubject = (a, b) => a === b || a !== null && b !== null && a.id === b.id && a.kind === b.kind;
75
- const nullSubjectStore = createDragSubjectStore();
76
- const DragSubjectStoreContext = createContext(null);
77
75
  const useDraggableHandle = () => {
78
76
  const elementRef = useRef(null);
79
77
  const draggableRef = useRef(null);
@@ -194,11 +192,20 @@ const useExternalTabSource = ({ createTab, disabled = false, label = "" }) => {
194
192
  id: `external-${id}`
195
193
  }, disabled, label);
196
194
  };
195
+ /**
196
+ * Both drag readers are documented as working anywhere under the provider, which
197
+ * includes a custom overlay mounted beside the layout rather than inside a drag
198
+ * layer, so they read the root's actor. Outside a provider entirely there is no
199
+ * actor to read and they report the empty state instead of throwing: a component
200
+ * that renders both inside and outside a layout should not crash in one of them.
201
+ */
197
202
  const useDragSubject = () => {
198
- return useStore(useContext(DragSubjectStoreContext) ?? nullSubjectStore, (state) => state.subject);
203
+ const actorRef = useContext(DragRootContext)?.actorRef;
204
+ return useSelector(actorRef, (snapshot) => snapshot?.context.subject ?? null);
199
205
  };
200
206
  const useDropIntent = () => {
201
- return useStore(useContext(DragSubjectStoreContext) ?? nullSubjectStore, (state) => state.intent);
207
+ const actorRef = useContext(DragRootContext)?.actorRef;
208
+ return useSelector(actorRef, (snapshot) => snapshot?.context.intent ?? null, sameDropIntent);
202
209
  };
203
210
 
204
211
  //#endregion
@@ -269,7 +276,7 @@ const usePersistence = (config, defaultModel) => {
269
276
  const loadedKey = useRef(config?.key);
270
277
  useEffect(() => {
271
278
  if (config !== null && config.key !== loadedKey.current) {
272
- console.warn("[dashfoo] persist key changed for a mounted layout; the displayed layout still reflects the previous key — remount (key={persistKey}) to load the new key");
279
+ console.warn("[dashfoo] persist key changed for a mounted layout; the displayed layout still reflects the previous key; remount (key={persistKey}) to load the new key");
273
280
  loadedKey.current = config.key;
274
281
  }
275
282
  }, [config]);
@@ -352,7 +359,7 @@ const useContainerWidth = () => {
352
359
  observerRef.current?.disconnect();
353
360
  if (!element) return;
354
361
  const observer = new ResizeObserver((entries) => {
355
- const entry = entries[0];
362
+ const entry = entries.at(0);
356
363
  if (entry) setWidth(entry.contentRect.width);
357
364
  });
358
365
  observer.observe(element);
@@ -393,8 +400,18 @@ const useResponsiveModel = ({ breakpoints }) => {
393
400
  }, [active, containerRef]);
394
401
  };
395
402
 
403
+ //#endregion
404
+ //#region src/lib/warn-once.ts
405
+ const warned = /* @__PURE__ */ new Set();
406
+ const warnOnce = (key, message) => {
407
+ if (warned.has(key)) return;
408
+ warned.add(key);
409
+ console.warn(`[dashfoo] ${message}`);
410
+ };
411
+
396
412
  //#endregion
397
413
  //#region src/hooks/store.ts
414
+ const CONTROLLED_HISTORY_MESSAGE = "undo/redo are unavailable while `model` is controlled; keep the history in the state you own and drive it through onModelChange";
398
415
  const useDashfooStore = (options) => {
399
416
  const { defaultModel, model: controlledModel, onAction, onActiveTabsetChange, onMaximizedTabsetChange, onModelChange } = options;
400
417
  const initialModel = controlledModel ?? defaultModel;
@@ -421,32 +438,22 @@ const useDashfooStore = (options) => {
421
438
  const dispatch = useCallback((action) => {
422
439
  const resolved = onAction ? onAction(action) : action;
423
440
  if (!resolved) return;
424
- const transition = (() => {
425
- if (controlledModel === void 0) {
426
- const before = actorRef.getSnapshot().context.history.present;
427
- actorRef.send({
428
- action: resolved,
429
- type: "DISPATCH"
430
- });
431
- return {
432
- after: actorRef.getSnapshot().context.history.present,
433
- before
434
- };
435
- }
436
- return {
437
- after: reducer(controlledModel, resolved),
438
- before: normalize(controlledModel)
439
- };
440
- })();
441
- notify(transition.before, transition.after, resolved);
441
+ const before = actorRef.getSnapshot().context.history.present;
442
+ actorRef.send({
443
+ action: resolved,
444
+ type: "DISPATCH"
445
+ });
446
+ notify(before, actorRef.getSnapshot().context.history.present, resolved);
442
447
  }, [
443
448
  actorRef,
444
- controlledModel,
445
449
  notify,
446
450
  onAction
447
451
  ]);
448
452
  const undo = useCallback(() => {
449
- if (controlledModel !== void 0) return;
453
+ if (controlledModel !== void 0) {
454
+ warnOnce("controlled-undo", CONTROLLED_HISTORY_MESSAGE);
455
+ return;
456
+ }
450
457
  const before = actorRef.getSnapshot().context.history.present;
451
458
  actorRef.send({ type: "UNDO" });
452
459
  const after = actorRef.getSnapshot().context.history.present;
@@ -457,7 +464,10 @@ const useDashfooStore = (options) => {
457
464
  notify
458
465
  ]);
459
466
  const redo = useCallback(() => {
460
- if (controlledModel !== void 0) return;
467
+ if (controlledModel !== void 0) {
468
+ warnOnce("controlled-redo", CONTROLLED_HISTORY_MESSAGE);
469
+ return;
470
+ }
461
471
  const before = actorRef.getSnapshot().context.history.present;
462
472
  actorRef.send({ type: "REDO" });
463
473
  const after = actorRef.getSnapshot().context.history.present;
@@ -474,8 +484,8 @@ const useDashfooStore = (options) => {
474
484
  });
475
485
  }, [actorRef]);
476
486
  return useMemo(() => ({
477
- canRedo: () => canRedo(actorRef.getSnapshot().context.history),
478
- canUndo: () => canUndo(actorRef.getSnapshot().context.history),
487
+ canRedo: () => controlledModel === void 0 && canRedo(actorRef.getSnapshot().context.history),
488
+ canUndo: () => controlledModel === void 0 && canUndo(actorRef.getSnapshot().context.history),
479
489
  dispatch,
480
490
  model,
481
491
  redo,
@@ -483,6 +493,7 @@ const useDashfooStore = (options) => {
483
493
  undo
484
494
  }), [
485
495
  actorRef,
496
+ controlledModel,
486
497
  dispatch,
487
498
  model,
488
499
  redo,
@@ -491,6 +502,49 @@ const useDashfooStore = (options) => {
491
502
  ]);
492
503
  };
493
504
 
505
+ //#endregion
506
+ //#region src/lib/drag-subject.ts
507
+ const isTabFactory = (value) => typeof value === "function";
508
+ /**
509
+ * Reads what a dnd-kit source represents. It lives beside the manager wiring
510
+ * rather than in the layer adapter because `dragstart` fires once per gesture on
511
+ * the shared manager, above any one layer.
512
+ */
513
+ const subjectFor = (source) => {
514
+ const data = source.data;
515
+ if (data?.type === "tabset") return {
516
+ id: String(data.tabsetId),
517
+ kind: "tabset"
518
+ };
519
+ if (data?.type === "external") {
520
+ if (!isTabFactory(data.createTab)) {
521
+ console.warn("[dashfoo] external drag source is missing its createTab function");
522
+ return null;
523
+ }
524
+ let candidate;
525
+ try {
526
+ candidate = data.createTab();
527
+ } catch (error) {
528
+ console.warn("[dashfoo] external drag source createTab threw", error);
529
+ return null;
530
+ }
531
+ const parsed = tabNodeSchema.safeParse(candidate);
532
+ if (!parsed.success) {
533
+ console.warn("[dashfoo] external drag source returned an invalid tab", parsed.error);
534
+ return null;
535
+ }
536
+ return {
537
+ id: String(source.id),
538
+ kind: "external",
539
+ tab: parsed.data
540
+ };
541
+ }
542
+ return {
543
+ id: String(source.id),
544
+ kind: "tab"
545
+ };
546
+ };
547
+
494
548
  //#endregion
495
549
  //#region src/lib/tab-insertion.ts
496
550
  const pointInRect = (point, rect) => point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
@@ -591,7 +645,9 @@ const DragPreviewOverlay = ({ manager }) => {
591
645
  y: PREVIEW_OFFSET.y + (rect ? point.y - rect.top : 0)
592
646
  });
593
647
  });
594
- const offEnd = manager.monitor.addEventListener("dragend", () => setChip(null));
648
+ const offEnd = manager.monitor.addEventListener("dragend", () => {
649
+ setChip(null);
650
+ });
595
651
  return () => {
596
652
  offStart();
597
653
  offEnd();
@@ -621,12 +677,107 @@ const DragPreviewOverlay = ({ manager }) => {
621
677
  //#region src/components/drag-root.tsx
622
678
  const DashfooDragProvider = ({ children }) => {
623
679
  const [manager] = useState(createDragManager);
624
- useInsertionEffect(() => () => manager.destroy(), [manager]);
625
- const [subjectStore] = useState(createDragSubjectStore);
680
+ useInsertionEffect(() => () => {
681
+ manager.destroy();
682
+ }, [manager]);
683
+ const actorRef = useActorRef(dragDockMachine);
684
+ const [scopes] = useState(() => /* @__PURE__ */ new Map());
685
+ const registerScope = useCallback((scope) => {
686
+ scopes.set(scope.layerId, scope);
687
+ return () => {
688
+ scopes.delete(scope.layerId);
689
+ };
690
+ }, [scopes]);
691
+ /**
692
+ * One listener set and one actor for the whole tree. Attached per layer, a
693
+ * single pointer move fanned out to every float's actor, and each one that did
694
+ * not own the target resolved `intent: null` and then swallowed the drop.
695
+ */
696
+ useEffect(() => {
697
+ const scopedTarget = () => {
698
+ const target = manager.dragOperation.target;
699
+ const data = target?.data;
700
+ const scope = data?.layerId === void 0 ? void 0 : scopes.get(data.layerId);
701
+ const element = target?.element;
702
+ if (!scope || data?.tabsetId === void 0 || !(element instanceof HTMLElement)) return null;
703
+ return {
704
+ element,
705
+ scope,
706
+ tabsetId: data.tabsetId
707
+ };
708
+ };
709
+ let intentLayerId = null;
710
+ const syncIntent = () => {
711
+ const operation = manager.dragOperation;
712
+ const found = scopedTarget();
713
+ const draggedId = operation.source ? String(operation.source.id) : void 0;
714
+ const intent = found === null ? null : found.scope.resolveIntent(found.tabsetId, found.element, operation.position.current, draggedId);
715
+ intentLayerId = intent === null || found === null ? null : found.scope.layerId;
716
+ actorRef.send({
717
+ intent,
718
+ type: "OVER"
719
+ });
720
+ };
721
+ const handleStart = (event) => {
722
+ const source = event.operation.source;
723
+ if (!source) return;
724
+ const subject = subjectFor(source);
725
+ if (!subject) {
726
+ manager.actions.stop({ canceled: true });
727
+ return;
728
+ }
729
+ intentLayerId = null;
730
+ actorRef.send({
731
+ subject,
732
+ type: "START"
733
+ });
734
+ };
735
+ const handleMove = () => {
736
+ syncIntent();
737
+ };
738
+ const handleCollision = () => {
739
+ syncIntent();
740
+ };
741
+ const handleEnd = (event) => {
742
+ if (event.canceled) {
743
+ actorRef.send({ type: "CANCEL" });
744
+ return;
745
+ }
746
+ syncIntent();
747
+ actorRef.send({ type: "DROP" });
748
+ };
749
+ const commitSubscription = actorRef.on("COMMIT", (emitted) => {
750
+ (intentLayerId === null ? void 0 : scopes.get(intentLayerId))?.commit(emitted.action);
751
+ });
752
+ const offStart = manager.monitor.addEventListener("dragstart", handleStart);
753
+ const offMove = manager.monitor.addEventListener("dragmove", handleMove);
754
+ const offCollision = manager.monitor.addEventListener("collision", handleCollision);
755
+ const offEnd = manager.monitor.addEventListener("dragend", handleEnd);
756
+ return () => {
757
+ commitSubscription.unsubscribe();
758
+ offStart();
759
+ offMove();
760
+ offCollision();
761
+ offEnd();
762
+ };
763
+ }, [
764
+ actorRef,
765
+ manager,
766
+ scopes
767
+ ]);
768
+ const rootValue = useMemo(() => ({
769
+ actorRef,
770
+ manager,
771
+ registerScope
772
+ }), [
773
+ actorRef,
774
+ manager,
775
+ registerScope
776
+ ]);
626
777
  return /* @__PURE__ */ jsx(SharedDragManagerContext.Provider, {
627
778
  value: manager,
628
- children: /* @__PURE__ */ jsxs(DragSubjectStoreContext.Provider, {
629
- value: subjectStore,
779
+ children: /* @__PURE__ */ jsxs(DragRootContext.Provider, {
780
+ value: rootValue,
630
781
  children: [children, /* @__PURE__ */ jsx(DragPreviewOverlay, { manager })]
631
782
  })
632
783
  });
@@ -642,17 +793,9 @@ const useLayout = (selector) => {
642
793
  return useStore(store, selector);
643
794
  };
644
795
 
645
- //#endregion
646
- //#region src/lib/warn-once.ts
647
- const warned = /* @__PURE__ */ new Set();
648
- const warnOnce = (key, message) => {
649
- if (warned.has(key)) return;
650
- warned.add(key);
651
- console.warn(`[dashfoo] ${message}`);
652
- };
653
-
654
796
  //#endregion
655
797
  //#region src/components/drag-adapter.tsx
798
+ const MISSING_ROOT = "[dashfoo] DashfooDragProvider unmounted while its layout is still mounted; keep the provider above the layout or remount the layout";
656
799
  const tabRects = (strip, excludeId) => [...strip.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => tab.dataset.tabId === excludeId ? [] : [tab.getBoundingClientRect()]);
657
800
  const intentForTabset = (id, element, point, draggedId) => {
658
801
  const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
@@ -661,8 +804,7 @@ const intentForTabset = (id, element, point, draggedId) => {
661
804
  location: "center",
662
805
  targetId: id
663
806
  };
664
- const target = resolveDockTarget(point, element.getBoundingClientRect());
665
- const location = target.kind === "tab" ? "center" : `split-${target.edge}`;
807
+ const location = dockLocationFor(resolveDockTarget(point, element.getBoundingClientRect()));
666
808
  if (location === "center" && strip) return {
667
809
  index: tabRects(strip, draggedId).length,
668
810
  location,
@@ -673,266 +815,81 @@ const intentForTabset = (id, element, point, draggedId) => {
673
815
  targetId: id
674
816
  };
675
817
  };
676
- const isTabFactory = (value) => typeof value === "function";
677
- const subjectFor = (source) => {
678
- const data = source.data;
679
- if (data?.type === "tabset") return {
680
- id: String(data.tabsetId),
681
- kind: "tabset"
682
- };
683
- if (data?.type === "external") {
684
- if (!isTabFactory(data.createTab)) {
685
- console.warn("[dashfoo] external drag source is missing its createTab function");
686
- return null;
687
- }
688
- let candidate;
689
- try {
690
- candidate = data.createTab();
691
- } catch (error) {
692
- console.warn("[dashfoo] external drag source createTab threw", error);
693
- return null;
694
- }
695
- const parsed = tabNodeSchema.safeParse(candidate);
696
- if (!parsed.success) {
697
- console.warn("[dashfoo] external drag source returned an invalid tab", parsed.error);
698
- return null;
699
- }
700
- return {
701
- id: String(source.id),
702
- kind: "external",
703
- tab: parsed.data
704
- };
705
- }
706
- return {
707
- id: String(source.id),
708
- kind: "tab"
709
- };
710
- };
711
- const DragProvider = ({ children, onCommit, splitDock }) => {
712
- const actorRef = useActorRef(dragDockMachine);
818
+ /**
819
+ * One drag layer: the main tree, or one float. It holds no drag state of its
820
+ * own. It registers how its own tabsets turn a pointer into an intent and where
821
+ * a committed action goes, then draws the indicator over its own droppables.
822
+ */
823
+ const DragScope = ({ children, onCommit, splitDock }) => {
824
+ const root = useContext(DragRootContext);
713
825
  const layoutStore = useContext(LayoutStoreContext);
714
- const sharedManager = useContext(SharedDragManagerContext);
715
- const [ownManager] = useState(() => sharedManager ? null : createDragManager());
716
- useInsertionEffect(() => () => ownManager?.destroy(), [ownManager]);
717
- const manager = sharedManager ?? ownManager;
718
- if (manager === null) throw new Error("[dashfoo] DashfooDragProvider unmounted while its layout is still mounted; keep the provider above the layout or remount the layout");
719
- const inheritedStore = useContext(DragSubjectStoreContext);
720
- const [subjectStore] = useState(() => inheritedStore ?? createDragSubjectStore());
721
- const ownerId = useId();
722
- useEffect(() => {
723
- const subscription = actorRef.subscribe((snapshot) => {
724
- const { intent, subject } = snapshot.context;
725
- const state = subjectStore.getState();
726
- if (intent !== null) {
727
- if (!sameDropIntent(state.intent, intent) || state.intentOwner !== ownerId) subjectStore.setState({
728
- intent,
729
- intentOwner: ownerId
730
- });
731
- } else if (state.intent !== null && state.intentOwner === ownerId) subjectStore.setState({
732
- intent: null,
733
- intentOwner: null
734
- });
735
- if (!sameDragSubject(subjectStore.getState().subject, subject)) subjectStore.setState({ subject });
736
- });
737
- return () => {
738
- subscription.unsubscribe();
739
- };
740
- }, [
741
- actorRef,
742
- ownerId,
743
- subjectStore
744
- ]);
745
- const onCommitRef = useRef(onCommit);
746
- const splitDockRef = useRef(splitDock);
747
- useEffect(() => {
748
- onCommitRef.current = onCommit;
749
- splitDockRef.current = splitDock;
750
- });
826
+ const layerId = useId();
827
+ const manager = root?.manager;
751
828
  const getTabsetElement = useCallback((id) => {
752
- const element = manager.registry.droppables.get(`${ownerId}:${id}`)?.element;
829
+ const element = manager?.registry.droppables.get(`${layerId}:${id}`)?.element;
753
830
  return element instanceof HTMLElement ? element : void 0;
754
- }, [manager, ownerId]);
755
- useEffect(() => {
756
- const subscription = actorRef.on("COMMIT", (emitted) => {
831
+ }, [layerId, manager]);
832
+ const scope = useMemo(() => ({
833
+ commit: (action) => {
757
834
  if (layoutStore?.getState().editable === false) return;
758
- const commit = onCommitRef.current ?? layoutStore?.getState().dispatch;
835
+ const commit = onCommit ?? layoutStore?.getState().dispatch;
759
836
  if (!commit) {
760
837
  warnOnce("drag-layer-no-commit", "Layout.DragLayer has neither an onCommit prop nor a <Layout.Root> above it; drops are ignored");
761
838
  return;
762
839
  }
763
- commit(emitted.action);
764
- });
765
- return () => {
766
- subscription.unsubscribe();
767
- };
768
- }, [actorRef, layoutStore]);
769
- useEffect(() => {
770
- const resolveIntent = (targetId, element, point, draggedId) => {
840
+ commit(action);
841
+ },
842
+ layerId,
843
+ resolveIntent: (targetId, element, point, draggedId) => {
771
844
  if (layoutStore?.getState().editable === false) return null;
772
845
  if (!shouldAllowDrop(draggedId, targetId, [...element.querySelectorAll("[data-dashfoo=\"tab\"]")].map((tab) => tab.dataset.tabId ?? ""))) return null;
773
846
  const intent = intentForTabset(targetId, element, point, draggedId);
774
- if (!(splitDockRef.current ?? layoutStore?.getState().splitDock ?? true) && intent.location.startsWith("split-")) return {
847
+ if (!(splitDock ?? layoutStore?.getState().splitDock ?? true) && splitEdge(intent.location) !== void 0) return {
775
848
  location: "center",
776
849
  targetId
777
850
  };
778
851
  return intent;
779
- };
780
- const handleStart = (event) => {
781
- const source = event.operation.source;
782
- if (!source) return;
783
- const subject = subjectFor(source);
784
- if (!subject) {
785
- manager.actions.stop({ canceled: true });
786
- return;
787
- }
788
- actorRef.send({
789
- subject,
790
- type: "START"
791
- });
792
- };
793
- const syncIntent = () => {
794
- const operation = manager.dragOperation;
795
- const point = operation.position.current;
796
- const target = operation.target;
797
- const data = target?.data;
798
- const tabsetId = data?.layerId === ownerId ? data.tabsetId : void 0;
799
- const element = tabsetId === void 0 ? void 0 : target?.element;
800
- const draggedId = operation.source ? String(operation.source.id) : void 0;
801
- actorRef.send({
802
- intent: tabsetId !== void 0 && element instanceof HTMLElement ? resolveIntent(tabsetId, element, point, draggedId) : null,
803
- type: "OVER"
804
- });
805
- };
806
- const handleMove = () => syncIntent();
807
- const handleCollision = () => syncIntent();
808
- const handleEnd = (event) => {
809
- if (event.canceled) {
810
- actorRef.send({ type: "CANCEL" });
811
- return;
812
- }
813
- syncIntent();
814
- actorRef.send({ type: "DROP" });
815
- };
816
- const offStart = manager.monitor.addEventListener("dragstart", handleStart);
817
- const offMove = manager.monitor.addEventListener("dragmove", handleMove);
818
- const offCollision = manager.monitor.addEventListener("collision", handleCollision);
819
- const offEnd = manager.monitor.addEventListener("dragend", handleEnd);
820
- return () => {
821
- offStart();
822
- offMove();
823
- offCollision();
824
- offEnd();
825
- };
826
- }, [
827
- actorRef,
852
+ }
853
+ }), [
854
+ layerId,
828
855
  layoutStore,
829
- manager,
830
- ownerId
856
+ onCommit,
857
+ splitDock
831
858
  ]);
832
- const contextValue = useMemo(() => ({
833
- layerId: ownerId,
834
- manager
835
- }), [manager, ownerId]);
836
- return /* @__PURE__ */ jsx(DragContext.Provider, {
859
+ const registerScope = root?.registerScope;
860
+ useEffect(() => registerScope?.(scope), [registerScope, scope]);
861
+ const contextValue = useMemo(() => root === null ? null : {
862
+ actorRef: root.actorRef,
863
+ layerId,
864
+ manager: root.manager
865
+ }, [layerId, root]);
866
+ if (contextValue === null) throw new Error(MISSING_ROOT);
867
+ return /* @__PURE__ */ jsxs(DragContext.Provider, {
837
868
  value: contextValue,
838
- children: /* @__PURE__ */ jsxs(DragSubjectStoreContext.Provider, {
839
- value: subjectStore,
840
- children: [
841
- children,
842
- /* @__PURE__ */ jsx(DockIndicator, {
843
- actorRef,
844
- getTabsetElement
845
- }),
846
- ownManager === null ? null : /* @__PURE__ */ jsx(DragPreviewOverlay, { manager: ownManager })
847
- ]
848
- })
869
+ children: [children, /* @__PURE__ */ jsx(DockIndicator, {
870
+ actorRef: contextValue.actorRef,
871
+ getTabsetElement
872
+ })]
849
873
  });
850
874
  };
875
+ /**
876
+ * Mounts its own root when there is none above it, so a hand-composed
877
+ * `Layout.DragLayer` still works standalone. The choice is made on the first
878
+ * render, as it was for the manager this used to create itself: a root that
879
+ * disappears later means a torn-down tree, which `DragScope` reports.
880
+ */
881
+ const DragProvider = (props) => {
882
+ const hasRoot = useContext(DragRootContext) !== null;
883
+ const [ownsRoot] = useState(() => !hasRoot);
884
+ const layer = /* @__PURE__ */ jsx(DragScope, { ...props });
885
+ return ownsRoot ? /* @__PURE__ */ jsx(DashfooDragProvider, { children: layer }) : layer;
886
+ };
851
887
 
852
888
  //#endregion
853
889
  //#region src/components/float-context.ts
854
890
  const FloatLayerContext = createContext(false);
855
891
  const useHasFloatLayer = () => useContext(FloatLayerContext);
856
892
 
857
- //#endregion
858
- //#region src/hooks/use-inline-rename.ts
859
- const useInlineRename = ({ currentName, inputRef, onCommit, onDone }) => {
860
- const done = useRef(false);
861
- useEffect(() => {
862
- inputRef.current?.focus();
863
- inputRef.current?.select();
864
- }, [inputRef]);
865
- const commit = (value) => {
866
- const trimmed = value.trim();
867
- if (trimmed && trimmed !== currentName) onCommit(trimmed);
868
- onDone();
869
- };
870
- const handleKeyDown = (event) => {
871
- if (event.key === "Enter") {
872
- done.current = true;
873
- commit(event.currentTarget.value);
874
- } else if (event.key === "Escape") {
875
- done.current = true;
876
- onDone();
877
- }
878
- };
879
- const handleBlur = (event) => {
880
- if (done.current) return;
881
- done.current = true;
882
- commit(event.currentTarget.value);
883
- };
884
- return {
885
- handleBlur,
886
- handleKeyDown
887
- };
888
- };
889
-
890
- //#endregion
891
- //#region src/lib/float-geometry.ts
892
- const MIN_WIDTH = 200;
893
- const MIN_HEIGHT = 120;
894
- const DEFAULT_WIDTH = 480;
895
- const DEFAULT_HEIGHT = 340;
896
- const LIFT_OFFSET = 16;
897
- const clampToBounds = (rect, bounds, size = rect) => ({
898
- height: rect.height,
899
- left: Math.max(0, Math.min(rect.left, Math.max(0, bounds.width - size.width))),
900
- top: Math.max(0, Math.min(rect.top, Math.max(0, bounds.height - size.height))),
901
- width: rect.width
902
- });
903
- const measureFloatRect = (element) => {
904
- const rect = (element.closest("[data-dashfoo=\"tabset\"]") ?? element).getBoundingClientRect();
905
- const seeded = {
906
- height: Math.max(MIN_HEIGHT, Math.min(Math.round(rect.height), DEFAULT_HEIGHT)),
907
- left: Math.round(rect.left) + LIFT_OFFSET,
908
- top: Math.round(rect.top) + LIFT_OFFSET,
909
- width: Math.max(MIN_WIDTH, Math.min(Math.round(rect.width), DEFAULT_WIDTH))
910
- };
911
- return clampToBounds(seeded, {
912
- height: window.innerHeight,
913
- width: window.innerWidth
914
- });
915
- };
916
- const resizeRect = (start, edges, dx, dy) => {
917
- let { height, left, top, width } = start;
918
- if (edges.x === 1) width = Math.max(MIN_WIDTH, start.width + dx);
919
- else if (edges.x === -1) {
920
- width = Math.max(MIN_WIDTH, start.width - dx);
921
- left = Math.max(0, start.left + (start.width - width));
922
- }
923
- if (edges.y === 1) height = Math.max(MIN_HEIGHT, start.height + dy);
924
- else if (edges.y === -1) {
925
- height = Math.max(MIN_HEIGHT, start.height - dy);
926
- top = Math.max(0, start.top + (start.height - height));
927
- }
928
- return {
929
- height,
930
- left,
931
- top,
932
- width
933
- };
934
- };
935
-
936
893
  //#endregion
937
894
  //#region src/components/float-resize-handles.ts
938
895
  const EDGE_THICKNESS = 12;
@@ -1052,6 +1009,202 @@ const RESIZE_HANDLES = [
1052
1009
  ];
1053
1010
  const EDGE_BY_KEY = new Map(RESIZE_HANDLES.map((h) => [h.key, h.edges]));
1054
1011
 
1012
+ //#endregion
1013
+ //#region src/lib/float-geometry.ts
1014
+ const MIN_WIDTH = 200;
1015
+ const MIN_HEIGHT = 120;
1016
+ const DEFAULT_WIDTH = 480;
1017
+ const DEFAULT_HEIGHT = 340;
1018
+ const LIFT_OFFSET = 16;
1019
+ const clampToBounds = (rect, bounds, size = rect) => ({
1020
+ height: rect.height,
1021
+ left: Math.max(0, Math.min(rect.left, Math.max(0, bounds.width - size.width))),
1022
+ top: Math.max(0, Math.min(rect.top, Math.max(0, bounds.height - size.height))),
1023
+ width: rect.width
1024
+ });
1025
+ const measureFloatRect = (element) => {
1026
+ const rect = (element.closest("[data-dashfoo=\"tabset\"]") ?? element).getBoundingClientRect();
1027
+ const seeded = {
1028
+ height: Math.max(MIN_HEIGHT, Math.min(Math.round(rect.height), DEFAULT_HEIGHT)),
1029
+ left: Math.round(rect.left) + LIFT_OFFSET,
1030
+ top: Math.round(rect.top) + LIFT_OFFSET,
1031
+ width: Math.max(MIN_WIDTH, Math.min(Math.round(rect.width), DEFAULT_WIDTH))
1032
+ };
1033
+ return clampToBounds(seeded, {
1034
+ height: window.innerHeight,
1035
+ width: window.innerWidth
1036
+ });
1037
+ };
1038
+ const resizeRect = (start, edges, dx, dy) => {
1039
+ let { height, left, top, width } = start;
1040
+ if (edges.x === 1) width = Math.max(MIN_WIDTH, start.width + dx);
1041
+ else if (edges.x === -1) {
1042
+ width = Math.max(MIN_WIDTH, start.width - dx);
1043
+ left = Math.max(0, start.left + (start.width - width));
1044
+ }
1045
+ if (edges.y === 1) height = Math.max(MIN_HEIGHT, start.height + dy);
1046
+ else if (edges.y === -1) {
1047
+ height = Math.max(MIN_HEIGHT, start.height - dy);
1048
+ top = Math.max(0, start.top + (start.height - height));
1049
+ }
1050
+ return {
1051
+ height,
1052
+ left,
1053
+ top,
1054
+ width
1055
+ };
1056
+ };
1057
+
1058
+ //#endregion
1059
+ //#region src/hooks/use-float-gesture.ts
1060
+ const CHIP_SIZE = {
1061
+ height: 34,
1062
+ width: 168
1063
+ };
1064
+ const TAP_SLOP = 4;
1065
+ /**
1066
+ * Moving and resizing writes straight to the panel's inline style and only
1067
+ * dispatches on pointer-up: routing every pointermove through the model would
1068
+ * re-render the whole layout tree behind the float.
1069
+ */
1070
+ const useFloatGesture = ({ editable, geometry, minimized, onCommit, onTap }) => {
1071
+ const panelRef = useRef(null);
1072
+ const gestureRef = useRef(null);
1073
+ const latestRef = useRef(geometry);
1074
+ const movedRef = useRef(false);
1075
+ const setPanel = (element) => {
1076
+ panelRef.current = element;
1077
+ };
1078
+ const handlePointerDown = (event) => {
1079
+ const panel = panelRef.current;
1080
+ if (!panel || !editable) return;
1081
+ const prior = gestureRef.current;
1082
+ if (prior && panel.hasPointerCapture?.(prior.pointerId)) panel.releasePointerCapture(prior.pointerId);
1083
+ movedRef.current = false;
1084
+ const edgeKey = event.currentTarget.dataset.edge;
1085
+ const parent = panel.offsetParent instanceof HTMLElement ? panel.offsetParent : null;
1086
+ const gesture = {
1087
+ bounds: {
1088
+ height: parent?.clientHeight ?? 0,
1089
+ width: parent?.clientWidth ?? 0
1090
+ },
1091
+ edges: edgeKey === void 0 ? null : EDGE_BY_KEY.get(edgeKey) ?? null,
1092
+ pointerId: event.pointerId,
1093
+ start: geometry,
1094
+ startX: event.clientX,
1095
+ startY: event.clientY
1096
+ };
1097
+ gestureRef.current = gesture;
1098
+ latestRef.current = geometry;
1099
+ if (gesture.edges) panel.setPointerCapture(event.pointerId);
1100
+ };
1101
+ const handlePointerUp = (event) => {
1102
+ const gesture = gestureRef.current;
1103
+ if (!gesture || event.pointerId !== gesture.pointerId) return;
1104
+ gestureRef.current = null;
1105
+ const panel = panelRef.current;
1106
+ if (panel?.hasPointerCapture?.(event.pointerId) === true) panel.releasePointerCapture(event.pointerId);
1107
+ if (!movedRef.current) {
1108
+ onTap();
1109
+ return;
1110
+ }
1111
+ onCommit(latestRef.current);
1112
+ };
1113
+ const handlePointerMove = (event) => {
1114
+ const gesture = gestureRef.current;
1115
+ const panel = panelRef.current;
1116
+ if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
1117
+ if (event.buttons === 0) {
1118
+ handlePointerUp(event);
1119
+ return;
1120
+ }
1121
+ const dx = event.clientX - gesture.startX;
1122
+ const dy = event.clientY - gesture.startY;
1123
+ if (!movedRef.current) {
1124
+ if (Math.hypot(dx, dy) <= TAP_SLOP) return;
1125
+ movedRef.current = true;
1126
+ if (!gesture.edges) panel.setPointerCapture(gesture.pointerId);
1127
+ }
1128
+ const next = gesture.edges ? resizeRect(gesture.start, gesture.edges, dx, dy) : clampToBounds({
1129
+ ...gesture.start,
1130
+ left: gesture.start.left + dx,
1131
+ top: gesture.start.top + dy
1132
+ }, gesture.bounds, minimized ? CHIP_SIZE : void 0);
1133
+ latestRef.current = next;
1134
+ panel.style.left = `${next.left}px`;
1135
+ panel.style.top = `${next.top}px`;
1136
+ if (!minimized) {
1137
+ panel.style.width = `${next.width}px`;
1138
+ panel.style.height = `${next.height}px`;
1139
+ }
1140
+ };
1141
+ const handlePointerCancel = (event) => {
1142
+ const gesture = gestureRef.current;
1143
+ const panel = panelRef.current;
1144
+ if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
1145
+ gestureRef.current = null;
1146
+ if (panel.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
1147
+ if (!movedRef.current) return;
1148
+ panel.style.left = `${gesture.start.left}px`;
1149
+ panel.style.top = `${gesture.start.top}px`;
1150
+ if (!minimized) {
1151
+ panel.style.width = `${gesture.start.width}px`;
1152
+ panel.style.height = `${gesture.start.height}px`;
1153
+ }
1154
+ };
1155
+ return {
1156
+ handlers: {
1157
+ onPointerCancel: handlePointerCancel,
1158
+ onPointerDown: handlePointerDown,
1159
+ onPointerMove: handlePointerMove,
1160
+ onPointerUp: handlePointerUp
1161
+ },
1162
+ ref: setPanel,
1163
+ style: minimized ? {
1164
+ left: geometry.left,
1165
+ top: geometry.top
1166
+ } : {
1167
+ height: geometry.height,
1168
+ left: geometry.left,
1169
+ top: geometry.top,
1170
+ width: geometry.width
1171
+ }
1172
+ };
1173
+ };
1174
+
1175
+ //#endregion
1176
+ //#region src/hooks/use-inline-rename.ts
1177
+ const useInlineRename = ({ currentName, inputRef, onCommit, onDone }) => {
1178
+ const done = useRef(false);
1179
+ useEffect(() => {
1180
+ inputRef.current?.focus();
1181
+ inputRef.current?.select();
1182
+ }, [inputRef]);
1183
+ const commit = (value) => {
1184
+ const trimmed = value.trim();
1185
+ if (trimmed && trimmed !== currentName) onCommit(trimmed);
1186
+ onDone();
1187
+ };
1188
+ const handleKeyDown = (event) => {
1189
+ if (event.key === "Enter") {
1190
+ done.current = true;
1191
+ commit(event.currentTarget.value);
1192
+ } else if (event.key === "Escape") {
1193
+ done.current = true;
1194
+ onDone();
1195
+ }
1196
+ };
1197
+ const handleBlur = (event) => {
1198
+ if (done.current) return;
1199
+ done.current = true;
1200
+ commit(event.currentTarget.value);
1201
+ };
1202
+ return {
1203
+ handleBlur,
1204
+ handleKeyDown
1205
+ };
1206
+ };
1207
+
1055
1208
  //#endregion
1056
1209
  //#region src/components/layout-root.tsx
1057
1210
  const DEFAULT_TABSET_MIN_SIZE = 320;
@@ -1061,15 +1214,15 @@ const rootStyle = {
1061
1214
  position: "relative",
1062
1215
  width: "100%"
1063
1216
  };
1064
- const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = true, draggableTabsets = true, editable = true, floatable = false, keepMounted = false, maximizable = true, model, renamableTabs = true, renderTab, renderTabLabel, renderTabsetToolbar, resizableSplits = true, rootRef, snap, style, ...props }) => {
1217
+ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = true, draggableTabsets = true, editable = true, floatable = false, keepMounted = false, maximizable = true, model, renamableTabs = true, renderTab, renderTabLabel, renderTabsetToolbar, resizableSplits = true, restructurable = true, rootRef, snap, style, ...props }) => {
1065
1218
  const global = model.global;
1066
1219
  const snapshot = {
1067
1220
  closableTabs: editable && closableTabs && global.tabEnableClose !== false,
1068
1221
  dispatch,
1069
- draggableTabs: editable && draggableTabs && global.tabEnableDrag !== false,
1070
- draggableTabsets: editable && draggableTabsets,
1222
+ draggableTabs: editable && restructurable && draggableTabs && global.tabEnableDrag !== false,
1223
+ draggableTabsets: editable && restructurable && draggableTabsets,
1071
1224
  editable,
1072
- floatable: editable && floatable,
1225
+ floatable: editable && restructurable && floatable,
1073
1226
  keepMounted,
1074
1227
  maximizable: maximizable && global.tabSetEnableMaximize !== false,
1075
1228
  maximizedTabsetId: model.maximizedTabsetId,
@@ -1077,7 +1230,7 @@ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = t
1077
1230
  renderTab,
1078
1231
  renderTabLabel,
1079
1232
  renderTabsetToolbar,
1080
- resizableSplits: editable && resizableSplits && global.enableSplitResize !== false,
1233
+ resizableSplits: editable && restructurable && resizableSplits && global.enableSplitResize !== false,
1081
1234
  snap: snap ?? global.snap ?? null,
1082
1235
  splitDock: global.enableSplitDock !== false,
1083
1236
  tabLocation: global.tabLocation ?? "top",
@@ -1099,15 +1252,142 @@ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = t
1099
1252
  return /* @__PURE__ */ jsx(LayoutStoreContext.Provider, {
1100
1253
  value: store,
1101
1254
  children: /* @__PURE__ */ jsx("div", {
1102
- ...props,
1255
+ ...props,
1256
+ "data-dashfoo": "layout",
1257
+ ref: rootRef,
1258
+ style: layoutStyle,
1259
+ children
1260
+ })
1261
+ });
1262
+ };
1263
+ /**
1264
+ * A nested subtree (a float) renders through the same components as the main
1265
+ * layout, minus a few capabilities. It republishes the parent store with the
1266
+ * overrides applied instead of re-deriving every flag from a synthetic model,
1267
+ * so anything the parent changes keeps flowing into the subtree.
1268
+ */
1269
+ const LayoutOverrides = ({ children, overrides }) => {
1270
+ const snapshot = {
1271
+ ...useLayout((state) => state),
1272
+ ...overrides
1273
+ };
1274
+ const [store] = useState(() => createLayoutStore(snapshot));
1275
+ useLayoutEffect(() => {
1276
+ store.setState(snapshot);
1277
+ });
1278
+ return /* @__PURE__ */ jsx(LayoutStoreContext.Provider, {
1279
+ value: store,
1280
+ children: /* @__PURE__ */ jsx("div", {
1103
1281
  "data-dashfoo": "layout",
1104
- ref: rootRef,
1105
- style: layoutStyle,
1282
+ style: rootStyle,
1106
1283
  children
1107
1284
  })
1108
1285
  });
1109
1286
  };
1110
1287
 
1288
+ //#endregion
1289
+ //#region src/hooks/use-snap-resize.ts
1290
+ const WEIGHT_EPSILON = .01;
1291
+ const IDLE = { kind: "idle" };
1292
+ const boundaryOf = (state) => state.kind === "dragging" || state.kind === "snapped" ? state.boundary : null;
1293
+ const snappedBoundaryOf = (state) => state.kind === "snapped" ? state.boundary : null;
1294
+ const layoutFromWeights = (children) => {
1295
+ const total = children.reduce((sum, child) => sum + child.weight, 0);
1296
+ return children.reduce((layout, child) => {
1297
+ layout[child.id] = child.weight / total * 100;
1298
+ return layout;
1299
+ }, {});
1300
+ };
1301
+ const orderedFromLayout = (layout, children) => children.map((child) => layout[child.id] ?? 0);
1302
+ const layoutFromOrdered = (sizes, children) => children.reduce((layout, child, index) => {
1303
+ layout[child.id] = sizes[index] ?? 0;
1304
+ return layout;
1305
+ }, {});
1306
+ const layoutsMatch = (current, expected) => {
1307
+ const currentIds = Object.keys(current);
1308
+ const expectedIds = Object.keys(expected);
1309
+ if (currentIds.length !== expectedIds.length) return false;
1310
+ return expectedIds.every((id) => Math.abs((current[id] ?? NaN) - expected[id]) <= WEIGHT_EPSILON);
1311
+ };
1312
+ const useSnapResize = ({ children, onWeightsSettled, resizableSplits, snap }) => {
1313
+ const groupRef = useRef(null);
1314
+ const state = useRef(IDLE);
1315
+ const [snappedBoundary, setSnappedBoundary] = useState(null);
1316
+ const transition = useCallback((next) => {
1317
+ state.current = next;
1318
+ setSnappedBoundary(snappedBoundaryOf(next));
1319
+ }, []);
1320
+ const desiredLayout = useMemo(() => layoutFromWeights(children), [children]);
1321
+ useLayoutEffect(() => {
1322
+ const group = groupRef.current;
1323
+ if (!group) return;
1324
+ const current = group.getLayout();
1325
+ if (!(Object.keys(current).length === Object.keys(desiredLayout).length && Object.keys(desiredLayout).every((id) => id in current)) || layoutsMatch(current, desiredLayout)) return;
1326
+ transition({ kind: "syncing" });
1327
+ group.setLayout(desiredLayout);
1328
+ const frame = requestAnimationFrame(() => {
1329
+ transition(IDLE);
1330
+ });
1331
+ return () => {
1332
+ cancelAnimationFrame(frame);
1333
+ transition(IDLE);
1334
+ };
1335
+ }, [desiredLayout, transition]);
1336
+ const onLayoutChange = (layout) => {
1337
+ if (state.current.kind === "syncing" || !resizableSplits) return;
1338
+ const boundary = boundaryOf(state.current);
1339
+ if (boundary === null) return;
1340
+ const decision = decideSnap(orderedFromLayout(layout, children), boundary, snap);
1341
+ if (decision.kind === "inactive") return;
1342
+ if (decision.kind === "clear") {
1343
+ transition({
1344
+ boundary,
1345
+ kind: "dragging"
1346
+ });
1347
+ return;
1348
+ }
1349
+ transition({
1350
+ boundary,
1351
+ kind: "snapped"
1352
+ });
1353
+ const group = groupRef.current;
1354
+ const target = layoutFromOrdered(decision.sizes, children);
1355
+ if (!group || layoutsMatch(layout, target)) return;
1356
+ transition({ kind: "syncing" });
1357
+ group.setLayout(target);
1358
+ transition({
1359
+ boundary,
1360
+ kind: "snapped"
1361
+ });
1362
+ };
1363
+ const onLayoutChanged = (layout, meta) => {
1364
+ if (!meta.isUserInteraction) return;
1365
+ if (state.current.kind === "syncing") return;
1366
+ const boundary = boundaryOf(state.current);
1367
+ transition(IDLE);
1368
+ if (!resizableSplits) return;
1369
+ const weights = settleSnap(children.map((child) => layout[child.id] ?? child.weight), boundary, snap);
1370
+ if (!weights.some((weight, index) => {
1371
+ const child = children[index];
1372
+ return child !== void 0 && Math.abs(weight - child.weight) > WEIGHT_EPSILON;
1373
+ })) return;
1374
+ onWeightsSettled(weights);
1375
+ };
1376
+ const beginBoundaryDrag = (boundary) => {
1377
+ transition({
1378
+ boundary,
1379
+ kind: "dragging"
1380
+ });
1381
+ };
1382
+ return {
1383
+ beginBoundaryDrag,
1384
+ groupRef,
1385
+ onLayoutChange,
1386
+ onLayoutChanged,
1387
+ snappedBoundary
1388
+ };
1389
+ };
1390
+
1111
1391
  //#endregion
1112
1392
  //#region src/lib/tab-ids.ts
1113
1393
  const tabDomId = (tabsetId, tabId) => `dashfoo-tab-${tabsetId}-${tabId}`;
@@ -1118,7 +1398,9 @@ const panelDomId = (tabsetId) => `dashfoo-panel-${tabsetId}`;
1118
1398
  const tabButtonIn = (scope, tabsetId, tabId) => scope?.querySelector(`#${CSS.escape(tabDomId(tabsetId, tabId))}`) ?? null;
1119
1399
  const createTabsetStore = (initial) => createStore((set, get) => ({
1120
1400
  ...initial,
1121
- cancelRename: () => set({ editingTabId: null }),
1401
+ cancelRename: () => {
1402
+ set({ editingTabId: null });
1403
+ },
1122
1404
  closeTab: (tabId) => {
1123
1405
  set({ restoreFocus: true });
1124
1406
  get().dispatch({
@@ -1137,7 +1419,9 @@ const createTabsetStore = (initial) => createStore((set, get) => ({
1137
1419
  overflowItems: [],
1138
1420
  registerRenameInput: () => {
1139
1421
  set((state) => ({ renameInputCount: state.renameInputCount + 1 }));
1140
- return () => set((state) => ({ renameInputCount: state.renameInputCount - 1 }));
1422
+ return () => {
1423
+ set((state) => ({ renameInputCount: state.renameInputCount - 1 }));
1424
+ };
1141
1425
  },
1142
1426
  registerTablist: (element) => {
1143
1427
  if (element) {
@@ -1149,11 +1433,13 @@ const createTabsetStore = (initial) => createStore((set, get) => ({
1149
1433
  set({ tablistElement: null });
1150
1434
  },
1151
1435
  renameInputCount: 0,
1152
- renameTab: (tabId, name) => get().dispatch({
1153
- name,
1154
- tabId,
1155
- type: "renameTab"
1156
- }),
1436
+ renameTab: (tabId, name) => {
1437
+ get().dispatch({
1438
+ name,
1439
+ tabId,
1440
+ type: "renameTab"
1441
+ });
1442
+ },
1157
1443
  restoreFocus: false,
1158
1444
  selectOverflowTab: (tabId) => {
1159
1445
  const { dispatch, node, tablistElement } = get();
@@ -1176,7 +1462,7 @@ const createTabsetStore = (initial) => createStore((set, get) => ({
1176
1462
  tabsetId: node.id,
1177
1463
  type: "selectTab"
1178
1464
  });
1179
- if (options?.focus) {
1465
+ if (options?.focus === true) {
1180
1466
  const tabId = node.children[index]?.id;
1181
1467
  if (tabId) tabButtonIn(tablistElement, node.id, tabId)?.focus();
1182
1468
  }
@@ -1308,7 +1594,7 @@ const TabOverflowMenu = ({ items, onSelect }) => {
1308
1594
  useEffect(() => {
1309
1595
  if (!open) return;
1310
1596
  const handlePointerDown = (event) => {
1311
- if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setOpen(false);
1597
+ if (event.target instanceof Node && rootRef.current?.contains(event.target) !== true) setOpen(false);
1312
1598
  };
1313
1599
  document.addEventListener("pointerdown", handlePointerDown);
1314
1600
  return () => {
@@ -1361,7 +1647,7 @@ const TabOverflowMenu = ({ items, onSelect }) => {
1361
1647
  }
1362
1648
  };
1363
1649
  const handleFocusOut = (event) => {
1364
- if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return;
1650
+ if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget) === true) return;
1365
1651
  setOpen(false);
1366
1652
  };
1367
1653
  return /* @__PURE__ */ jsxs("div", {
@@ -1387,7 +1673,9 @@ const TabOverflowMenu = ({ items, onSelect }) => {
1387
1673
  tabIndex: -1,
1388
1674
  children: items.map((item, index) => /* @__PURE__ */ jsx("button", {
1389
1675
  "data-dashfoo": "tab-overflow-item",
1390
- onClick: () => handleSelect(item.id),
1676
+ onClick: () => {
1677
+ handleSelect(item.id);
1678
+ },
1391
1679
  ref: (node) => {
1392
1680
  itemRefs.current[index] = node;
1393
1681
  },
@@ -1418,7 +1706,9 @@ const overflowingIds = (tablist) => {
1418
1706
  const box = tablist.getBoundingClientRect();
1419
1707
  return [...tablist.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => {
1420
1708
  const rect = tab.getBoundingClientRect();
1421
- return (rect.right > box.right + 1 || rect.left < box.left - 1) && tab.dataset.tabId ? [tab.dataset.tabId] : [];
1709
+ const clipped = rect.right > box.right + 1 || rect.left < box.left - 1;
1710
+ const tabId = tab.dataset.tabId;
1711
+ return clipped && tabId !== void 0 && tabId !== "" ? [tabId] : [];
1422
1712
  });
1423
1713
  };
1424
1714
  const useTabOverflow = (tablist, signature) => {
@@ -1723,7 +2013,9 @@ const RenameEditor = ({ defaultValue, onBlur, onKeyDown, ref: userRef, ...props
1723
2013
  const rename = useInlineRename({
1724
2014
  currentName: tab.name,
1725
2015
  inputRef,
1726
- onCommit: (name) => commitRename(tab.id, name),
2016
+ onCommit: (name) => {
2017
+ commitRename(tab.id, name);
2018
+ },
1727
2019
  onDone: cancelRename
1728
2020
  });
1729
2021
  const refCallback = useMemo(() => mergeRefs(inputRef, userRef), [userRef]);
@@ -1963,22 +2255,6 @@ const groupStyle = {
1963
2255
  minHeight: 0,
1964
2256
  minWidth: 0
1965
2257
  };
1966
- const WEIGHT_EPSILON = .01;
1967
- const layoutFromWeights = (children, total) => children.reduce((layout, child) => {
1968
- layout[child.id] = (child.weight ?? 1) / total * 100;
1969
- return layout;
1970
- }, {});
1971
- const orderedFromLayout = (layout, children) => children.map((child) => layout[child.id] ?? 0);
1972
- const layoutFromOrdered = (sizes, children) => children.reduce((layout, child, index) => {
1973
- layout[child.id] = sizes[index] ?? 0;
1974
- return layout;
1975
- }, {});
1976
- const layoutsMatch = (current, expected) => {
1977
- const currentIds = Object.keys(current);
1978
- const expectedIds = Object.keys(expected);
1979
- if (currentIds.length !== expectedIds.length) return false;
1980
- return expectedIds.every((id) => Math.abs((current[id] ?? NaN) - expected[id]) <= WEIGHT_EPSILON);
1981
- };
1982
2258
  const descendantMinSize = (child, axis, tabsetMinSize) => {
1983
2259
  const explicit = dimensionToPixels(child.min);
1984
2260
  if (explicit !== void 0) return explicit;
@@ -2001,89 +2277,30 @@ const RowView = ({ node, renderTabset }) => {
2001
2277
  const effectiveSnap = node.snap ?? globalSnap;
2002
2278
  const snapActive = snapEnabled(effectiveSnap);
2003
2279
  const orientation = node.orientation === "row" ? "horizontal" : "vertical";
2004
- const total = node.children.reduce((sum, child) => sum + (child.weight ?? 1), 0);
2005
- const desiredLayout = useMemo(() => layoutFromWeights(node.children, total), [node.children, total]);
2006
- const groupRef = useRef(null);
2007
- const groupElement = useRef(null);
2008
- const syncing = useRef(false);
2009
- const activeBoundary = useRef(null);
2010
- const snapping = useRef(false);
2011
- const snappedBoundaryRef = useRef(null);
2012
- const [snappedBoundary, setSnappedBoundary] = useState(null);
2013
- const setHighlight = (boundary) => {
2014
- if (boundary !== snappedBoundaryRef.current) {
2015
- snappedBoundaryRef.current = boundary;
2016
- setSnappedBoundary(boundary);
2017
- }
2018
- };
2019
- const setSnapTransition = (active) => {
2020
- const element = groupElement.current;
2021
- if (!element) return;
2022
- if (active) element.dataset.dashfooSnapping = "true";
2023
- else delete element.dataset.dashfooSnapping;
2024
- };
2025
- useLayoutEffect(() => {
2026
- const group = groupRef.current;
2027
- if (!group) return;
2028
- const current = group.getLayout();
2029
- if (!(Object.keys(current).length === Object.keys(desiredLayout).length && Object.keys(desiredLayout).every((id) => id in current)) || layoutsMatch(current, desiredLayout)) return;
2030
- syncing.current = true;
2031
- group.setLayout(desiredLayout);
2032
- const frame = requestAnimationFrame(() => {
2033
- syncing.current = false;
2034
- });
2035
- return () => {
2036
- cancelAnimationFrame(frame);
2037
- syncing.current = false;
2038
- };
2039
- }, [desiredLayout]);
2040
- const handleLayoutChange = (layout) => {
2041
- if (snapping.current || syncing.current || !resizableSplits) return;
2042
- if (effectiveSnap === null || !snapActive) return;
2043
- const boundary = activeBoundary.current;
2044
- if (boundary === null) return;
2045
- const { sizes, snapped } = snapSizes(orderedFromLayout(layout, node.children), boundary, effectiveSnap);
2046
- setHighlight(snapped ? boundary : null);
2047
- if (!snapped) {
2048
- setSnapTransition(false);
2049
- return;
2050
- }
2051
- setSnapTransition(true);
2052
- const group = groupRef.current;
2053
- const target = layoutFromOrdered(sizes, node.children);
2054
- if (!group || layoutsMatch(layout, target)) return;
2055
- snapping.current = true;
2056
- group.setLayout(target);
2057
- snapping.current = false;
2058
- };
2059
- const handleLayoutChanged = (layout, meta) => {
2060
- if (!meta.isUserInteraction) return;
2061
- if (syncing.current) return;
2062
- const boundary = activeBoundary.current;
2063
- activeBoundary.current = null;
2064
- setHighlight(null);
2065
- setSnapTransition(false);
2066
- if (!resizableSplits) return;
2067
- let weights = node.children.map((child) => layout[child.id] ?? child.weight ?? 1);
2068
- if (boundary !== null && effectiveSnap !== null && snapActive) weights = snapSizes(weights, boundary, effectiveSnap).sizes;
2069
- if (!weights.some((weight, index) => Math.abs(weight - (node.children[index]?.weight ?? 1)) > WEIGHT_EPSILON)) return;
2070
- dispatch({
2071
- rowId: node.id,
2072
- type: "adjustSplit",
2073
- weights
2074
- });
2075
- };
2280
+ const total = node.children.reduce((sum, child) => sum + child.weight, 0);
2281
+ const { beginBoundaryDrag, groupRef, onLayoutChange, onLayoutChanged, snappedBoundary } = useSnapResize({
2282
+ children: node.children,
2283
+ onWeightsSettled: (weights) => {
2284
+ dispatch({
2285
+ rowId: node.id,
2286
+ type: "adjustSplit",
2287
+ weights
2288
+ });
2289
+ },
2290
+ resizableSplits,
2291
+ snap: effectiveSnap
2292
+ });
2076
2293
  return /* @__PURE__ */ jsx(Group, {
2077
2294
  "data-dashfoo": "row",
2295
+ "data-dashfoo-snapping": snappedBoundary === null ? void 0 : "true",
2078
2296
  disabled: !resizableSplits,
2079
- elementRef: groupElement,
2080
2297
  groupRef,
2081
- onLayoutChange: snapActive ? handleLayoutChange : void 0,
2082
- onLayoutChanged: handleLayoutChanged,
2298
+ onLayoutChange: snapActive ? onLayoutChange : void 0,
2299
+ onLayoutChanged,
2083
2300
  orientation,
2084
2301
  style: groupStyle,
2085
2302
  children: node.children.map((child, index) => {
2086
- const percent = (child.weight ?? 1) / total * 100;
2303
+ const percent = child.weight / total * 100;
2087
2304
  let min = child.min ? dimensionToSize(child.min) : void 0;
2088
2305
  if (min === void 0) {
2089
2306
  const minimum = descendantMinSize(child, node.orientation, tabsetMinSize);
@@ -2095,7 +2312,7 @@ const RowView = ({ node, renderTabset }) => {
2095
2312
  "data-dashfoo-snapped": snappedBoundary === index - 1 ? "true" : void 0,
2096
2313
  disabled: !resizableSplits,
2097
2314
  onPointerDown: snapActive && resizableSplits ? () => {
2098
- activeBoundary.current = index - 1;
2315
+ beginBoundaryDrag(index - 1);
2099
2316
  } : void 0
2100
2317
  }) : null, /* @__PURE__ */ jsx(Panel$1, {
2101
2318
  defaultSize: `${percent}%`,
@@ -2139,21 +2356,24 @@ const bodyStyle = {
2139
2356
  minHeight: 0,
2140
2357
  position: "relative"
2141
2358
  };
2142
- const CHIP_SIZE = {
2143
- height: 34,
2144
- width: 168
2359
+ const FLOAT_OVERRIDES = {
2360
+ draggableTabsets: false,
2361
+ floatable: false,
2362
+ maximizable: false,
2363
+ maximizedTabsetId: void 0
2145
2364
  };
2146
- const TAP_SLOP = 4;
2147
2365
  const FloatTitleEditor = ({ dispatch, node, onDone }) => {
2148
2366
  const inputRef = useRef(null);
2149
2367
  const { handleBlur, handleKeyDown } = useInlineRename({
2150
2368
  currentName: floatTitle(node),
2151
2369
  inputRef,
2152
- onCommit: (name) => dispatch({
2153
- floatId: node.id,
2154
- name,
2155
- type: "renameFloat"
2156
- }),
2370
+ onCommit: (name) => {
2371
+ dispatch({
2372
+ floatId: node.id,
2373
+ name,
2374
+ type: "renameFloat"
2375
+ });
2376
+ },
2157
2377
  onDone
2158
2378
  });
2159
2379
  return /* @__PURE__ */ jsx("input", {
@@ -2162,7 +2382,9 @@ const FloatTitleEditor = ({ dispatch, node, onDone }) => {
2162
2382
  defaultValue: floatTitle(node),
2163
2383
  onBlur: handleBlur,
2164
2384
  onKeyDown: handleKeyDown,
2165
- onPointerDown: (event) => event.stopPropagation(),
2385
+ onPointerDown: (event) => {
2386
+ event.stopPropagation();
2387
+ },
2166
2388
  ref: inputRef,
2167
2389
  style: titleStyle,
2168
2390
  type: "text"
@@ -2173,35 +2395,24 @@ const FloatTitle = ({ dispatch, node, renamable }) => {
2173
2395
  if (renamable && editing) return /* @__PURE__ */ jsx(FloatTitleEditor, {
2174
2396
  dispatch,
2175
2397
  node,
2176
- onDone: () => setEditing(false)
2398
+ onDone: () => {
2399
+ setEditing(false);
2400
+ }
2177
2401
  });
2178
2402
  return /* @__PURE__ */ jsx("span", {
2179
2403
  "data-dashfoo": "float-title",
2180
- onDoubleClick: renamable ? () => setEditing(true) : void 0,
2404
+ onDoubleClick: renamable ? () => {
2405
+ setEditing(true);
2406
+ } : void 0,
2181
2407
  style: titleStyle,
2182
2408
  title: renamable ? "Double-click to rename" : void 0,
2183
2409
  children: floatTitle(node)
2184
2410
  });
2185
2411
  };
2186
- const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2412
+ const FloatPanel = ({ node, onFocus, zIndex }) => {
2187
2413
  const dispatch = useLayout((state) => state.dispatch);
2188
- const closableTabs = useLayout((state) => state.closableTabs);
2189
- const draggableTabs = useLayout((state) => state.draggableTabs);
2190
2414
  const editable = useLayout((state) => state.editable);
2191
- const renamableTabs = useLayout((state) => state.renamableTabs);
2192
- const resizableSplits = useLayout((state) => state.resizableSplits);
2193
- const keepMounted = useLayout((state) => state.keepMounted);
2194
- const renderTab = useLayout((state) => state.renderTab);
2195
- const renderTabLabel = useLayout((state) => state.renderTabLabel);
2196
- const renderTabsetToolbar = useLayout((state) => state.renderTabsetToolbar);
2197
- const snap = useLayout((state) => state.snap);
2198
- const panelRef = useRef(null);
2199
- const gestureRef = useRef(null);
2200
- const latestRef = useRef(node.geometry);
2201
- const movedRef = useRef(false);
2202
- const setPanel = (element) => {
2203
- panelRef.current = element;
2204
- };
2415
+ const minimized = node.minimized === true;
2205
2416
  const restore = () => {
2206
2417
  if (!editable) return;
2207
2418
  dispatch({
@@ -2210,88 +2421,30 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2210
2421
  type: "setFloatMinimized"
2211
2422
  });
2212
2423
  };
2213
- const handlePointerDown = (event) => {
2214
- const panel = panelRef.current;
2215
- if (!panel || !editable) return;
2216
- const prior = gestureRef.current;
2217
- if (prior && panel.hasPointerCapture?.(prior.pointerId)) panel.releasePointerCapture(prior.pointerId);
2218
- movedRef.current = false;
2219
- const edgeKey = event.currentTarget.dataset.edge;
2220
- const parent = panel.offsetParent;
2221
- const gesture = {
2222
- bounds: {
2223
- height: parent?.clientHeight ?? 0,
2224
- width: parent?.clientWidth ?? 0
2225
- },
2226
- edges: edgeKey ? EDGE_BY_KEY.get(edgeKey) ?? null : null,
2227
- pointerId: event.pointerId,
2228
- start: node.geometry,
2229
- startX: event.clientX,
2230
- startY: event.clientY
2231
- };
2232
- gestureRef.current = gesture;
2233
- latestRef.current = node.geometry;
2234
- if (gesture.edges) panel.setPointerCapture(event.pointerId);
2235
- };
2236
- const handlePointerUp = (event) => {
2237
- const gesture = gestureRef.current;
2238
- if (!gesture || event.pointerId !== gesture.pointerId) return;
2239
- gestureRef.current = null;
2240
- const panel = panelRef.current;
2241
- if (panel?.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
2242
- if (!movedRef.current) {
2243
- if (node.minimized) restore();
2244
- return;
2245
- }
2246
- dispatch({
2247
- floatId: node.id,
2248
- geometry: latestRef.current,
2249
- type: "moveFloat"
2250
- });
2251
- };
2252
- const handlePointerMove = (event) => {
2253
- const gesture = gestureRef.current;
2254
- const panel = panelRef.current;
2255
- if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
2256
- if (event.buttons === 0) {
2257
- handlePointerUp(event);
2258
- return;
2259
- }
2260
- const dx = event.clientX - gesture.startX;
2261
- const dy = event.clientY - gesture.startY;
2262
- if (!movedRef.current) {
2263
- if (Math.hypot(dx, dy) <= TAP_SLOP) return;
2264
- movedRef.current = true;
2265
- if (!gesture.edges) panel.setPointerCapture(gesture.pointerId);
2266
- }
2267
- const next = gesture.edges ? resizeRect(gesture.start, gesture.edges, dx, dy) : clampToBounds({
2268
- ...gesture.start,
2269
- left: gesture.start.left + dx,
2270
- top: gesture.start.top + dy
2271
- }, gesture.bounds, node.minimized ? CHIP_SIZE : void 0);
2272
- latestRef.current = next;
2273
- panel.style.left = `${next.left}px`;
2274
- panel.style.top = `${next.top}px`;
2275
- if (!node.minimized) {
2276
- panel.style.width = `${next.width}px`;
2277
- panel.style.height = `${next.height}px`;
2278
- }
2279
- };
2280
- const handlePointerCancel = (event) => {
2281
- const gesture = gestureRef.current;
2282
- const panel = panelRef.current;
2283
- if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
2284
- gestureRef.current = null;
2285
- if (panel.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
2286
- if (!movedRef.current) return;
2287
- panel.style.left = `${gesture.start.left}px`;
2288
- panel.style.top = `${gesture.start.top}px`;
2289
- if (!node.minimized) {
2290
- panel.style.width = `${gesture.start.width}px`;
2291
- panel.style.height = `${gesture.start.height}px`;
2424
+ const { handlers, ref, style } = useFloatGesture({
2425
+ editable,
2426
+ geometry: node.geometry,
2427
+ minimized,
2428
+ onCommit: (geometry) => {
2429
+ dispatch({
2430
+ floatId: node.id,
2431
+ geometry,
2432
+ type: "moveFloat"
2433
+ });
2434
+ },
2435
+ onTap: () => {
2436
+ if (minimized) restore();
2292
2437
  }
2438
+ });
2439
+ const { onPointerDown, ...rootHandlers } = handlers;
2440
+ const frameStyle = {
2441
+ ...style,
2442
+ pointerEvents: "auto",
2443
+ position: "absolute",
2444
+ zIndex
2293
2445
  };
2294
- if (node.minimized) return /* @__PURE__ */ jsxs("button", {
2446
+ if (minimized) return /* @__PURE__ */ jsxs("button", {
2447
+ ...rootHandlers,
2295
2448
  "aria-label": `Restore ${floatTitle(node)} panel`,
2296
2449
  "data-dashfoo": "float-chip",
2297
2450
  onKeyDown: (event) => {
@@ -2300,19 +2453,10 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2300
2453
  restore();
2301
2454
  }
2302
2455
  },
2303
- onPointerCancel: handlePointerCancel,
2304
- onPointerDown: handlePointerDown,
2456
+ onPointerDown,
2305
2457
  onPointerDownCapture: onFocus,
2306
- onPointerMove: handlePointerMove,
2307
- onPointerUp: handlePointerUp,
2308
- ref: setPanel,
2309
- style: {
2310
- left: node.geometry.left,
2311
- pointerEvents: "auto",
2312
- position: "absolute",
2313
- top: node.geometry.top,
2314
- zIndex
2315
- },
2458
+ ref,
2459
+ style: frameStyle,
2316
2460
  title: floatTitle(node),
2317
2461
  type: "button",
2318
2462
  children: [/* @__PURE__ */ jsx(FloatIcon, {}), /* @__PURE__ */ jsx("span", {
@@ -2320,31 +2464,16 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2320
2464
  children: floatTitle(node)
2321
2465
  })]
2322
2466
  });
2323
- const floatModel = {
2324
- global,
2325
- layout: node.layout,
2326
- version: 1
2327
- };
2328
2467
  return /* @__PURE__ */ jsxs("div", {
2468
+ ...rootHandlers,
2329
2469
  "data-dashfoo": "float",
2330
- onPointerCancel: handlePointerCancel,
2331
2470
  onPointerDownCapture: onFocus,
2332
- onPointerMove: handlePointerMove,
2333
- onPointerUp: handlePointerUp,
2334
- ref: setPanel,
2335
- style: {
2336
- height: node.geometry.height,
2337
- left: node.geometry.left,
2338
- pointerEvents: "auto",
2339
- position: "absolute",
2340
- top: node.geometry.top,
2341
- width: node.geometry.width,
2342
- zIndex
2343
- },
2471
+ ref,
2472
+ style: frameStyle,
2344
2473
  children: [
2345
2474
  /* @__PURE__ */ jsxs("div", {
2346
2475
  "data-dashfoo": "float-titlebar",
2347
- onPointerDown: handlePointerDown,
2476
+ onPointerDown,
2348
2477
  style: editable ? titleBarStyle : {
2349
2478
  ...titleBarStyle,
2350
2479
  cursor: "default"
@@ -2363,12 +2492,16 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2363
2492
  editable && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
2364
2493
  "aria-label": "Minimize panel",
2365
2494
  "data-dashfoo": "float-minimize",
2366
- onClick: () => dispatch({
2367
- floatId: node.id,
2368
- minimized: true,
2369
- type: "setFloatMinimized"
2370
- }),
2371
- onPointerDown: (event) => event.stopPropagation(),
2495
+ onClick: () => {
2496
+ dispatch({
2497
+ floatId: node.id,
2498
+ minimized: true,
2499
+ type: "setFloatMinimized"
2500
+ });
2501
+ },
2502
+ onPointerDown: (event) => {
2503
+ event.stopPropagation();
2504
+ },
2372
2505
  style: dockButtonStyle,
2373
2506
  title: "Minimize panel",
2374
2507
  type: "button",
@@ -2376,11 +2509,15 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2376
2509
  }), /* @__PURE__ */ jsx("button", {
2377
2510
  "aria-label": "Dock panel back into the main layout",
2378
2511
  "data-dashfoo": "float-dock",
2379
- onClick: () => dispatch({
2380
- floatId: node.id,
2381
- type: "dockFloat"
2382
- }),
2383
- onPointerDown: (event) => event.stopPropagation(),
2512
+ onClick: () => {
2513
+ dispatch({
2514
+ floatId: node.id,
2515
+ type: "dockFloat"
2516
+ });
2517
+ },
2518
+ onPointerDown: (event) => {
2519
+ event.stopPropagation();
2520
+ },
2384
2521
  style: dockButtonStyle,
2385
2522
  title: "Dock panel back",
2386
2523
  type: "button",
@@ -2391,29 +2528,15 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
2391
2528
  /* @__PURE__ */ jsx("div", {
2392
2529
  "data-dashfoo": "float-body",
2393
2530
  style: bodyStyle,
2394
- children: /* @__PURE__ */ jsx(LayoutRoot, {
2395
- closableTabs,
2396
- dispatch,
2397
- draggableTabs,
2398
- draggableTabsets: false,
2399
- editable,
2400
- floatable: false,
2401
- keepMounted,
2402
- maximizable: false,
2403
- model: floatModel,
2404
- renamableTabs,
2405
- renderTab,
2406
- renderTabLabel,
2407
- renderTabsetToolbar,
2408
- resizableSplits,
2409
- snap: snap ?? void 0,
2531
+ children: /* @__PURE__ */ jsx(LayoutOverrides, {
2532
+ overrides: FLOAT_OVERRIDES,
2410
2533
  children: /* @__PURE__ */ jsx(DragProvider, { children: /* @__PURE__ */ jsx(RowView, { node: node.layout }) })
2411
2534
  })
2412
2535
  }),
2413
2536
  editable && RESIZE_HANDLES.map((handle) => /* @__PURE__ */ jsx("div", {
2414
2537
  "data-dashfoo": "float-resize",
2415
2538
  "data-edge": handle.key,
2416
- onPointerDown: handlePointerDown,
2539
+ onPointerDown,
2417
2540
  style: {
2418
2541
  position: "absolute",
2419
2542
  touchAction: "none",
@@ -2433,26 +2556,24 @@ const overlayStyle = {
2433
2556
  position: "fixed",
2434
2557
  zIndex: 1
2435
2558
  };
2436
- const FloatOverlay = ({ floats, global }) => {
2559
+ const FloatOverlay = ({ floats }) => {
2437
2560
  const [topId, setTopId] = useState(null);
2438
2561
  if (floats.length === 0) return null;
2439
2562
  return /* @__PURE__ */ jsx("div", {
2440
2563
  "data-dashfoo": "float-overlay",
2441
2564
  style: overlayStyle,
2442
2565
  children: floats.map((node) => /* @__PURE__ */ jsx(FloatPanel, {
2443
- global,
2444
2566
  node,
2445
- onFocus: () => setTopId(node.id),
2567
+ onFocus: () => {
2568
+ setTopId(node.id);
2569
+ },
2446
2570
  zIndex: node.id === topId ? 2 : 1
2447
2571
  }, node.id))
2448
2572
  });
2449
2573
  };
2450
- const FloatLayer = ({ children, floats, global }) => /* @__PURE__ */ jsxs(FloatLayerContext.Provider, {
2574
+ const FloatLayer = ({ children, floats }) => /* @__PURE__ */ jsxs(FloatLayerContext.Provider, {
2451
2575
  value: true,
2452
- children: [children, /* @__PURE__ */ jsx(FloatOverlay, {
2453
- floats,
2454
- global
2455
- })]
2576
+ children: [children, /* @__PURE__ */ jsx(FloatOverlay, { floats })]
2456
2577
  });
2457
2578
 
2458
2579
  //#endregion
@@ -2499,48 +2620,62 @@ const DashfooLayout = forwardRef((props, ref) => {
2499
2620
  });
2500
2621
  const hasSharedManager = useContext(SharedDragManagerContext) !== null;
2501
2622
  useImperativeHandle(ref, () => ({
2502
- addTab: (tab, target) => store.dispatch({
2503
- index: target.index,
2504
- location: target.location ?? "center",
2505
- tab,
2506
- targetId: target.targetId,
2507
- type: "addNode"
2508
- }),
2623
+ addTab: (tab, target) => {
2624
+ store.dispatch({
2625
+ index: target.index,
2626
+ location: target.location ?? "center",
2627
+ tab,
2628
+ targetId: target.targetId,
2629
+ type: "addNode"
2630
+ });
2631
+ },
2509
2632
  canRedo: store.canRedo,
2510
2633
  canUndo: store.canUndo,
2511
- closeTab: (tabId) => store.dispatch({
2512
- tabId,
2513
- type: "deleteTab"
2514
- }),
2634
+ closeTab: (tabId) => {
2635
+ store.dispatch({
2636
+ tabId,
2637
+ type: "deleteTab"
2638
+ });
2639
+ },
2515
2640
  dispatch: store.dispatch,
2516
- dockFloat: (floatId) => store.dispatch({
2517
- floatId,
2518
- type: "dockFloat"
2519
- }),
2520
- floatTab: (tabId) => store.dispatch({
2521
- tabId,
2522
- type: "floatTab"
2523
- }),
2641
+ dockFloat: (floatId) => {
2642
+ store.dispatch({
2643
+ floatId,
2644
+ type: "dockFloat"
2645
+ });
2646
+ },
2647
+ floatTab: (tabId) => {
2648
+ store.dispatch({
2649
+ tabId,
2650
+ type: "floatTab"
2651
+ });
2652
+ },
2524
2653
  getModel: () => store.model,
2525
- maximizeTabset: (tabsetId) => store.dispatch({
2526
- tabsetId,
2527
- type: "setMaximizedTabset"
2528
- }),
2654
+ maximizeTabset: (tabsetId) => {
2655
+ store.dispatch({
2656
+ tabsetId,
2657
+ type: "setMaximizedTabset"
2658
+ });
2659
+ },
2529
2660
  redo: store.redo,
2530
- renameTab: (tabId, name) => store.dispatch({
2531
- name,
2532
- tabId,
2533
- type: "renameTab"
2534
- }),
2661
+ renameTab: (tabId, name) => {
2662
+ store.dispatch({
2663
+ name,
2664
+ tabId,
2665
+ type: "renameTab"
2666
+ });
2667
+ },
2535
2668
  resetLayout: () => {
2536
2669
  persistence.clear();
2537
2670
  if (defaultModel !== void 0) store.setModel(defaultModel);
2538
2671
  },
2539
- selectTab: (tabsetId, index) => store.dispatch({
2540
- index,
2541
- tabsetId,
2542
- type: "selectTab"
2543
- }),
2672
+ selectTab: (tabsetId, index) => {
2673
+ store.dispatch({
2674
+ index,
2675
+ tabsetId,
2676
+ type: "selectTab"
2677
+ });
2678
+ },
2544
2679
  undo: store.undo
2545
2680
  }), [
2546
2681
  defaultModel,
@@ -2566,24 +2701,18 @@ const DashfooLayout = forwardRef((props, ref) => {
2566
2701
  responsive?.orientation,
2567
2702
  store.model
2568
2703
  ]);
2569
- const storeDispatch = store.dispatch;
2570
- const dispatch = useCallback((action) => {
2571
- if (isCompact && action.type === "adjustSplit") return;
2572
- storeDispatch(action);
2573
- }, [isCompact, storeDispatch]);
2574
- const maximized = view.maximizedTabsetId ? findTabset(view, view.maximizedTabsetId) : void 0;
2704
+ const maximized = view.maximizedTabsetId === void 0 ? void 0 : findTabset(view, view.maximizedTabsetId);
2575
2705
  const tree = /* @__PURE__ */ jsx(Layout.FloatLayer, {
2576
- floats: store.model.floats ?? [],
2577
- global: store.model.global,
2706
+ floats: store.model.floats,
2578
2707
  children: /* @__PURE__ */ jsx(Layout.DragLayer, { children: maximized ? /* @__PURE__ */ jsx(Layout.Tabset, { node: maximized }) : /* @__PURE__ */ jsx(Layout.Rows, { node: view.layout }) })
2579
2708
  });
2580
2709
  return /* @__PURE__ */ jsx(Layout.Root, {
2581
2710
  closableTabs,
2582
- dispatch,
2583
- draggableTabs: isCompact ? false : draggableTabs,
2584
- draggableTabsets: isCompact ? false : draggableTabsets,
2711
+ dispatch: store.dispatch,
2712
+ draggableTabs,
2713
+ draggableTabsets,
2585
2714
  editable,
2586
- floatable: isCompact ? false : floatable,
2715
+ floatable,
2587
2716
  keepMounted,
2588
2717
  maximizable,
2589
2718
  model: view,
@@ -2591,7 +2720,8 @@ const DashfooLayout = forwardRef((props, ref) => {
2591
2720
  renderTab,
2592
2721
  renderTabLabel,
2593
2722
  renderTabsetToolbar,
2594
- resizableSplits: isCompact ? false : resizableSplits,
2723
+ resizableSplits,
2724
+ restructurable: !isCompact,
2595
2725
  rootRef: containerRef,
2596
2726
  snap,
2597
2727
  children: hasSharedManager ? tree : /* @__PURE__ */ jsx(DashfooDragProvider, { children: tree })