@dashfoo/react 0.6.4 → 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.d.ts +44 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +635 -557
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { canRedo, canUndo, dashfooMachine, dragDockMachine, findTabset, fromJSON, normalize,
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
203
|
+
const actorRef = useContext(DragRootContext)?.actorRef;
|
|
204
|
+
return useSelector(actorRef, (snapshot) => snapshot?.context.subject ?? null);
|
|
199
205
|
};
|
|
200
206
|
const useDropIntent = () => {
|
|
201
|
-
|
|
207
|
+
const actorRef = useContext(DragRootContext)?.actorRef;
|
|
208
|
+
return useSelector(actorRef, (snapshot) => snapshot?.context.intent ?? null, sameDropIntent);
|
|
202
209
|
};
|
|
203
210
|
|
|
204
211
|
//#endregion
|
|
@@ -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
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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)
|
|
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)
|
|
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;
|
|
@@ -626,160 +680,43 @@ const DashfooDragProvider = ({ children }) => {
|
|
|
626
680
|
useInsertionEffect(() => () => {
|
|
627
681
|
manager.destroy();
|
|
628
682
|
}, [manager]);
|
|
629
|
-
const [subjectStore] = useState(createDragSubjectStore);
|
|
630
|
-
return /* @__PURE__ */ jsx(SharedDragManagerContext.Provider, {
|
|
631
|
-
value: manager,
|
|
632
|
-
children: /* @__PURE__ */ jsxs(DragSubjectStoreContext.Provider, {
|
|
633
|
-
value: subjectStore,
|
|
634
|
-
children: [children, /* @__PURE__ */ jsx(DragPreviewOverlay, { manager })]
|
|
635
|
-
})
|
|
636
|
-
});
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
//#endregion
|
|
640
|
-
//#region src/hooks/layout-store.ts
|
|
641
|
-
const createLayoutStore = (initial) => createStore(() => initial);
|
|
642
|
-
const LayoutStoreContext = createContext(null);
|
|
643
|
-
const useLayout = (selector) => {
|
|
644
|
-
const store = useContext(LayoutStoreContext);
|
|
645
|
-
if (store === null) throw new Error("dashfoo components must be rendered inside <DashfooLayout> or <Layout.Root>.");
|
|
646
|
-
return useStore(store, selector);
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
//#endregion
|
|
650
|
-
//#region src/lib/warn-once.ts
|
|
651
|
-
const warned = /* @__PURE__ */ new Set();
|
|
652
|
-
const warnOnce = (key, message) => {
|
|
653
|
-
if (warned.has(key)) return;
|
|
654
|
-
warned.add(key);
|
|
655
|
-
console.warn(`[dashfoo] ${message}`);
|
|
656
|
-
};
|
|
657
|
-
|
|
658
|
-
//#endregion
|
|
659
|
-
//#region src/components/drag-adapter.tsx
|
|
660
|
-
const tabRects = (strip, excludeId) => [...strip.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => tab.dataset.tabId === excludeId ? [] : [tab.getBoundingClientRect()]);
|
|
661
|
-
const intentForTabset = (id, element, point, draggedId) => {
|
|
662
|
-
const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
|
|
663
|
-
if (strip && pointInRect(point, strip.getBoundingClientRect())) return {
|
|
664
|
-
index: insertionIndex(tabRects(strip, draggedId), point.x),
|
|
665
|
-
location: "center",
|
|
666
|
-
targetId: id
|
|
667
|
-
};
|
|
668
|
-
const target = resolveDockTarget(point, element.getBoundingClientRect());
|
|
669
|
-
const location = target.kind === "tab" ? "center" : `split-${target.edge}`;
|
|
670
|
-
if (location === "center" && strip) return {
|
|
671
|
-
index: tabRects(strip, draggedId).length,
|
|
672
|
-
location,
|
|
673
|
-
targetId: id
|
|
674
|
-
};
|
|
675
|
-
return {
|
|
676
|
-
location,
|
|
677
|
-
targetId: id
|
|
678
|
-
};
|
|
679
|
-
};
|
|
680
|
-
const isTabFactory = (value) => typeof value === "function";
|
|
681
|
-
const subjectFor = (source) => {
|
|
682
|
-
const data = source.data;
|
|
683
|
-
if (data?.type === "tabset") return {
|
|
684
|
-
id: String(data.tabsetId),
|
|
685
|
-
kind: "tabset"
|
|
686
|
-
};
|
|
687
|
-
if (data?.type === "external") {
|
|
688
|
-
if (!isTabFactory(data.createTab)) {
|
|
689
|
-
console.warn("[dashfoo] external drag source is missing its createTab function");
|
|
690
|
-
return null;
|
|
691
|
-
}
|
|
692
|
-
let candidate;
|
|
693
|
-
try {
|
|
694
|
-
candidate = data.createTab();
|
|
695
|
-
} catch (error) {
|
|
696
|
-
console.warn("[dashfoo] external drag source createTab threw", error);
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
const parsed = tabNodeSchema.safeParse(candidate);
|
|
700
|
-
if (!parsed.success) {
|
|
701
|
-
console.warn("[dashfoo] external drag source returned an invalid tab", parsed.error);
|
|
702
|
-
return null;
|
|
703
|
-
}
|
|
704
|
-
return {
|
|
705
|
-
id: String(source.id),
|
|
706
|
-
kind: "external",
|
|
707
|
-
tab: parsed.data
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
return {
|
|
711
|
-
id: String(source.id),
|
|
712
|
-
kind: "tab"
|
|
713
|
-
};
|
|
714
|
-
};
|
|
715
|
-
const DragProvider = ({ children, onCommit, splitDock }) => {
|
|
716
683
|
const actorRef = useActorRef(dragDockMachine);
|
|
717
|
-
const
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
useInsertionEffect(() => () => ownManager?.destroy(), [ownManager]);
|
|
721
|
-
const manager = sharedManager ?? ownManager;
|
|
722
|
-
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");
|
|
723
|
-
const inheritedStore = useContext(DragSubjectStoreContext);
|
|
724
|
-
const [subjectStore] = useState(() => inheritedStore ?? createDragSubjectStore());
|
|
725
|
-
const ownerId = useId();
|
|
726
|
-
useEffect(() => {
|
|
727
|
-
const subscription = actorRef.subscribe((snapshot) => {
|
|
728
|
-
const { intent, subject } = snapshot.context;
|
|
729
|
-
const state = subjectStore.getState();
|
|
730
|
-
if (intent !== null) {
|
|
731
|
-
if (!sameDropIntent(state.intent, intent) || state.intentOwner !== ownerId) subjectStore.setState({
|
|
732
|
-
intent,
|
|
733
|
-
intentOwner: ownerId
|
|
734
|
-
});
|
|
735
|
-
} else if (state.intent !== null && state.intentOwner === ownerId) subjectStore.setState({
|
|
736
|
-
intent: null,
|
|
737
|
-
intentOwner: null
|
|
738
|
-
});
|
|
739
|
-
if (!sameDragSubject(subjectStore.getState().subject, subject)) subjectStore.setState({ subject });
|
|
740
|
-
});
|
|
684
|
+
const [scopes] = useState(() => /* @__PURE__ */ new Map());
|
|
685
|
+
const registerScope = useCallback((scope) => {
|
|
686
|
+
scopes.set(scope.layerId, scope);
|
|
741
687
|
return () => {
|
|
742
|
-
|
|
688
|
+
scopes.delete(scope.layerId);
|
|
743
689
|
};
|
|
744
|
-
}, [
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const splitDockRef = useRef(splitDock);
|
|
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
|
+
*/
|
|
751
696
|
useEffect(() => {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const commit = onCommitRef.current ?? layoutStore?.getState().dispatch;
|
|
763
|
-
if (!commit) {
|
|
764
|
-
warnOnce("drag-layer-no-commit", "Layout.DragLayer has neither an onCommit prop nor a <Layout.Root> above it; drops are ignored");
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
commit(emitted.action);
|
|
768
|
-
});
|
|
769
|
-
return () => {
|
|
770
|
-
subscription.unsubscribe();
|
|
771
|
-
};
|
|
772
|
-
}, [actorRef, layoutStore]);
|
|
773
|
-
useEffect(() => {
|
|
774
|
-
const resolveIntent = (targetId, element, point, draggedId) => {
|
|
775
|
-
if (layoutStore?.getState().editable === false) return null;
|
|
776
|
-
if (!shouldAllowDrop(draggedId, targetId, [...element.querySelectorAll("[data-dashfoo=\"tab\"]")].map((tab) => tab.dataset.tabId ?? ""))) return null;
|
|
777
|
-
const intent = intentForTabset(targetId, element, point, draggedId);
|
|
778
|
-
if (!(splitDockRef.current ?? layoutStore?.getState().splitDock ?? true) && intent.location.startsWith("split-")) return {
|
|
779
|
-
location: "center",
|
|
780
|
-
targetId
|
|
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
|
|
781
707
|
};
|
|
782
|
-
|
|
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
|
+
});
|
|
783
720
|
};
|
|
784
721
|
const handleStart = (event) => {
|
|
785
722
|
const source = event.operation.source;
|
|
@@ -789,24 +726,12 @@ const DragProvider = ({ children, onCommit, splitDock }) => {
|
|
|
789
726
|
manager.actions.stop({ canceled: true });
|
|
790
727
|
return;
|
|
791
728
|
}
|
|
729
|
+
intentLayerId = null;
|
|
792
730
|
actorRef.send({
|
|
793
731
|
subject,
|
|
794
732
|
type: "START"
|
|
795
733
|
});
|
|
796
734
|
};
|
|
797
|
-
const syncIntent = () => {
|
|
798
|
-
const operation = manager.dragOperation;
|
|
799
|
-
const point = operation.position.current;
|
|
800
|
-
const target = operation.target;
|
|
801
|
-
const data = target?.data;
|
|
802
|
-
const tabsetId = data?.layerId === ownerId ? data.tabsetId : void 0;
|
|
803
|
-
const element = tabsetId === void 0 ? void 0 : target?.element;
|
|
804
|
-
const draggedId = operation.source ? String(operation.source.id) : void 0;
|
|
805
|
-
actorRef.send({
|
|
806
|
-
intent: tabsetId !== void 0 && element instanceof HTMLElement ? resolveIntent(tabsetId, element, point, draggedId) : null,
|
|
807
|
-
type: "OVER"
|
|
808
|
-
});
|
|
809
|
-
};
|
|
810
735
|
const handleMove = () => {
|
|
811
736
|
syncIntent();
|
|
812
737
|
};
|
|
@@ -821,11 +746,15 @@ const DragProvider = ({ children, onCommit, splitDock }) => {
|
|
|
821
746
|
syncIntent();
|
|
822
747
|
actorRef.send({ type: "DROP" });
|
|
823
748
|
};
|
|
749
|
+
const commitSubscription = actorRef.on("COMMIT", (emitted) => {
|
|
750
|
+
(intentLayerId === null ? void 0 : scopes.get(intentLayerId))?.commit(emitted.action);
|
|
751
|
+
});
|
|
824
752
|
const offStart = manager.monitor.addEventListener("dragstart", handleStart);
|
|
825
753
|
const offMove = manager.monitor.addEventListener("dragmove", handleMove);
|
|
826
754
|
const offCollision = manager.monitor.addEventListener("collision", handleCollision);
|
|
827
755
|
const offEnd = manager.monitor.addEventListener("dragend", handleEnd);
|
|
828
756
|
return () => {
|
|
757
|
+
commitSubscription.unsubscribe();
|
|
829
758
|
offStart();
|
|
830
759
|
offMove();
|
|
831
760
|
offCollision();
|
|
@@ -833,114 +762,134 @@ const DragProvider = ({ children, onCommit, splitDock }) => {
|
|
|
833
762
|
};
|
|
834
763
|
}, [
|
|
835
764
|
actorRef,
|
|
836
|
-
layoutStore,
|
|
837
765
|
manager,
|
|
838
|
-
|
|
766
|
+
scopes
|
|
839
767
|
]);
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
manager
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
ownManager === null ? null : /* @__PURE__ */ jsx(DragPreviewOverlay, { manager: ownManager })
|
|
855
|
-
]
|
|
768
|
+
const rootValue = useMemo(() => ({
|
|
769
|
+
actorRef,
|
|
770
|
+
manager,
|
|
771
|
+
registerScope
|
|
772
|
+
}), [
|
|
773
|
+
actorRef,
|
|
774
|
+
manager,
|
|
775
|
+
registerScope
|
|
776
|
+
]);
|
|
777
|
+
return /* @__PURE__ */ jsx(SharedDragManagerContext.Provider, {
|
|
778
|
+
value: manager,
|
|
779
|
+
children: /* @__PURE__ */ jsxs(DragRootContext.Provider, {
|
|
780
|
+
value: rootValue,
|
|
781
|
+
children: [children, /* @__PURE__ */ jsx(DragPreviewOverlay, { manager })]
|
|
856
782
|
})
|
|
857
783
|
});
|
|
858
784
|
};
|
|
859
785
|
|
|
860
786
|
//#endregion
|
|
861
|
-
//#region src/
|
|
862
|
-
const
|
|
863
|
-
const
|
|
787
|
+
//#region src/hooks/layout-store.ts
|
|
788
|
+
const createLayoutStore = (initial) => createStore(() => initial);
|
|
789
|
+
const LayoutStoreContext = createContext(null);
|
|
790
|
+
const useLayout = (selector) => {
|
|
791
|
+
const store = useContext(LayoutStoreContext);
|
|
792
|
+
if (store === null) throw new Error("dashfoo components must be rendered inside <DashfooLayout> or <Layout.Root>.");
|
|
793
|
+
return useStore(store, selector);
|
|
794
|
+
};
|
|
864
795
|
|
|
865
796
|
//#endregion
|
|
866
|
-
//#region src/
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (trimmed && trimmed !== currentName) onCommit(trimmed);
|
|
876
|
-
onDone();
|
|
877
|
-
};
|
|
878
|
-
const handleKeyDown = (event) => {
|
|
879
|
-
if (event.key === "Enter") {
|
|
880
|
-
done.current = true;
|
|
881
|
-
commit(event.currentTarget.value);
|
|
882
|
-
} else if (event.key === "Escape") {
|
|
883
|
-
done.current = true;
|
|
884
|
-
onDone();
|
|
885
|
-
}
|
|
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";
|
|
799
|
+
const tabRects = (strip, excludeId) => [...strip.querySelectorAll("[data-dashfoo=\"tab\"]")].flatMap((tab) => tab.dataset.tabId === excludeId ? [] : [tab.getBoundingClientRect()]);
|
|
800
|
+
const intentForTabset = (id, element, point, draggedId) => {
|
|
801
|
+
const strip = element.querySelector("[data-dashfoo=\"tabstrip\"]");
|
|
802
|
+
if (strip && pointInRect(point, strip.getBoundingClientRect())) return {
|
|
803
|
+
index: insertionIndex(tabRects(strip, draggedId), point.x),
|
|
804
|
+
location: "center",
|
|
805
|
+
targetId: id
|
|
886
806
|
};
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
807
|
+
const location = dockLocationFor(resolveDockTarget(point, element.getBoundingClientRect()));
|
|
808
|
+
if (location === "center" && strip) return {
|
|
809
|
+
index: tabRects(strip, draggedId).length,
|
|
810
|
+
location,
|
|
811
|
+
targetId: id
|
|
891
812
|
};
|
|
892
813
|
return {
|
|
893
|
-
|
|
894
|
-
|
|
814
|
+
location,
|
|
815
|
+
targetId: id
|
|
895
816
|
};
|
|
896
817
|
};
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const
|
|
903
|
-
const
|
|
904
|
-
const
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
});
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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);
|
|
825
|
+
const layoutStore = useContext(LayoutStoreContext);
|
|
826
|
+
const layerId = useId();
|
|
827
|
+
const manager = root?.manager;
|
|
828
|
+
const getTabsetElement = useCallback((id) => {
|
|
829
|
+
const element = manager?.registry.droppables.get(`${layerId}:${id}`)?.element;
|
|
830
|
+
return element instanceof HTMLElement ? element : void 0;
|
|
831
|
+
}, [layerId, manager]);
|
|
832
|
+
const scope = useMemo(() => ({
|
|
833
|
+
commit: (action) => {
|
|
834
|
+
if (layoutStore?.getState().editable === false) return;
|
|
835
|
+
const commit = onCommit ?? layoutStore?.getState().dispatch;
|
|
836
|
+
if (!commit) {
|
|
837
|
+
warnOnce("drag-layer-no-commit", "Layout.DragLayer has neither an onCommit prop nor a <Layout.Root> above it; drops are ignored");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
commit(action);
|
|
841
|
+
},
|
|
842
|
+
layerId,
|
|
843
|
+
resolveIntent: (targetId, element, point, draggedId) => {
|
|
844
|
+
if (layoutStore?.getState().editable === false) return null;
|
|
845
|
+
if (!shouldAllowDrop(draggedId, targetId, [...element.querySelectorAll("[data-dashfoo=\"tab\"]")].map((tab) => tab.dataset.tabId ?? ""))) return null;
|
|
846
|
+
const intent = intentForTabset(targetId, element, point, draggedId);
|
|
847
|
+
if (!(splitDock ?? layoutStore?.getState().splitDock ?? true) && splitEdge(intent.location) !== void 0) return {
|
|
848
|
+
location: "center",
|
|
849
|
+
targetId
|
|
850
|
+
};
|
|
851
|
+
return intent;
|
|
852
|
+
}
|
|
853
|
+
}), [
|
|
854
|
+
layerId,
|
|
855
|
+
layoutStore,
|
|
856
|
+
onCommit,
|
|
857
|
+
splitDock
|
|
858
|
+
]);
|
|
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, {
|
|
868
|
+
value: contextValue,
|
|
869
|
+
children: [children, /* @__PURE__ */ jsx(DockIndicator, {
|
|
870
|
+
actorRef: contextValue.actorRef,
|
|
871
|
+
getTabsetElement
|
|
872
|
+
})]
|
|
922
873
|
});
|
|
923
874
|
};
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
return {
|
|
937
|
-
height,
|
|
938
|
-
left,
|
|
939
|
-
top,
|
|
940
|
-
width
|
|
941
|
-
};
|
|
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;
|
|
942
886
|
};
|
|
943
887
|
|
|
888
|
+
//#endregion
|
|
889
|
+
//#region src/components/float-context.ts
|
|
890
|
+
const FloatLayerContext = createContext(false);
|
|
891
|
+
const useHasFloatLayer = () => useContext(FloatLayerContext);
|
|
892
|
+
|
|
944
893
|
//#endregion
|
|
945
894
|
//#region src/components/float-resize-handles.ts
|
|
946
895
|
const EDGE_THICKNESS = 12;
|
|
@@ -1056,9 +1005,205 @@ const RESIZE_HANDLES = [
|
|
|
1056
1005
|
left: CORNER_OFFSET,
|
|
1057
1006
|
width: CORNER_SIZE
|
|
1058
1007
|
}
|
|
1059
|
-
}
|
|
1060
|
-
];
|
|
1061
|
-
const EDGE_BY_KEY = new Map(RESIZE_HANDLES.map((h) => [h.key, h.edges]));
|
|
1008
|
+
}
|
|
1009
|
+
];
|
|
1010
|
+
const EDGE_BY_KEY = new Map(RESIZE_HANDLES.map((h) => [h.key, h.edges]));
|
|
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
|
+
};
|
|
1062
1207
|
|
|
1063
1208
|
//#endregion
|
|
1064
1209
|
//#region src/components/layout-root.tsx
|
|
@@ -1069,15 +1214,15 @@ const rootStyle = {
|
|
|
1069
1214
|
position: "relative",
|
|
1070
1215
|
width: "100%"
|
|
1071
1216
|
};
|
|
1072
|
-
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 }) => {
|
|
1073
1218
|
const global = model.global;
|
|
1074
1219
|
const snapshot = {
|
|
1075
1220
|
closableTabs: editable && closableTabs && global.tabEnableClose !== false,
|
|
1076
1221
|
dispatch,
|
|
1077
|
-
draggableTabs: editable && draggableTabs && global.tabEnableDrag !== false,
|
|
1078
|
-
draggableTabsets: editable && draggableTabsets,
|
|
1222
|
+
draggableTabs: editable && restructurable && draggableTabs && global.tabEnableDrag !== false,
|
|
1223
|
+
draggableTabsets: editable && restructurable && draggableTabsets,
|
|
1079
1224
|
editable,
|
|
1080
|
-
floatable: editable && floatable,
|
|
1225
|
+
floatable: editable && restructurable && floatable,
|
|
1081
1226
|
keepMounted,
|
|
1082
1227
|
maximizable: maximizable && global.tabSetEnableMaximize !== false,
|
|
1083
1228
|
maximizedTabsetId: model.maximizedTabsetId,
|
|
@@ -1085,7 +1230,7 @@ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = t
|
|
|
1085
1230
|
renderTab,
|
|
1086
1231
|
renderTabLabel,
|
|
1087
1232
|
renderTabsetToolbar,
|
|
1088
|
-
resizableSplits: editable && resizableSplits && global.enableSplitResize !== false,
|
|
1233
|
+
resizableSplits: editable && restructurable && resizableSplits && global.enableSplitResize !== false,
|
|
1089
1234
|
snap: snap ?? global.snap ?? null,
|
|
1090
1235
|
splitDock: global.enableSplitDock !== false,
|
|
1091
1236
|
tabLocation: global.tabLocation ?? "top",
|
|
@@ -1115,6 +1260,133 @@ const LayoutRoot = ({ children, closableTabs = true, dispatch, draggableTabs = t
|
|
|
1115
1260
|
})
|
|
1116
1261
|
});
|
|
1117
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", {
|
|
1281
|
+
"data-dashfoo": "layout",
|
|
1282
|
+
style: rootStyle,
|
|
1283
|
+
children
|
|
1284
|
+
})
|
|
1285
|
+
});
|
|
1286
|
+
};
|
|
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
|
+
};
|
|
1118
1390
|
|
|
1119
1391
|
//#endregion
|
|
1120
1392
|
//#region src/lib/tab-ids.ts
|
|
@@ -1983,22 +2255,6 @@ const groupStyle = {
|
|
|
1983
2255
|
minHeight: 0,
|
|
1984
2256
|
minWidth: 0
|
|
1985
2257
|
};
|
|
1986
|
-
const WEIGHT_EPSILON = .01;
|
|
1987
|
-
const layoutFromWeights = (children, total) => children.reduce((layout, child) => {
|
|
1988
|
-
layout[child.id] = (child.weight ?? 1) / total * 100;
|
|
1989
|
-
return layout;
|
|
1990
|
-
}, {});
|
|
1991
|
-
const orderedFromLayout = (layout, children) => children.map((child) => layout[child.id] ?? 0);
|
|
1992
|
-
const layoutFromOrdered = (sizes, children) => children.reduce((layout, child, index) => {
|
|
1993
|
-
layout[child.id] = sizes[index] ?? 0;
|
|
1994
|
-
return layout;
|
|
1995
|
-
}, {});
|
|
1996
|
-
const layoutsMatch = (current, expected) => {
|
|
1997
|
-
const currentIds = Object.keys(current);
|
|
1998
|
-
const expectedIds = Object.keys(expected);
|
|
1999
|
-
if (currentIds.length !== expectedIds.length) return false;
|
|
2000
|
-
return expectedIds.every((id) => Math.abs((current[id] ?? NaN) - expected[id]) <= WEIGHT_EPSILON);
|
|
2001
|
-
};
|
|
2002
2258
|
const descendantMinSize = (child, axis, tabsetMinSize) => {
|
|
2003
2259
|
const explicit = dimensionToPixels(child.min);
|
|
2004
2260
|
if (explicit !== void 0) return explicit;
|
|
@@ -2021,89 +2277,30 @@ const RowView = ({ node, renderTabset }) => {
|
|
|
2021
2277
|
const effectiveSnap = node.snap ?? globalSnap;
|
|
2022
2278
|
const snapActive = snapEnabled(effectiveSnap);
|
|
2023
2279
|
const orientation = node.orientation === "row" ? "horizontal" : "vertical";
|
|
2024
|
-
const total = node.children.reduce((sum, child) => sum +
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
}
|
|
2038
|
-
};
|
|
2039
|
-
const setSnapTransition = (active) => {
|
|
2040
|
-
const element = groupElement.current;
|
|
2041
|
-
if (!element) return;
|
|
2042
|
-
if (active) element.dataset.dashfooSnapping = "true";
|
|
2043
|
-
else delete element.dataset.dashfooSnapping;
|
|
2044
|
-
};
|
|
2045
|
-
useLayoutEffect(() => {
|
|
2046
|
-
const group = groupRef.current;
|
|
2047
|
-
if (!group) return;
|
|
2048
|
-
const current = group.getLayout();
|
|
2049
|
-
if (!(Object.keys(current).length === Object.keys(desiredLayout).length && Object.keys(desiredLayout).every((id) => id in current)) || layoutsMatch(current, desiredLayout)) return;
|
|
2050
|
-
syncing.current = true;
|
|
2051
|
-
group.setLayout(desiredLayout);
|
|
2052
|
-
const frame = requestAnimationFrame(() => {
|
|
2053
|
-
syncing.current = false;
|
|
2054
|
-
});
|
|
2055
|
-
return () => {
|
|
2056
|
-
cancelAnimationFrame(frame);
|
|
2057
|
-
syncing.current = false;
|
|
2058
|
-
};
|
|
2059
|
-
}, [desiredLayout]);
|
|
2060
|
-
const handleLayoutChange = (layout) => {
|
|
2061
|
-
if (snapping.current || syncing.current || !resizableSplits) return;
|
|
2062
|
-
if (effectiveSnap === null || !snapActive) return;
|
|
2063
|
-
const boundary = activeBoundary.current;
|
|
2064
|
-
if (boundary === null) return;
|
|
2065
|
-
const { sizes, snapped } = snapSizes(orderedFromLayout(layout, node.children), boundary, effectiveSnap);
|
|
2066
|
-
setHighlight(snapped ? boundary : null);
|
|
2067
|
-
if (!snapped) {
|
|
2068
|
-
setSnapTransition(false);
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
|
-
setSnapTransition(true);
|
|
2072
|
-
const group = groupRef.current;
|
|
2073
|
-
const target = layoutFromOrdered(sizes, node.children);
|
|
2074
|
-
if (!group || layoutsMatch(layout, target)) return;
|
|
2075
|
-
snapping.current = true;
|
|
2076
|
-
group.setLayout(target);
|
|
2077
|
-
snapping.current = false;
|
|
2078
|
-
};
|
|
2079
|
-
const handleLayoutChanged = (layout, meta) => {
|
|
2080
|
-
if (!meta.isUserInteraction) return;
|
|
2081
|
-
if (syncing.current) return;
|
|
2082
|
-
const boundary = activeBoundary.current;
|
|
2083
|
-
activeBoundary.current = null;
|
|
2084
|
-
setHighlight(null);
|
|
2085
|
-
setSnapTransition(false);
|
|
2086
|
-
if (!resizableSplits) return;
|
|
2087
|
-
let weights = node.children.map((child) => layout[child.id] ?? child.weight ?? 1);
|
|
2088
|
-
if (boundary !== null && effectiveSnap !== null && snapActive) weights = snapSizes(weights, boundary, effectiveSnap).sizes;
|
|
2089
|
-
if (!weights.some((weight, index) => Math.abs(weight - (node.children[index]?.weight ?? 1)) > WEIGHT_EPSILON)) return;
|
|
2090
|
-
dispatch({
|
|
2091
|
-
rowId: node.id,
|
|
2092
|
-
type: "adjustSplit",
|
|
2093
|
-
weights
|
|
2094
|
-
});
|
|
2095
|
-
};
|
|
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
|
+
});
|
|
2096
2293
|
return /* @__PURE__ */ jsx(Group, {
|
|
2097
2294
|
"data-dashfoo": "row",
|
|
2295
|
+
"data-dashfoo-snapping": snappedBoundary === null ? void 0 : "true",
|
|
2098
2296
|
disabled: !resizableSplits,
|
|
2099
|
-
elementRef: groupElement,
|
|
2100
2297
|
groupRef,
|
|
2101
|
-
onLayoutChange: snapActive ?
|
|
2102
|
-
onLayoutChanged
|
|
2298
|
+
onLayoutChange: snapActive ? onLayoutChange : void 0,
|
|
2299
|
+
onLayoutChanged,
|
|
2103
2300
|
orientation,
|
|
2104
2301
|
style: groupStyle,
|
|
2105
2302
|
children: node.children.map((child, index) => {
|
|
2106
|
-
const percent =
|
|
2303
|
+
const percent = child.weight / total * 100;
|
|
2107
2304
|
let min = child.min ? dimensionToSize(child.min) : void 0;
|
|
2108
2305
|
if (min === void 0) {
|
|
2109
2306
|
const minimum = descendantMinSize(child, node.orientation, tabsetMinSize);
|
|
@@ -2115,7 +2312,7 @@ const RowView = ({ node, renderTabset }) => {
|
|
|
2115
2312
|
"data-dashfoo-snapped": snappedBoundary === index - 1 ? "true" : void 0,
|
|
2116
2313
|
disabled: !resizableSplits,
|
|
2117
2314
|
onPointerDown: snapActive && resizableSplits ? () => {
|
|
2118
|
-
|
|
2315
|
+
beginBoundaryDrag(index - 1);
|
|
2119
2316
|
} : void 0
|
|
2120
2317
|
}) : null, /* @__PURE__ */ jsx(Panel$1, {
|
|
2121
2318
|
defaultSize: `${percent}%`,
|
|
@@ -2159,11 +2356,12 @@ const bodyStyle = {
|
|
|
2159
2356
|
minHeight: 0,
|
|
2160
2357
|
position: "relative"
|
|
2161
2358
|
};
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2164
|
-
|
|
2359
|
+
const FLOAT_OVERRIDES = {
|
|
2360
|
+
draggableTabsets: false,
|
|
2361
|
+
floatable: false,
|
|
2362
|
+
maximizable: false,
|
|
2363
|
+
maximizedTabsetId: void 0
|
|
2165
2364
|
};
|
|
2166
|
-
const TAP_SLOP = 4;
|
|
2167
2365
|
const FloatTitleEditor = ({ dispatch, node, onDone }) => {
|
|
2168
2366
|
const inputRef = useRef(null);
|
|
2169
2367
|
const { handleBlur, handleKeyDown } = useInlineRename({
|
|
@@ -2211,25 +2409,10 @@ const FloatTitle = ({ dispatch, node, renamable }) => {
|
|
|
2211
2409
|
children: floatTitle(node)
|
|
2212
2410
|
});
|
|
2213
2411
|
};
|
|
2214
|
-
const FloatPanel = ({
|
|
2412
|
+
const FloatPanel = ({ node, onFocus, zIndex }) => {
|
|
2215
2413
|
const dispatch = useLayout((state) => state.dispatch);
|
|
2216
|
-
const closableTabs = useLayout((state) => state.closableTabs);
|
|
2217
|
-
const draggableTabs = useLayout((state) => state.draggableTabs);
|
|
2218
2414
|
const editable = useLayout((state) => state.editable);
|
|
2219
|
-
const
|
|
2220
|
-
const resizableSplits = useLayout((state) => state.resizableSplits);
|
|
2221
|
-
const keepMounted = useLayout((state) => state.keepMounted);
|
|
2222
|
-
const renderTab = useLayout((state) => state.renderTab);
|
|
2223
|
-
const renderTabLabel = useLayout((state) => state.renderTabLabel);
|
|
2224
|
-
const renderTabsetToolbar = useLayout((state) => state.renderTabsetToolbar);
|
|
2225
|
-
const snap = useLayout((state) => state.snap);
|
|
2226
|
-
const panelRef = useRef(null);
|
|
2227
|
-
const gestureRef = useRef(null);
|
|
2228
|
-
const latestRef = useRef(node.geometry);
|
|
2229
|
-
const movedRef = useRef(false);
|
|
2230
|
-
const setPanel = (element) => {
|
|
2231
|
-
panelRef.current = element;
|
|
2232
|
-
};
|
|
2415
|
+
const minimized = node.minimized === true;
|
|
2233
2416
|
const restore = () => {
|
|
2234
2417
|
if (!editable) return;
|
|
2235
2418
|
dispatch({
|
|
@@ -2238,88 +2421,30 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
|
|
|
2238
2421
|
type: "setFloatMinimized"
|
|
2239
2422
|
});
|
|
2240
2423
|
};
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
edges: edgeKey === void 0 ? null : EDGE_BY_KEY.get(edgeKey) ?? null,
|
|
2255
|
-
pointerId: event.pointerId,
|
|
2256
|
-
start: node.geometry,
|
|
2257
|
-
startX: event.clientX,
|
|
2258
|
-
startY: event.clientY
|
|
2259
|
-
};
|
|
2260
|
-
gestureRef.current = gesture;
|
|
2261
|
-
latestRef.current = node.geometry;
|
|
2262
|
-
if (gesture.edges) panel.setPointerCapture(event.pointerId);
|
|
2263
|
-
};
|
|
2264
|
-
const handlePointerUp = (event) => {
|
|
2265
|
-
const gesture = gestureRef.current;
|
|
2266
|
-
if (!gesture || event.pointerId !== gesture.pointerId) return;
|
|
2267
|
-
gestureRef.current = null;
|
|
2268
|
-
const panel = panelRef.current;
|
|
2269
|
-
if (panel?.hasPointerCapture?.(event.pointerId) === true) panel.releasePointerCapture(event.pointerId);
|
|
2270
|
-
if (!movedRef.current) {
|
|
2271
|
-
if (node.minimized === true) restore();
|
|
2272
|
-
return;
|
|
2273
|
-
}
|
|
2274
|
-
dispatch({
|
|
2275
|
-
floatId: node.id,
|
|
2276
|
-
geometry: latestRef.current,
|
|
2277
|
-
type: "moveFloat"
|
|
2278
|
-
});
|
|
2279
|
-
};
|
|
2280
|
-
const handlePointerMove = (event) => {
|
|
2281
|
-
const gesture = gestureRef.current;
|
|
2282
|
-
const panel = panelRef.current;
|
|
2283
|
-
if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
|
|
2284
|
-
if (event.buttons === 0) {
|
|
2285
|
-
handlePointerUp(event);
|
|
2286
|
-
return;
|
|
2287
|
-
}
|
|
2288
|
-
const dx = event.clientX - gesture.startX;
|
|
2289
|
-
const dy = event.clientY - gesture.startY;
|
|
2290
|
-
if (!movedRef.current) {
|
|
2291
|
-
if (Math.hypot(dx, dy) <= TAP_SLOP) return;
|
|
2292
|
-
movedRef.current = true;
|
|
2293
|
-
if (!gesture.edges) panel.setPointerCapture(gesture.pointerId);
|
|
2294
|
-
}
|
|
2295
|
-
const next = gesture.edges ? resizeRect(gesture.start, gesture.edges, dx, dy) : clampToBounds({
|
|
2296
|
-
...gesture.start,
|
|
2297
|
-
left: gesture.start.left + dx,
|
|
2298
|
-
top: gesture.start.top + dy
|
|
2299
|
-
}, gesture.bounds, node.minimized === true ? CHIP_SIZE : void 0);
|
|
2300
|
-
latestRef.current = next;
|
|
2301
|
-
panel.style.left = `${next.left}px`;
|
|
2302
|
-
panel.style.top = `${next.top}px`;
|
|
2303
|
-
if (node.minimized !== true) {
|
|
2304
|
-
panel.style.width = `${next.width}px`;
|
|
2305
|
-
panel.style.height = `${next.height}px`;
|
|
2306
|
-
}
|
|
2307
|
-
};
|
|
2308
|
-
const handlePointerCancel = (event) => {
|
|
2309
|
-
const gesture = gestureRef.current;
|
|
2310
|
-
const panel = panelRef.current;
|
|
2311
|
-
if (!gesture || !panel || event.pointerId !== gesture.pointerId) return;
|
|
2312
|
-
gestureRef.current = null;
|
|
2313
|
-
if (panel.hasPointerCapture?.(event.pointerId)) panel.releasePointerCapture(event.pointerId);
|
|
2314
|
-
if (!movedRef.current) return;
|
|
2315
|
-
panel.style.left = `${gesture.start.left}px`;
|
|
2316
|
-
panel.style.top = `${gesture.start.top}px`;
|
|
2317
|
-
if (node.minimized !== true) {
|
|
2318
|
-
panel.style.width = `${gesture.start.width}px`;
|
|
2319
|
-
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();
|
|
2320
2437
|
}
|
|
2438
|
+
});
|
|
2439
|
+
const { onPointerDown, ...rootHandlers } = handlers;
|
|
2440
|
+
const frameStyle = {
|
|
2441
|
+
...style,
|
|
2442
|
+
pointerEvents: "auto",
|
|
2443
|
+
position: "absolute",
|
|
2444
|
+
zIndex
|
|
2321
2445
|
};
|
|
2322
|
-
if (
|
|
2446
|
+
if (minimized) return /* @__PURE__ */ jsxs("button", {
|
|
2447
|
+
...rootHandlers,
|
|
2323
2448
|
"aria-label": `Restore ${floatTitle(node)} panel`,
|
|
2324
2449
|
"data-dashfoo": "float-chip",
|
|
2325
2450
|
onKeyDown: (event) => {
|
|
@@ -2328,19 +2453,10 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
|
|
|
2328
2453
|
restore();
|
|
2329
2454
|
}
|
|
2330
2455
|
},
|
|
2331
|
-
|
|
2332
|
-
onPointerDown: handlePointerDown,
|
|
2456
|
+
onPointerDown,
|
|
2333
2457
|
onPointerDownCapture: onFocus,
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
ref: setPanel,
|
|
2337
|
-
style: {
|
|
2338
|
-
left: node.geometry.left,
|
|
2339
|
-
pointerEvents: "auto",
|
|
2340
|
-
position: "absolute",
|
|
2341
|
-
top: node.geometry.top,
|
|
2342
|
-
zIndex
|
|
2343
|
-
},
|
|
2458
|
+
ref,
|
|
2459
|
+
style: frameStyle,
|
|
2344
2460
|
title: floatTitle(node),
|
|
2345
2461
|
type: "button",
|
|
2346
2462
|
children: [/* @__PURE__ */ jsx(FloatIcon, {}), /* @__PURE__ */ jsx("span", {
|
|
@@ -2348,31 +2464,16 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
|
|
|
2348
2464
|
children: floatTitle(node)
|
|
2349
2465
|
})]
|
|
2350
2466
|
});
|
|
2351
|
-
const floatModel = {
|
|
2352
|
-
global,
|
|
2353
|
-
layout: node.layout,
|
|
2354
|
-
version: 1
|
|
2355
|
-
};
|
|
2356
2467
|
return /* @__PURE__ */ jsxs("div", {
|
|
2468
|
+
...rootHandlers,
|
|
2357
2469
|
"data-dashfoo": "float",
|
|
2358
|
-
onPointerCancel: handlePointerCancel,
|
|
2359
2470
|
onPointerDownCapture: onFocus,
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
ref: setPanel,
|
|
2363
|
-
style: {
|
|
2364
|
-
height: node.geometry.height,
|
|
2365
|
-
left: node.geometry.left,
|
|
2366
|
-
pointerEvents: "auto",
|
|
2367
|
-
position: "absolute",
|
|
2368
|
-
top: node.geometry.top,
|
|
2369
|
-
width: node.geometry.width,
|
|
2370
|
-
zIndex
|
|
2371
|
-
},
|
|
2471
|
+
ref,
|
|
2472
|
+
style: frameStyle,
|
|
2372
2473
|
children: [
|
|
2373
2474
|
/* @__PURE__ */ jsxs("div", {
|
|
2374
2475
|
"data-dashfoo": "float-titlebar",
|
|
2375
|
-
onPointerDown
|
|
2476
|
+
onPointerDown,
|
|
2376
2477
|
style: editable ? titleBarStyle : {
|
|
2377
2478
|
...titleBarStyle,
|
|
2378
2479
|
cursor: "default"
|
|
@@ -2427,29 +2528,15 @@ const FloatPanel = ({ global, node, onFocus, zIndex }) => {
|
|
|
2427
2528
|
/* @__PURE__ */ jsx("div", {
|
|
2428
2529
|
"data-dashfoo": "float-body",
|
|
2429
2530
|
style: bodyStyle,
|
|
2430
|
-
children: /* @__PURE__ */ jsx(
|
|
2431
|
-
|
|
2432
|
-
dispatch,
|
|
2433
|
-
draggableTabs,
|
|
2434
|
-
draggableTabsets: false,
|
|
2435
|
-
editable,
|
|
2436
|
-
floatable: false,
|
|
2437
|
-
keepMounted,
|
|
2438
|
-
maximizable: false,
|
|
2439
|
-
model: floatModel,
|
|
2440
|
-
renamableTabs,
|
|
2441
|
-
renderTab,
|
|
2442
|
-
renderTabLabel,
|
|
2443
|
-
renderTabsetToolbar,
|
|
2444
|
-
resizableSplits,
|
|
2445
|
-
snap: snap ?? void 0,
|
|
2531
|
+
children: /* @__PURE__ */ jsx(LayoutOverrides, {
|
|
2532
|
+
overrides: FLOAT_OVERRIDES,
|
|
2446
2533
|
children: /* @__PURE__ */ jsx(DragProvider, { children: /* @__PURE__ */ jsx(RowView, { node: node.layout }) })
|
|
2447
2534
|
})
|
|
2448
2535
|
}),
|
|
2449
2536
|
editable && RESIZE_HANDLES.map((handle) => /* @__PURE__ */ jsx("div", {
|
|
2450
2537
|
"data-dashfoo": "float-resize",
|
|
2451
2538
|
"data-edge": handle.key,
|
|
2452
|
-
onPointerDown
|
|
2539
|
+
onPointerDown,
|
|
2453
2540
|
style: {
|
|
2454
2541
|
position: "absolute",
|
|
2455
2542
|
touchAction: "none",
|
|
@@ -2469,14 +2556,13 @@ const overlayStyle = {
|
|
|
2469
2556
|
position: "fixed",
|
|
2470
2557
|
zIndex: 1
|
|
2471
2558
|
};
|
|
2472
|
-
const FloatOverlay = ({ floats
|
|
2559
|
+
const FloatOverlay = ({ floats }) => {
|
|
2473
2560
|
const [topId, setTopId] = useState(null);
|
|
2474
2561
|
if (floats.length === 0) return null;
|
|
2475
2562
|
return /* @__PURE__ */ jsx("div", {
|
|
2476
2563
|
"data-dashfoo": "float-overlay",
|
|
2477
2564
|
style: overlayStyle,
|
|
2478
2565
|
children: floats.map((node) => /* @__PURE__ */ jsx(FloatPanel, {
|
|
2479
|
-
global,
|
|
2480
2566
|
node,
|
|
2481
2567
|
onFocus: () => {
|
|
2482
2568
|
setTopId(node.id);
|
|
@@ -2485,12 +2571,9 @@ const FloatOverlay = ({ floats, global }) => {
|
|
|
2485
2571
|
}, node.id))
|
|
2486
2572
|
});
|
|
2487
2573
|
};
|
|
2488
|
-
const FloatLayer = ({ children, floats
|
|
2574
|
+
const FloatLayer = ({ children, floats }) => /* @__PURE__ */ jsxs(FloatLayerContext.Provider, {
|
|
2489
2575
|
value: true,
|
|
2490
|
-
children: [children, /* @__PURE__ */ jsx(FloatOverlay, {
|
|
2491
|
-
floats,
|
|
2492
|
-
global
|
|
2493
|
-
})]
|
|
2576
|
+
children: [children, /* @__PURE__ */ jsx(FloatOverlay, { floats })]
|
|
2494
2577
|
});
|
|
2495
2578
|
|
|
2496
2579
|
//#endregion
|
|
@@ -2618,24 +2701,18 @@ const DashfooLayout = forwardRef((props, ref) => {
|
|
|
2618
2701
|
responsive?.orientation,
|
|
2619
2702
|
store.model
|
|
2620
2703
|
]);
|
|
2621
|
-
const storeDispatch = store.dispatch;
|
|
2622
|
-
const dispatch = useCallback((action) => {
|
|
2623
|
-
if (isCompact && action.type === "adjustSplit") return;
|
|
2624
|
-
storeDispatch(action);
|
|
2625
|
-
}, [isCompact, storeDispatch]);
|
|
2626
2704
|
const maximized = view.maximizedTabsetId === void 0 ? void 0 : findTabset(view, view.maximizedTabsetId);
|
|
2627
2705
|
const tree = /* @__PURE__ */ jsx(Layout.FloatLayer, {
|
|
2628
|
-
floats: store.model.floats
|
|
2629
|
-
global: store.model.global,
|
|
2706
|
+
floats: store.model.floats,
|
|
2630
2707
|
children: /* @__PURE__ */ jsx(Layout.DragLayer, { children: maximized ? /* @__PURE__ */ jsx(Layout.Tabset, { node: maximized }) : /* @__PURE__ */ jsx(Layout.Rows, { node: view.layout }) })
|
|
2631
2708
|
});
|
|
2632
2709
|
return /* @__PURE__ */ jsx(Layout.Root, {
|
|
2633
2710
|
closableTabs,
|
|
2634
|
-
dispatch,
|
|
2635
|
-
draggableTabs
|
|
2636
|
-
draggableTabsets
|
|
2711
|
+
dispatch: store.dispatch,
|
|
2712
|
+
draggableTabs,
|
|
2713
|
+
draggableTabsets,
|
|
2637
2714
|
editable,
|
|
2638
|
-
floatable
|
|
2715
|
+
floatable,
|
|
2639
2716
|
keepMounted,
|
|
2640
2717
|
maximizable,
|
|
2641
2718
|
model: view,
|
|
@@ -2643,7 +2720,8 @@ const DashfooLayout = forwardRef((props, ref) => {
|
|
|
2643
2720
|
renderTab,
|
|
2644
2721
|
renderTabLabel,
|
|
2645
2722
|
renderTabsetToolbar,
|
|
2646
|
-
resizableSplits
|
|
2723
|
+
resizableSplits,
|
|
2724
|
+
restructurable: !isCompact,
|
|
2647
2725
|
rootRef: containerRef,
|
|
2648
2726
|
snap,
|
|
2649
2727
|
children: hasSharedManager ? tree : /* @__PURE__ */ jsx(DashfooDragProvider, { children: tree })
|