@dimina-kit/electron-deck 0.1.0-dev.20260702175315 → 0.1.0-dev.20260703051110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -15
- package/dist/client/index.d.ts +8 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/dock-react/dock-view.d.ts +43 -19
- package/dist/dock-react/dock-view.d.ts.map +1 -1
- package/dist/dock-react/drag-redock.d.ts +18 -0
- package/dist/dock-react/drag-redock.d.ts.map +1 -1
- package/dist/dock-react/group-view.d.ts +18 -0
- package/dist/dock-react/group-view.d.ts.map +1 -0
- package/dist/dock-react/index.d.ts +3 -2
- package/dist/dock-react/index.d.ts.map +1 -1
- package/dist/dock-react/index.js +384 -289
- package/dist/dock-react/index.js.map +1 -1
- package/dist/dock-react/panel-body.d.ts +24 -0
- package/dist/dock-react/panel-body.d.ts.map +1 -0
- package/dist/dock-react/split-sizing.d.ts +109 -0
- package/dist/dock-react/split-sizing.d.ts.map +1 -0
- package/dist/dock-react/split-view.d.ts +15 -0
- package/dist/dock-react/split-view.d.ts.map +1 -0
- package/dist/{electron-deck-B9fun93z.js → electron-deck-Bi-u-Aze.js} +175 -79
- package/dist/electron-deck-Bi-u-Aze.js.map +1 -0
- package/dist/electron-deck.d.ts +5 -0
- package/dist/electron-deck.d.ts.map +1 -1
- package/dist/events.d.ts +1 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/deck-app-view-move.d.ts +17 -0
- package/dist/internal/deck-app-view-move.d.ts.map +1 -0
- package/dist/internal/deck-app.d.ts +58 -1
- package/dist/internal/deck-app.d.ts.map +1 -1
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +2 -2
- package/dist/layout/model.d.ts.map +1 -1
- package/dist/layout/mutations.d.ts.map +1 -1
- package/dist/layout/tree-query.d.ts +18 -0
- package/dist/layout/tree-query.d.ts.map +1 -0
- package/dist/layout/types.d.ts +11 -9
- package/dist/layout/types.d.ts.map +1 -1
- package/dist/{layout-ByOiOdnc.js → layout-D-LYJIpM.js} +57 -41
- package/dist/layout-D-LYJIpM.js.map +1 -0
- package/dist/main/compositor.d.ts.map +1 -1
- package/dist/main/index.js +1 -1
- package/dist/main/view-handle.d.ts.map +1 -1
- package/dist/preload/index.cjs +5 -0
- package/dist/preload/index.cjs.map +1 -1
- package/dist/preload/index.d.ts +5 -0
- package/dist/preload/index.d.ts.map +1 -1
- package/dist/preload/index.js +5 -0
- package/dist/preload/index.js.map +1 -1
- package/dist/types.d.ts +32 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/{view-handle-DDhpBuW-.js → view-handle-zav25k3-.js} +141 -86
- package/dist/view-handle-zav25k3-.js.map +1 -0
- package/package.json +11 -5
- package/dist/electron-deck-B9fun93z.js.map +0 -1
- package/dist/layout-ByOiOdnc.js.map +0 -1
- package/dist/view-handle-DDhpBuW-.js.map +0 -1
package/dist/dock-react/index.js
CHANGED
|
@@ -1,9 +1,79 @@
|
|
|
1
|
-
import { a as closePanelForUser, c as extractPanel, d as setActive, m as splitPanel, p as setSizes, u as movePanel } from "../layout-
|
|
1
|
+
import { a as closePanelForUser, c as extractPanel, d as setActive, g as findGroupById, h as countPanels, m as splitPanel, p as setSizes, u as movePanel, v as findPanelGroupId } from "../layout-D-LYJIpM.js";
|
|
2
2
|
import { Fragment, createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Group, Panel, Separator } from "react-resizable-panels";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/dock-react/drag-redock.ts
|
|
6
6
|
/**
|
|
7
|
+
* A zero/negative/non-finite rect has no meaningful edge bands; a non-finite
|
|
8
|
+
* point can't be classified either. Note a non-finite WIDTH/HEIGHT (e.g.
|
|
9
|
+
* Infinity) satisfies `> 0` and would otherwise slip through — `band = ef *
|
|
10
|
+
* min(Infinity, h)` yields a finite band and the point is misclassified as an
|
|
11
|
+
* EDGE zone — so reject finiteness EXPLICITLY (N1).
|
|
12
|
+
*/
|
|
13
|
+
function isDegenerateDropInput(width, height, x, y) {
|
|
14
|
+
return !(width > 0) || !(height > 0) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(x) || !Number.isFinite(y);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Clamp the band fraction to [0, 0.5] (N1): a fraction > 0.5 makes the left
|
|
18
|
+
* and right bands (and top/bottom) OVERLAP, so a point near the right edge of
|
|
19
|
+
* a narrow rect would satisfy BOTH `inLeft` and `inRight` and be misread as
|
|
20
|
+
* `left`. Capping at 0.5 keeps the two bands disjoint.
|
|
21
|
+
*/
|
|
22
|
+
function clampEdgeFraction(edgeFraction) {
|
|
23
|
+
return Number.isFinite(edgeFraction) ? Math.max(0, Math.min(.5, edgeFraction)) : .25;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A pointer dragged past the panel edge has no band membership; clamp it to
|
|
27
|
+
* the nearest edge zone. `undefined` when the point is IN the rect (the
|
|
28
|
+
* caller falls through to band classification). Both axes out (a diagonal
|
|
29
|
+
* corner): the axis with the LARGER overshoot magnitude wins; an exact tie is
|
|
30
|
+
* broken by HORIZONTAL (the x axis).
|
|
31
|
+
*/
|
|
32
|
+
function classifyOutOfRectDropZone(width, height, x, y) {
|
|
33
|
+
const outX = x < 0 ? x : x > width ? x - width : 0;
|
|
34
|
+
const outY = y < 0 ? y : y > height ? y - height : 0;
|
|
35
|
+
if (outX === 0 && outY === 0) return void 0;
|
|
36
|
+
const magX = Math.abs(outX);
|
|
37
|
+
if (magX >= Math.abs(outY) && magX > 0) return outX < 0 ? "left" : "right";
|
|
38
|
+
return outY < 0 ? "top" : "bottom";
|
|
39
|
+
}
|
|
40
|
+
function computeBandMembership(width, height, x, y, ef) {
|
|
41
|
+
const band = ef * Math.min(width, height);
|
|
42
|
+
const inLeft = x < band;
|
|
43
|
+
const inRight = x > width - band;
|
|
44
|
+
const inTop = y < band;
|
|
45
|
+
const inBottom = y > height - band;
|
|
46
|
+
return {
|
|
47
|
+
inLeft,
|
|
48
|
+
inTop,
|
|
49
|
+
activeHoriz: inLeft || inRight,
|
|
50
|
+
activeVert: inTop || inBottom
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Corner tie-break (both axes active): pick the edge with the SMALLER
|
|
55
|
+
* normalized distance. Only the ACTIVE horizontal edge and ACTIVE vertical
|
|
56
|
+
* edge can compete (left vs right and top vs bottom can't both be active for
|
|
57
|
+
* a sane rect). On an exact tie, HORIZONTAL wins (`<=` on the horizontal
|
|
58
|
+
* distance).
|
|
59
|
+
*/
|
|
60
|
+
function cornerTieBreak(width, height, x, y, inLeft, inTop) {
|
|
61
|
+
if ((inLeft ? x / width : (width - x) / width) <= (inTop ? y / height : (height - y) / height)) return inLeft ? "left" : "right";
|
|
62
|
+
return inTop ? "top" : "bottom";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Classify an IN-RECT point by edge-band membership. No band => interior
|
|
66
|
+
* (`center`, the tab-join zone). One active band => that edge directly. Both
|
|
67
|
+
* axes active (a corner) => {@link cornerTieBreak}.
|
|
68
|
+
*/
|
|
69
|
+
function classifyInRectDropZone(width, height, x, y, ef) {
|
|
70
|
+
const { inLeft, inTop, activeHoriz, activeVert } = computeBandMembership(width, height, x, y, ef);
|
|
71
|
+
if (!activeHoriz && !activeVert) return "center";
|
|
72
|
+
if (activeHoriz && !activeVert) return inLeft ? "left" : "right";
|
|
73
|
+
if (activeVert && !activeHoriz) return inTop ? "top" : "bottom";
|
|
74
|
+
return cornerTieBreak(width, height, x, y, inLeft, inTop);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
7
77
|
* Classify a pointer position (RELATIVE to the rect's top-left; `(0,0)` is the
|
|
8
78
|
* corner) into a drop zone.
|
|
9
79
|
*
|
|
@@ -18,29 +88,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
18
88
|
function computeDropZone(rect, point, edgeFraction = .25) {
|
|
19
89
|
const { width, height } = rect;
|
|
20
90
|
const { x, y } = point;
|
|
21
|
-
if (
|
|
22
|
-
const ef =
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const magX = Math.abs(outX);
|
|
27
|
-
if (magX >= Math.abs(outY)) {
|
|
28
|
-
if (magX > 0) return outX < 0 ? "left" : "right";
|
|
29
|
-
}
|
|
30
|
-
return outY < 0 ? "top" : "bottom";
|
|
31
|
-
}
|
|
32
|
-
const band = ef * Math.min(width, height);
|
|
33
|
-
const inLeft = x < band;
|
|
34
|
-
const inRight = x > width - band;
|
|
35
|
-
const inTop = y < band;
|
|
36
|
-
const inBottom = y > height - band;
|
|
37
|
-
const activeHoriz = inLeft || inRight;
|
|
38
|
-
const activeVert = inTop || inBottom;
|
|
39
|
-
if (!activeHoriz && !activeVert) return "center";
|
|
40
|
-
if (activeHoriz && !activeVert) return inLeft ? "left" : "right";
|
|
41
|
-
if (activeVert && !activeHoriz) return inTop ? "top" : "bottom";
|
|
42
|
-
if ((inLeft ? x / width : (width - x) / width) <= (inTop ? y / height : (height - y) / height)) return inLeft ? "left" : "right";
|
|
43
|
-
return inTop ? "top" : "bottom";
|
|
91
|
+
if (isDegenerateDropInput(width, height, x, y)) return "center";
|
|
92
|
+
const ef = clampEdgeFraction(edgeFraction);
|
|
93
|
+
const outOfRectZone = classifyOutOfRectDropZone(width, height, x, y);
|
|
94
|
+
if (outOfRectZone !== void 0) return outOfRectZone;
|
|
95
|
+
return classifyInRectDropZone(width, height, x, y, ef);
|
|
44
96
|
}
|
|
45
97
|
/**
|
|
46
98
|
* Translate a drop zone into an engine-neutral re-dock descriptor.
|
|
@@ -107,55 +159,44 @@ function computeReorderIndex(tabRects, pointerX) {
|
|
|
107
159
|
else break;
|
|
108
160
|
return index;
|
|
109
161
|
}
|
|
110
|
-
//#endregion
|
|
111
|
-
//#region src/dock-react/dock-view.tsx
|
|
112
162
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* Pure function of the current model snapshot (+ registry + the two host
|
|
116
|
-
* callbacks). It owns NO layout state: it subscribes to the `LayoutModel`,
|
|
117
|
-
* keeps the latest tree in component state, and re-renders on every emission.
|
|
118
|
-
* All structural targeting is via STABLE `data-*` attributes — see the
|
|
119
|
-
* contract doc-block in `dock-view.test.tsx`.
|
|
163
|
+
* Translate a tab strip's VISIBLE-tab drop index into the insertion index
|
|
164
|
+
* `movePanel`'s same-group reorder expects.
|
|
120
165
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*
|
|
126
|
-
* custom type (vs `text/plain`) keeps deck drags from being confused with
|
|
127
|
-
* arbitrary text drops; the panel id also stays recoverable from the source
|
|
128
|
-
* tab's `data-deck-tab` attribute (the jsdom seam relies on the latter).
|
|
129
|
-
*/
|
|
130
|
-
var DRAG_PANEL_MIME = "application/x-deck-panel";
|
|
131
|
-
var activeDragPanelId = null;
|
|
132
|
-
/**
|
|
133
|
-
* The current layout EPOCH — the model's revision, bumped once per committed
|
|
134
|
-
* mutation. `DockView` provides it; descendant panels read it via
|
|
135
|
-
* `useDockLayoutEpoch`.
|
|
166
|
+
* `computeReorderIndex` reports how many VISIBLE-tab midpoints the pointer has
|
|
167
|
+
* passed (0..`visibleTabIds.length`), COUNTING the dragged tab's own midpoint and
|
|
168
|
+
* measured over rects that OMIT `hideTab` panels. `movePanel` inserts into
|
|
169
|
+
* `panels.filter(p => p !== dragged)` — a different coordinate space. Two shifts
|
|
170
|
+
* reconcile them:
|
|
136
171
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* at its old position. The epoch is the layout layer's explicit "something
|
|
144
|
-
* moved" signal: a panel hosting a native overlay re-measures (e.g. pulses its
|
|
145
|
-
* view-anchor) when the epoch changes, catching the translate the browser never
|
|
146
|
-
* reports. Default 0 (outside a provider): the consuming effect runs once on
|
|
147
|
-
* mount, which is harmless.
|
|
148
|
-
*/
|
|
149
|
-
var LayoutEpochContext = createContext(0);
|
|
150
|
-
/**
|
|
151
|
-
* Read the current dock layout epoch (the model revision). Re-renders the caller
|
|
152
|
-
* whenever a layout mutation commits. Keyed into a panel's effect deps, it lets
|
|
153
|
-
* a native-overlay host re-measure on a reorder that fires no geometry event.
|
|
154
|
-
* Returns 0 when used outside a `<DockView>`.
|
|
172
|
+
* 1. Once the pointer passes the dragged tab's OWN midpoint the strip index has
|
|
173
|
+
* counted the dragged slot, so drop it back out (−1) to get the insertion
|
|
174
|
+
* slot among the OTHER visible tabs.
|
|
175
|
+
* 2. Map that visible slot onto the full `panels` order (which may carry hidden
|
|
176
|
+
* tabs the strip never measured): insert before whichever visible tab now
|
|
177
|
+
* occupies the slot, or append when the slot is past the last visible tab.
|
|
155
178
|
*/
|
|
156
|
-
function
|
|
157
|
-
|
|
179
|
+
function resolveReorderInsertIndex(panels, visibleTabIds, draggedPanelId, stripInsertIndex) {
|
|
180
|
+
const draggedVisibleIndex = visibleTabIds.indexOf(draggedPanelId);
|
|
181
|
+
const passedOwnMidpoint = draggedVisibleIndex >= 0 && stripInsertIndex > draggedVisibleIndex;
|
|
182
|
+
const filteredVisible = visibleTabIds.filter((id) => id !== draggedPanelId);
|
|
183
|
+
const filteredPanels = panels.filter((id) => id !== draggedPanelId);
|
|
184
|
+
let visibleInsert = passedOwnMidpoint ? stripInsertIndex - 1 : stripInsertIndex;
|
|
185
|
+
if (visibleInsert < 0) visibleInsert = 0;
|
|
186
|
+
if (visibleInsert > filteredVisible.length) visibleInsert = filteredVisible.length;
|
|
187
|
+
if (visibleInsert >= filteredVisible.length) return filteredPanels.length;
|
|
188
|
+
const anchor = filteredVisible[visibleInsert];
|
|
189
|
+
const anchorIndex = filteredPanels.indexOf(anchor);
|
|
190
|
+
return anchorIndex >= 0 ? anchorIndex : filteredPanels.length;
|
|
158
191
|
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/dock-react/split-sizing.ts
|
|
194
|
+
/** Epsilon (in percentage points) within which two split layouts are treated as
|
|
195
|
+
* equivalent. Guards BOTH sides of the model↔view loop: we skip pushing a
|
|
196
|
+
* `setLayout` when the live layout already matches the target, and skip the
|
|
197
|
+
* `onLayoutChanged` write-back when the incoming layout matches the model — so
|
|
198
|
+
* `setLayout`→`onLayoutChanged`→write-back→re-sync cannot loop. */
|
|
199
|
+
var LAYOUT_EPSILON = .5;
|
|
159
200
|
/** Normalize raw split weights to percentages summing ~100 for `defaultSize`. */
|
|
160
201
|
function toPercentages(sizes) {
|
|
161
202
|
const total = sizes.reduce((a, b) => a + (b > 0 ? b : 0), 0);
|
|
@@ -181,146 +222,6 @@ function computeFlexiblePercentages(sizes, constraints) {
|
|
|
181
222
|
});
|
|
182
223
|
return out;
|
|
183
224
|
}
|
|
184
|
-
function DockView(props) {
|
|
185
|
-
const { model, registry, renderDomPanel, bindNativeSlot, suppressReorderOnlyDropIndicator, onActiveTabClick } = props;
|
|
186
|
-
const [tree, setTree] = useState(() => model.get());
|
|
187
|
-
const [epoch, setEpoch] = useState(0);
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
setTree(model.get());
|
|
190
|
-
return model.subscribe((snap) => {
|
|
191
|
-
setTree(snap.tree);
|
|
192
|
-
setEpoch(snap.revision);
|
|
193
|
-
});
|
|
194
|
-
}, [model]);
|
|
195
|
-
const handleActivate = useCallback((groupId, panelId) => {
|
|
196
|
-
model.apply((t) => setActive(t, groupId, panelId));
|
|
197
|
-
}, [model]);
|
|
198
|
-
const handleClose = useCallback((panelId) => {
|
|
199
|
-
model.apply((t) => closePanelForUser(t, panelId, registry));
|
|
200
|
-
}, [model, registry]);
|
|
201
|
-
const canClose = countPanels(tree.root) > 1;
|
|
202
|
-
const isPanelInTree = useCallback((panelId) => findPanelGroupId(tree.root, panelId) !== void 0, [tree]);
|
|
203
|
-
const handleApplyLayout = useCallback((splitId, weights) => {
|
|
204
|
-
model.apply((t) => setSizes(t, splitId, weights));
|
|
205
|
-
}, [model]);
|
|
206
|
-
const handleRedock = useCallback((groupId, activePanelId, draggedPanelId, zone, reorderIndex) => {
|
|
207
|
-
const target = {
|
|
208
|
-
groupId,
|
|
209
|
-
panelId: activePanelId
|
|
210
|
-
};
|
|
211
|
-
const draggedGroupId = findPanelGroupId(model.get().root, draggedPanelId);
|
|
212
|
-
if (registry.get(draggedPanelId)?.draggable === false) return;
|
|
213
|
-
if (registry.get(activePanelId)?.draggable === false) return;
|
|
214
|
-
if (registry.get(draggedPanelId)?.dropPolicy === "reorder-only") {
|
|
215
|
-
if (draggedGroupId === void 0 || draggedGroupId !== groupId) return;
|
|
216
|
-
if (zone !== "center") return;
|
|
217
|
-
const group = findGroupById(model.get().root, groupId);
|
|
218
|
-
const index = reorderIndex ?? (group ? group.panels.indexOf(activePanelId) : void 0);
|
|
219
|
-
model.apply((t) => movePanel(t, draggedPanelId, {
|
|
220
|
-
groupId,
|
|
221
|
-
index
|
|
222
|
-
}));
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (isNoopRedock(draggedPanelId, draggedGroupId, target, zone)) return;
|
|
226
|
-
const mutation = dropZoneToMutation(zone, draggedPanelId, target);
|
|
227
|
-
if (mutation.kind === "move") {
|
|
228
|
-
model.apply((t) => movePanel(t, mutation.panelId, { groupId: mutation.destGroupId }));
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
model.apply((t) => {
|
|
232
|
-
const { tree } = extractPanel(t, mutation.newPanelId);
|
|
233
|
-
if (findPanelGroupId(tree.root, mutation.atPanelId) === void 0) return t;
|
|
234
|
-
return splitPanel(tree, mutation.atPanelId, mutation.dir, mutation.newPanelId, mutation.side);
|
|
235
|
-
});
|
|
236
|
-
}, [model, registry]);
|
|
237
|
-
return /* @__PURE__ */ jsx(LayoutEpochContext.Provider, {
|
|
238
|
-
value: epoch,
|
|
239
|
-
children: renderNode(tree.root, {
|
|
240
|
-
registry,
|
|
241
|
-
renderDomPanel,
|
|
242
|
-
bindNativeSlot,
|
|
243
|
-
onActivate: handleActivate,
|
|
244
|
-
onActiveTabClick,
|
|
245
|
-
onApplyLayout: handleApplyLayout,
|
|
246
|
-
onRedock: handleRedock,
|
|
247
|
-
onClose: handleClose,
|
|
248
|
-
canClose,
|
|
249
|
-
isPanelInTree,
|
|
250
|
-
suppressReorderOnlyDropIndicator: suppressReorderOnlyDropIndicator ?? false
|
|
251
|
-
})
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
/** The id of the tab group currently holding `panelId`, or `undefined` if the
|
|
255
|
-
* panel is not in the tree. Used by `handleRedock` to detect a drop back into
|
|
256
|
-
* the dragged panel's own group (M2) and to guard a vanished split anchor (M1). */
|
|
257
|
-
function findPanelGroupId(node, panelId) {
|
|
258
|
-
if (node.kind === "tabs") return node.panels.includes(panelId) ? node.id : void 0;
|
|
259
|
-
for (const child of node.children) {
|
|
260
|
-
const found = findPanelGroupId(child, panelId);
|
|
261
|
-
if (found !== void 0) return found;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
/** The tab-group node with `groupId`, or `undefined`. Used by `handleRedock` to
|
|
265
|
-
* read the live `panels` order of a reorder target (to anchor a same-group
|
|
266
|
-
* reorder on the active panel's current index when the gesture carries no
|
|
267
|
-
* pointer-derived index — e.g. the `__deckHandleDrop` test seam). */
|
|
268
|
-
function findGroupById(node, groupId) {
|
|
269
|
-
if (node.kind === "tabs") return node.id === groupId ? node : void 0;
|
|
270
|
-
for (const child of node.children) {
|
|
271
|
-
const found = findGroupById(child, groupId);
|
|
272
|
-
if (found !== void 0) return found;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
/** Total panels anywhere in the tree. Drives the last-panel close suppression:
|
|
276
|
-
* a GroupView only knows its own node, so DockView computes this global count
|
|
277
|
-
* once and threads the resulting `canClose` boolean down. */
|
|
278
|
-
function countPanels(node) {
|
|
279
|
-
if (node.kind === "tabs") return node.panels.length;
|
|
280
|
-
return node.children.reduce((sum, child) => sum + countPanels(child), 0);
|
|
281
|
-
}
|
|
282
|
-
function renderNode(node, ctx) {
|
|
283
|
-
return node.kind === "split" ? renderSplit(node, ctx) : renderGroup(node, ctx);
|
|
284
|
-
}
|
|
285
|
-
/** A split is a stateful component (it holds the rrp Group imperative ref + runs
|
|
286
|
-
* the M1 model→view sync effect), so render it via JSX with a STABLE key so
|
|
287
|
-
* React preserves that ref / its kept-alive subtree across model emissions
|
|
288
|
-
* rather than remounting on every snapshot. Mirrors `renderGroup`/`GroupView`. */
|
|
289
|
-
function renderSplit(node, ctx) {
|
|
290
|
-
return /* @__PURE__ */ jsx(SplitView, {
|
|
291
|
-
node,
|
|
292
|
-
ctx
|
|
293
|
-
}, node.id);
|
|
294
|
-
}
|
|
295
|
-
/** Epsilon (in percentage points) within which two split layouts are treated as
|
|
296
|
-
* equivalent. Guards BOTH sides of the model↔view loop: we skip pushing a
|
|
297
|
-
* `setLayout` when the live layout already matches the target, and skip the
|
|
298
|
-
* `onLayoutChanged` write-back when the incoming layout matches the model — so
|
|
299
|
-
* `setLayout`→`onLayoutChanged`→write-back→re-sync cannot loop. */
|
|
300
|
-
var LAYOUT_EPSILON = .5;
|
|
301
|
-
/** TIGHT tolerance (percentage points) for the BASIS-NORMALIZED flexible-ratio
|
|
302
|
-
* compare in `handleLayoutChanged`. We normalize the incoming layout's flexible
|
|
303
|
-
* subset to ratios summing to 100 and compare them against the model's flexible
|
|
304
|
-
* ratios (`computeFlexiblePercentages`, also summing to 100). If they match
|
|
305
|
-
* within this tolerance the `onLayoutChanged` is either our own `setLayout` echo
|
|
306
|
-
* OR a ratio-preserving spontaneous re-measure (mount / fixed-px re-pin /
|
|
307
|
-
* container resize) — SKIP the write-back. If they differ it is a genuine user
|
|
308
|
-
* resize (pointer OR keyboard) — WRITE BACK.
|
|
309
|
-
*
|
|
310
|
-
* Set to ~0.1pp: large enough to absorb rrp's ~3-decimal float noise on an echo,
|
|
311
|
-
* yet FAR below a real drag's delta. R1's "sub-0.5%" drag moves a panel ~0.33pp
|
|
312
|
-
* of the container; normalized over the two-flexible-child subset that is ~0.66pp
|
|
313
|
-
* of ratio — comfortably above 0.1, so it is NOT mistaken for an echo and is
|
|
314
|
-
* written back.
|
|
315
|
-
*
|
|
316
|
-
* CAVEAT: this is a flexible-RATIO tolerance (the flexible subset normalized to
|
|
317
|
-
* sum 100), NOT a container-%. It is safe against rrp's 3-decimal echo noise.
|
|
318
|
-
* In a pathologically WIDE split (~≥10 flexible children) a single arrow-key
|
|
319
|
-
* nudge (±~5% of the container on one child) can, once normalized over many
|
|
320
|
-
* flexible siblings, fall BELOW 0.1pp of ratio and be skipped — exotic and
|
|
321
|
-
* self-healing (the next, larger resize writes back). If that ever matters,
|
|
322
|
-
* scale the tolerance down by the flexible child count. */
|
|
323
|
-
var FLEX_RATIO_TOLERANCE = .1;
|
|
324
225
|
/**
|
|
325
226
|
* The minimum weight a FLEXIBLE child may hold, given how many flexible children
|
|
326
227
|
* share the split (Bug #1). A flexible `<Panel>` has no rrp `minSize` by default
|
|
@@ -388,38 +289,48 @@ function layoutsEquivalent(a, b, ids, epsilon = LAYOUT_EPSILON) {
|
|
|
388
289
|
*/
|
|
389
290
|
function buildSetLayoutMap(childIds, sizes, constraints, live) {
|
|
390
291
|
const isFixedAt = (i) => (constraints?.[i] ?? null) !== null;
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (!isFixedAt(i)) continue;
|
|
394
|
-
const livePct = live[childIds[i]];
|
|
395
|
-
if (typeof livePct !== "number") return null;
|
|
396
|
-
fixedTotal += livePct;
|
|
397
|
-
}
|
|
292
|
+
const fixedTotal = sumFixedLivePercent(childIds, isFixedAt, live);
|
|
293
|
+
if (fixedTotal === null) return null;
|
|
398
294
|
const remaining = Math.max(0, 100 - fixedTotal);
|
|
399
295
|
let flexWeightTotal = 0;
|
|
296
|
+
let flexibleCount = 0;
|
|
400
297
|
for (let i = 0; i < childIds.length; i++) {
|
|
401
298
|
if (isFixedAt(i)) continue;
|
|
402
299
|
const w = sizes[i] ?? 0;
|
|
403
300
|
flexWeightTotal += w > 0 ? w : 0;
|
|
301
|
+
flexibleCount += 1;
|
|
404
302
|
}
|
|
405
|
-
const flexibleCount = childIds.length - childIds.filter((_, i) => isFixedAt(i)).length;
|
|
406
303
|
const out = {};
|
|
407
304
|
for (let i = 0; i < childIds.length; i++) {
|
|
408
305
|
const id = childIds[i];
|
|
409
|
-
|
|
410
|
-
out[id] = live[id];
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
if (flexWeightTotal <= 0) {
|
|
414
|
-
out[id] = flexibleCount > 0 ? remaining / flexibleCount : remaining;
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
const w = sizes[i] ?? 0;
|
|
418
|
-
out[id] = remaining * (w > 0 ? w : 0) / flexWeightTotal;
|
|
306
|
+
out[id] = isFixedAt(i) ? live[id] : flexibleShare(sizes[i] ?? 0, remaining, flexWeightTotal, flexibleCount);
|
|
419
307
|
}
|
|
420
308
|
return out;
|
|
421
309
|
}
|
|
422
310
|
/**
|
|
311
|
+
* Sum the fixed children's CURRENT live percentages (preserving their px
|
|
312
|
+
* lock). Null when a fixed child has no measured live % — the caller cannot
|
|
313
|
+
* build a faithful map then.
|
|
314
|
+
*/
|
|
315
|
+
function sumFixedLivePercent(childIds, isFixedAt, live) {
|
|
316
|
+
let fixedTotal = 0;
|
|
317
|
+
for (let i = 0; i < childIds.length; i++) {
|
|
318
|
+
if (!isFixedAt(i)) continue;
|
|
319
|
+
const livePct = live[childIds[i]];
|
|
320
|
+
if (typeof livePct !== "number") return null;
|
|
321
|
+
fixedTotal += livePct;
|
|
322
|
+
}
|
|
323
|
+
return fixedTotal;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* One flexible child's percentage share of the `remaining` space. A degenerate
|
|
327
|
+
* weight pool (total ≤ 0) splits the remaining space evenly instead.
|
|
328
|
+
*/
|
|
329
|
+
function flexibleShare(weight, remaining, flexWeightTotal, flexibleCount) {
|
|
330
|
+
if (flexWeightTotal <= 0) return flexibleCount > 0 ? remaining / flexibleCount : remaining;
|
|
331
|
+
return remaining * (weight > 0 ? weight : 0) / flexWeightTotal;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
423
334
|
* Extract an rrp `onLayoutChanged` map's FLEXIBLE subset (skipping fixed-px
|
|
424
335
|
* children) in child order and NORMALIZE it by its own sum to ratios summing to
|
|
425
336
|
* ~100 — the basis `computeFlexiblePercentages` already produces for the model,
|
|
@@ -444,6 +355,14 @@ function incomingFlexRatios(childIds, constraints, layout) {
|
|
|
444
355
|
ratios: toPercentages(raw)
|
|
445
356
|
};
|
|
446
357
|
}
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/dock-react/split-view.tsx
|
|
360
|
+
/**
|
|
361
|
+
* `SplitView` — one split node rendered as a react-resizable-panels Group, plus
|
|
362
|
+
* the model↔view sync machine (M1): it pushes the model's flexible weights into
|
|
363
|
+
* the live Group via `setLayout` and writes a genuine user resize back through
|
|
364
|
+
* `ctx.onApplyLayout`. The split-sizing arithmetic lives in `./split-sizing`.
|
|
365
|
+
*/
|
|
447
366
|
function SplitView(props) {
|
|
448
367
|
const { node, ctx } = props;
|
|
449
368
|
const orientation = node.orientation === "row" ? "horizontal" : "vertical";
|
|
@@ -494,7 +413,7 @@ function SplitView(props) {
|
|
|
494
413
|
if (!incoming) return;
|
|
495
414
|
const modelPctByIndex = computeFlexiblePercentages(cur.sizes, cur.constraints);
|
|
496
415
|
const modelNorm = incoming.indices.map((i) => modelPctByIndex.get(i) ?? 0);
|
|
497
|
-
if (!incoming.ratios.some((r, k) => Math.abs(r - modelNorm[k]) >
|
|
416
|
+
if (!incoming.ratios.some((r, k) => Math.abs(r - modelNorm[k]) > .1)) return;
|
|
498
417
|
const weights = curChildIds.map((id, i) => isFixedAt(i) ? cur.sizes[i] ?? 1 : layout[id]);
|
|
499
418
|
ctx.onApplyLayout(cur.id, clampFlexibleWeights(weights, cur.constraints));
|
|
500
419
|
};
|
|
@@ -558,12 +477,85 @@ function SplitView(props) {
|
|
|
558
477
|
function panelKey(node) {
|
|
559
478
|
return node.id;
|
|
560
479
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/dock-react/panel-body.tsx
|
|
482
|
+
/**
|
|
483
|
+
* Tab-group body rendering under DOM-panel keepalive: `renderActiveBody` (the
|
|
484
|
+
* body region) + `NativeSlot` (the anchor for an active native panel).
|
|
485
|
+
*/
|
|
486
|
+
/**
|
|
487
|
+
* Render a tab group's body region under DOM-panel KEEPALIVE.
|
|
488
|
+
*
|
|
489
|
+
* - DOM panels: ALL of the group's DOM panels are mounted SIMULTANEOUSLY, each
|
|
490
|
+
* under a STABLE React key (`dom-${panelId}`) so switching the active tab never
|
|
491
|
+
* remounts a body — React state + scroll persist across A→B→A. The active body
|
|
492
|
+
* fills the region (flex:1); inactive ones stay in the DOM but `display:none`.
|
|
493
|
+
* Each body's host renderer receives `{ active }` and is re-invoked with the
|
|
494
|
+
* new flag on every activation change (no remount), so a host can fire
|
|
495
|
+
* on-activation side effects off the false→true edge.
|
|
496
|
+
* - Native panels: EXEMPT from keepalive. The single ACTIVE native panel mounts a
|
|
497
|
+
* `NativeSlot` (keyed on the active id, so deactivation unmounts it and fires
|
|
498
|
+
* `bindNativeSlot(id, null)`); inactive native panels render nothing. Keeping a
|
|
499
|
+
* bound native slot mounted-but-hidden would collapse its WebContentsView rect.
|
|
500
|
+
*/
|
|
501
|
+
function renderActiveBody(node, ctx) {
|
|
502
|
+
const activeId = node.active;
|
|
503
|
+
if (!activeId) return null;
|
|
504
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [ctx.registry.get(activeId)?.kind === "native" ? /* @__PURE__ */ jsx(NativeSlot, {
|
|
505
|
+
panelId: activeId,
|
|
506
|
+
bindNativeSlot: ctx.bindNativeSlot
|
|
507
|
+
}, activeId) : null, node.panels.filter((panelId) => ctx.registry.get(panelId)?.kind !== "native").slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((panelId) => {
|
|
508
|
+
const active = panelId === activeId;
|
|
509
|
+
return /* @__PURE__ */ jsx("div", {
|
|
510
|
+
"data-deck-panel-body": panelId,
|
|
511
|
+
style: {
|
|
512
|
+
display: active ? "flex" : "none",
|
|
513
|
+
flexDirection: "column",
|
|
514
|
+
flex: 1,
|
|
515
|
+
minWidth: 0,
|
|
516
|
+
minHeight: 0
|
|
517
|
+
},
|
|
518
|
+
children: ctx.renderDomPanel(panelId, { active })
|
|
519
|
+
}, `dom-${panelId}`);
|
|
520
|
+
})] });
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Empty anchor slot for an ACTIVE native panel. A ref-callback binds the live
|
|
524
|
+
* element on mount and unbinds (`null`) on unmount. Keying the element on the
|
|
525
|
+
* active panel id (in the parent) guarantees deactivation unmounts this slot —
|
|
526
|
+
* firing the `null` cleanup — and re-activation mounts a fresh one, re-binding.
|
|
527
|
+
*/
|
|
528
|
+
function NativeSlot(props) {
|
|
529
|
+
const { panelId, bindNativeSlot } = props;
|
|
530
|
+
const bindRef = useRef(bindNativeSlot);
|
|
531
|
+
bindRef.current = bindNativeSlot;
|
|
532
|
+
return /* @__PURE__ */ jsx("div", {
|
|
533
|
+
ref: useCallback((el) => {
|
|
534
|
+
bindRef.current(panelId, el);
|
|
535
|
+
}, [panelId]),
|
|
536
|
+
"data-deck-native-slot": panelId,
|
|
537
|
+
style: {
|
|
538
|
+
flex: 1,
|
|
539
|
+
minWidth: 0,
|
|
540
|
+
minHeight: 0,
|
|
541
|
+
height: "100%"
|
|
542
|
+
}
|
|
543
|
+
});
|
|
566
544
|
}
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/dock-react/group-view.tsx
|
|
547
|
+
/**
|
|
548
|
+
* `GroupView` — one tab GROUP: the draggable tab strip, the active body, the
|
|
549
|
+
* imperative `__deckHandleDrop` seam, and a geometry-driven drop indicator while
|
|
550
|
+
* a drag hovers. Owns all tab drag/drop handlers.
|
|
551
|
+
*/
|
|
552
|
+
/**
|
|
553
|
+
* The dataTransfer MIME under which a drag carries the dragged panel id. A
|
|
554
|
+
* custom type (vs `text/plain`) keeps deck drags from being confused with
|
|
555
|
+
* arbitrary text drops; the panel id also stays recoverable from the source
|
|
556
|
+
* tab's `data-deck-tab` attribute (the jsdom seam relies on the latter).
|
|
557
|
+
*/
|
|
558
|
+
var DRAG_PANEL_MIME = "application/x-deck-panel";
|
|
567
559
|
/**
|
|
568
560
|
* One tab GROUP: tab strip (draggable tabs) + active body + the imperative
|
|
569
561
|
* `__deckHandleDrop` seam + a geometry-driven drop indicator while a drag hovers.
|
|
@@ -582,8 +574,10 @@ function GroupView(props) {
|
|
|
582
574
|
};
|
|
583
575
|
};
|
|
584
576
|
const handleDragOver = (e) => {
|
|
577
|
+
if (!e.dataTransfer?.types.includes(DRAG_PANEL_MIME)) return;
|
|
585
578
|
e.preventDefault();
|
|
586
|
-
|
|
579
|
+
const draggedId = ctx.activeDragPanelId.current;
|
|
580
|
+
if (ctx.suppressReorderOnlyDropIndicator && draggedId !== null && ctx.registry.get(draggedId)?.dropPolicy === "reorder-only") {
|
|
587
581
|
setDropZone(null);
|
|
588
582
|
return;
|
|
589
583
|
}
|
|
@@ -610,7 +604,7 @@ function GroupView(props) {
|
|
|
610
604
|
y: e.clientY - rect.top
|
|
611
605
|
});
|
|
612
606
|
setDropZone(null);
|
|
613
|
-
const dragged = e.dataTransfer
|
|
607
|
+
const dragged = e.dataTransfer?.getData(DRAG_PANEL_MIME);
|
|
614
608
|
if (!dragged || ctx.registry.get(dragged) === void 0 || !ctx.isPanelInTree(dragged)) return;
|
|
615
609
|
const reorderIndex = computeReorderIndex(Array.from(e.currentTarget.querySelectorAll("[data-deck-tab]")).map((el) => {
|
|
616
610
|
const r = el.getBoundingClientRect();
|
|
@@ -622,7 +616,7 @@ function GroupView(props) {
|
|
|
622
616
|
ctx.onRedock(node.id, node.active, dragged, zone, reorderIndex);
|
|
623
617
|
};
|
|
624
618
|
const handleTabStripDragOver = (e) => {
|
|
625
|
-
if (!e.dataTransfer
|
|
619
|
+
if (!e.dataTransfer?.types.includes(DRAG_PANEL_MIME)) return;
|
|
626
620
|
e.preventDefault();
|
|
627
621
|
e.stopPropagation();
|
|
628
622
|
setDropZone(null);
|
|
@@ -631,7 +625,7 @@ function GroupView(props) {
|
|
|
631
625
|
e.preventDefault();
|
|
632
626
|
e.stopPropagation();
|
|
633
627
|
setDropZone(null);
|
|
634
|
-
const dragged = e.dataTransfer
|
|
628
|
+
const dragged = e.dataTransfer?.getData(DRAG_PANEL_MIME);
|
|
635
629
|
if (!dragged || ctx.registry.get(dragged) === void 0 || !ctx.isPanelInTree(dragged)) return;
|
|
636
630
|
const reorderIndex = computeReorderIndex(Array.from(e.currentTarget.querySelectorAll("[data-deck-tab]")).map((el) => {
|
|
637
631
|
const r = el.getBoundingClientRect();
|
|
@@ -682,10 +676,10 @@ function GroupView(props) {
|
|
|
682
676
|
e.dataTransfer.setData(DRAG_PANEL_MIME, panelId);
|
|
683
677
|
e.dataTransfer.setData("text/plain", panelId);
|
|
684
678
|
e.dataTransfer.effectAllowed = "move";
|
|
685
|
-
activeDragPanelId = panelId;
|
|
679
|
+
ctx.activeDragPanelId.current = panelId;
|
|
686
680
|
},
|
|
687
681
|
onDragEnd: () => {
|
|
688
|
-
activeDragPanelId = null;
|
|
682
|
+
ctx.activeDragPanelId.current = null;
|
|
689
683
|
},
|
|
690
684
|
onClick: () => {
|
|
691
685
|
if (!active) ctx.onActivate(node.id, panelId);
|
|
@@ -777,66 +771,167 @@ function DropIndicator(props) {
|
|
|
777
771
|
style: dropIndicatorStyle(props.zone)
|
|
778
772
|
});
|
|
779
773
|
}
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/dock-react/dock-view.tsx
|
|
780
776
|
/**
|
|
781
|
-
*
|
|
777
|
+
* `<DockView>` — React renderer for a layout-as-data tree.
|
|
782
778
|
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
779
|
+
* Pure function of the current model snapshot (+ registry + the two host
|
|
780
|
+
* callbacks). It owns NO layout state: it subscribes to the `LayoutModel`,
|
|
781
|
+
* keeps the latest tree in component state, and re-renders on every emission.
|
|
782
|
+
* All structural targeting is via STABLE `data-*` attributes — see the
|
|
783
|
+
* contract doc-block in `dock-view.test.tsx`.
|
|
784
|
+
*
|
|
785
|
+
* This file lives under `src/dock-react/` (NOT `src/layout/`), so importing
|
|
786
|
+
* react here does not violate the pure-TS layout boundary. The stateful child
|
|
787
|
+
* views (`SplitView`, `GroupView`) and the body renderer live in sibling
|
|
788
|
+
* modules; this file owns the orchestration (`renderNode`) + the shared
|
|
789
|
+
* `RenderContext`.
|
|
794
790
|
*/
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
791
|
+
/**
|
|
792
|
+
* The current layout EPOCH — the model's revision, bumped once per committed
|
|
793
|
+
* mutation. `DockView` provides it; descendant panels read it via
|
|
794
|
+
* `useDockLayoutEpoch`.
|
|
795
|
+
*
|
|
796
|
+
* Its reason to exist: a native-view overlay anchored inside a dock panel
|
|
797
|
+
* (a `WebContentsView` tracking its slot's geometry) re-publishes its bounds
|
|
798
|
+
* only on GEOMETRY events (ResizeObserver / window-resize / splitter-drag). A
|
|
799
|
+
* layout mutation that REORDERS a slot without resizing it — flipping a
|
|
800
|
+
* fixed-width simulator column left↔right, moving a region — produces NO such
|
|
801
|
+
* event (a same-size flex reorder fires nothing), so the overlay would freeze
|
|
802
|
+
* at its old position. The epoch is the layout layer's explicit "something
|
|
803
|
+
* moved" signal: a panel hosting a native overlay re-measures (e.g. pulses its
|
|
804
|
+
* view-anchor) when the epoch changes, catching the translate the browser never
|
|
805
|
+
* reports. Default 0 (outside a provider): the consuming effect runs once on
|
|
806
|
+
* mount, which is harmless.
|
|
807
|
+
*/
|
|
808
|
+
var LayoutEpochContext = createContext(0);
|
|
809
|
+
/**
|
|
810
|
+
* Read the current dock layout epoch (the model revision). Re-renders the caller
|
|
811
|
+
* whenever a layout mutation commits. Keyed into a panel's effect deps, it lets
|
|
812
|
+
* a native-overlay host re-measure on a reorder that fires no geometry event.
|
|
813
|
+
* Returns 0 when used outside a `<DockView>`.
|
|
814
|
+
*/
|
|
815
|
+
function useDockLayoutEpoch() {
|
|
816
|
+
return useContext(LayoutEpochContext);
|
|
815
817
|
}
|
|
816
818
|
/**
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
819
|
+
* Apply a `reorder-only` panel's redock. Such a panel may ONLY reorder WITHIN
|
|
820
|
+
* its own group: it never leaves the group (a center drop into another group)
|
|
821
|
+
* and never edge-splits (any edge zone, own or other group) — those drops are
|
|
822
|
+
* rejected here. The reorder anchors on the pointer-derived index when the
|
|
823
|
+
* gesture supplies one (real drag), else on the active anchor's CURRENT index
|
|
824
|
+
* (the imperative seam carries no pointer). The pointer index is a VISIBLE-tab
|
|
825
|
+
* strip index (hideTab panels omitted, dragged tab's own slot counted); the
|
|
826
|
+
* strip↔model coordinate reconciliation lives only here.
|
|
821
827
|
*/
|
|
822
|
-
function
|
|
823
|
-
const {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
828
|
+
function applyReorderOnlyRedock(model, registry, drop) {
|
|
829
|
+
const { groupId, activePanelId, draggedPanelId, draggedGroupId, zone, reorderIndex } = drop;
|
|
830
|
+
if (draggedGroupId === void 0 || draggedGroupId !== groupId) return;
|
|
831
|
+
if (zone !== "center") return;
|
|
832
|
+
const group = findGroupById(model.get().root, groupId);
|
|
833
|
+
let index;
|
|
834
|
+
if (reorderIndex !== void 0 && group) {
|
|
835
|
+
const visibleTabIds = group.panels.filter((panelId) => registry.get(panelId)?.hideTab !== true);
|
|
836
|
+
index = resolveReorderInsertIndex(group.panels, visibleTabIds, draggedPanelId, reorderIndex);
|
|
837
|
+
} else index = group ? group.panels.indexOf(activePanelId) : void 0;
|
|
838
|
+
model.apply((t) => movePanel(t, draggedPanelId, {
|
|
839
|
+
groupId,
|
|
840
|
+
index
|
|
841
|
+
}));
|
|
842
|
+
}
|
|
843
|
+
function DockView(props) {
|
|
844
|
+
const { model, registry, renderDomPanel, bindNativeSlot, suppressReorderOnlyDropIndicator, onActiveTabClick } = props;
|
|
845
|
+
const [tree, setTree] = useState(() => model.get());
|
|
846
|
+
const [epoch, setEpoch] = useState(0);
|
|
847
|
+
const activeDragPanelId = useRef(null);
|
|
848
|
+
useEffect(() => {
|
|
849
|
+
setTree(model.get());
|
|
850
|
+
return model.subscribe((snap) => {
|
|
851
|
+
setTree(snap.tree);
|
|
852
|
+
setEpoch(snap.revision);
|
|
853
|
+
});
|
|
854
|
+
}, [model]);
|
|
855
|
+
const handleActivate = useCallback((groupId, panelId) => {
|
|
856
|
+
model.apply((t) => setActive(t, groupId, panelId));
|
|
857
|
+
}, [model]);
|
|
858
|
+
const handleClose = useCallback((panelId) => {
|
|
859
|
+
model.apply((t) => closePanelForUser(t, panelId, registry));
|
|
860
|
+
}, [model, registry]);
|
|
861
|
+
const canClose = countPanels(tree.root) > 1;
|
|
862
|
+
const isPanelInTree = useCallback((panelId) => findPanelGroupId(tree.root, panelId) !== void 0, [tree]);
|
|
863
|
+
const handleApplyLayout = useCallback((splitId, weights) => {
|
|
864
|
+
model.apply((t) => setSizes(t, splitId, weights));
|
|
865
|
+
}, [model]);
|
|
866
|
+
const handleRedock = useCallback((groupId, activePanelId, draggedPanelId, zone, reorderIndex) => {
|
|
867
|
+
const target = {
|
|
868
|
+
groupId,
|
|
869
|
+
panelId: activePanelId
|
|
870
|
+
};
|
|
871
|
+
const draggedGroupId = findPanelGroupId(model.get().root, draggedPanelId);
|
|
872
|
+
if (registry.get(draggedPanelId)?.draggable === false) return;
|
|
873
|
+
if (registry.get(activePanelId)?.draggable === false) return;
|
|
874
|
+
if (registry.get(draggedPanelId)?.dropPolicy === "reorder-only") {
|
|
875
|
+
applyReorderOnlyRedock(model, registry, {
|
|
876
|
+
groupId,
|
|
877
|
+
activePanelId,
|
|
878
|
+
draggedPanelId,
|
|
879
|
+
draggedGroupId,
|
|
880
|
+
zone,
|
|
881
|
+
reorderIndex
|
|
882
|
+
});
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (isNoopRedock(draggedPanelId, draggedGroupId, target, zone)) return;
|
|
886
|
+
const mutation = dropZoneToMutation(zone, draggedPanelId, target);
|
|
887
|
+
if (mutation.kind === "move") {
|
|
888
|
+
model.apply((t) => movePanel(t, mutation.panelId, { groupId: mutation.destGroupId }));
|
|
889
|
+
return;
|
|
836
890
|
}
|
|
891
|
+
model.apply((t) => {
|
|
892
|
+
const { tree } = extractPanel(t, mutation.newPanelId);
|
|
893
|
+
if (findPanelGroupId(tree.root, mutation.atPanelId) === void 0) return t;
|
|
894
|
+
return splitPanel(tree, mutation.atPanelId, mutation.dir, mutation.newPanelId, mutation.side);
|
|
895
|
+
});
|
|
896
|
+
}, [model, registry]);
|
|
897
|
+
return /* @__PURE__ */ jsx(LayoutEpochContext.Provider, {
|
|
898
|
+
value: epoch,
|
|
899
|
+
children: renderNode(tree.root, {
|
|
900
|
+
registry,
|
|
901
|
+
renderDomPanel,
|
|
902
|
+
bindNativeSlot,
|
|
903
|
+
onActivate: handleActivate,
|
|
904
|
+
onActiveTabClick,
|
|
905
|
+
onApplyLayout: handleApplyLayout,
|
|
906
|
+
onRedock: handleRedock,
|
|
907
|
+
onClose: handleClose,
|
|
908
|
+
canClose,
|
|
909
|
+
isPanelInTree,
|
|
910
|
+
suppressReorderOnlyDropIndicator: suppressReorderOnlyDropIndicator ?? false,
|
|
911
|
+
activeDragPanelId
|
|
912
|
+
})
|
|
837
913
|
});
|
|
838
914
|
}
|
|
915
|
+
function renderNode(node, ctx) {
|
|
916
|
+
return node.kind === "split" ? renderSplit(node, ctx) : renderGroup(node, ctx);
|
|
917
|
+
}
|
|
918
|
+
/** A split is a stateful component (it holds the rrp Group imperative ref + runs
|
|
919
|
+
* the M1 model→view sync effect), so render it via JSX with a STABLE key so
|
|
920
|
+
* React preserves that ref / its kept-alive subtree across model emissions
|
|
921
|
+
* rather than remounting on every snapshot. Mirrors `renderGroup`/`GroupView`. */
|
|
922
|
+
function renderSplit(node, ctx) {
|
|
923
|
+
return /* @__PURE__ */ jsx(SplitView, {
|
|
924
|
+
node,
|
|
925
|
+
ctx
|
|
926
|
+
}, node.id);
|
|
927
|
+
}
|
|
928
|
+
function renderGroup(node, ctx) {
|
|
929
|
+
return /* @__PURE__ */ jsx(GroupView, {
|
|
930
|
+
node,
|
|
931
|
+
ctx
|
|
932
|
+
}, node.id);
|
|
933
|
+
}
|
|
839
934
|
//#endregion
|
|
840
|
-
export { DockView, computeDropZone, computeFlexiblePercentages, computeReorderIndex, dropZoneToMutation, isNoopRedock, layoutsEquivalent, useDockLayoutEpoch };
|
|
935
|
+
export { DockView, computeDropZone, computeFlexiblePercentages, computeReorderIndex, dropZoneToMutation, isNoopRedock, layoutsEquivalent, resolveReorderInsertIndex, useDockLayoutEpoch };
|
|
841
936
|
|
|
842
937
|
//# sourceMappingURL=index.js.map
|