@dimina-kit/electron-deck 0.1.0-dev.20260616102751 → 0.1.0-dev.20260618090552

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.
Files changed (36) hide show
  1. package/README.md +26 -24
  2. package/dist/client/index.js +1 -1
  3. package/dist/client/index.js.map +1 -1
  4. package/dist/client/layout-client.d.ts +1 -1
  5. package/dist/dock-react/dock-view.d.ts +14 -9
  6. package/dist/dock-react/dock-view.d.ts.map +1 -1
  7. package/dist/dock-react/drag-redock.d.ts +15 -0
  8. package/dist/dock-react/drag-redock.d.ts.map +1 -1
  9. package/dist/dock-react/index.d.ts +2 -2
  10. package/dist/dock-react/index.d.ts.map +1 -1
  11. package/dist/dock-react/index.js +197 -39
  12. package/dist/dock-react/index.js.map +1 -1
  13. package/dist/{electron-deck-DfFKeFEC.js → electron-deck-CW6gkGS-.js} +4 -4
  14. package/dist/electron-deck-CW6gkGS-.js.map +1 -0
  15. package/dist/host/index.js +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/internal/deck-app.d.ts +3 -3
  18. package/dist/layout/index.d.ts +2 -1
  19. package/dist/layout/index.d.ts.map +1 -1
  20. package/dist/layout/index.js +2 -2
  21. package/dist/layout/mutations.d.ts.map +1 -1
  22. package/dist/layout/sanitize.d.ts +27 -0
  23. package/dist/layout/sanitize.d.ts.map +1 -0
  24. package/dist/layout/serialize.d.ts.map +1 -1
  25. package/dist/layout/types.d.ts +49 -7
  26. package/dist/layout/types.d.ts.map +1 -1
  27. package/dist/{layout-CZtF0auJ.js → layout-WI8y1hxP.js} +55 -6
  28. package/dist/layout-WI8y1hxP.js.map +1 -0
  29. package/dist/main/compositor.d.ts +1 -1
  30. package/dist/main/index.d.ts +2 -2
  31. package/dist/main/view-handle.d.ts +8 -8
  32. package/dist/types.d.ts +2 -2
  33. package/dist/view-handle-CR-yWNfr.js.map +1 -1
  34. package/package.json +5 -5
  35. package/dist/electron-deck-DfFKeFEC.js.map +0 -1
  36. package/dist/layout-CZtF0auJ.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { a as movePanel, c as setSizes, l as splitPanel, n as closePanel, o as setActive, r as extractPanel } from "../layout-CZtF0auJ.js";
2
- import { Fragment, useCallback, useEffect, useRef, useState } from "react";
1
+ import { a as movePanel, c as setSizes, l as splitPanel, n as closePanel, o as setActive, r as extractPanel } from "../layout-WI8y1hxP.js";
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
@@ -89,6 +89,24 @@ function isNoopRedock(dragged, draggedGroupId, target, zone) {
89
89
  if (zone === "center" && draggedGroupId !== void 0 && draggedGroupId === target.groupId) return true;
90
90
  return false;
91
91
  }
92
+ /**
93
+ * Map a pointer x-position over a horizontal tab strip to an insertion index for
94
+ * a within-strip REORDER (the `dropPolicy:'reorder-only'` gesture). The strip is
95
+ * the dragged tab's own group; `tabRects` are the tab buttons' rects in visual
96
+ * order (each `left` is the rect's left edge, `width` its width). The index is
97
+ * the count of tabs whose MIDPOINT the pointer has passed: the LEFT half of a tab
98
+ * inserts BEFORE it, the RIGHT half (and the exact midpoint) inserts AFTER it. A
99
+ * pointer left of the first tab → 0; past the last tab → `tabRects.length`. Pure
100
+ * (no DOM): the caller measures the rects and passes the pointer x. An empty
101
+ * strip or a non-finite pointer → 0.
102
+ */
103
+ function computeReorderIndex(tabRects, pointerX) {
104
+ if (!Number.isFinite(pointerX)) return 0;
105
+ let index = 0;
106
+ for (const rect of tabRects) if (pointerX >= rect.left + rect.width / 2) index += 1;
107
+ else break;
108
+ return index;
109
+ }
92
110
  //#endregion
93
111
  //#region src/dock-react/dock-view.tsx
94
112
  /**
@@ -110,6 +128,33 @@ function isNoopRedock(dragged, draggedGroupId, target, zone) {
110
128
  * tab's `data-deck-tab` attribute (the jsdom seam relies on the latter).
111
129
  */
112
130
  var DRAG_PANEL_MIME = "application/x-deck-panel";
131
+ /**
132
+ * The current layout EPOCH — the model's revision, bumped once per committed
133
+ * mutation. `DockView` provides it; descendant panels read it via
134
+ * `useDockLayoutEpoch`.
135
+ *
136
+ * Its reason to exist: a native-view overlay anchored inside a dock panel
137
+ * (a `WebContentsView` tracking its slot's geometry) re-publishes its bounds
138
+ * only on GEOMETRY events (ResizeObserver / window-resize / splitter-drag). A
139
+ * layout mutation that REORDERS a slot without resizing it — flipping a
140
+ * fixed-width simulator column left↔right, moving a region — produces NO such
141
+ * event (a same-size flex reorder fires nothing), so the overlay would freeze
142
+ * at its old position. The epoch is the layout layer's explicit "something
143
+ * moved" signal: a panel hosting a native overlay re-measures (e.g. pulses its
144
+ * view-anchor) when the epoch changes, catching the translate the browser never
145
+ * reports. Default 0 (outside a provider): the consuming effect runs once on
146
+ * mount, which is harmless.
147
+ */
148
+ var LayoutEpochContext = createContext(0);
149
+ /**
150
+ * Read the current dock layout epoch (the model revision). Re-renders the caller
151
+ * whenever a layout mutation commits. Keyed into a panel's effect deps, it lets
152
+ * a native-overlay host re-measure on a reorder that fires no geometry event.
153
+ * Returns 0 when used outside a `<DockView>`.
154
+ */
155
+ function useDockLayoutEpoch() {
156
+ return useContext(LayoutEpochContext);
157
+ }
113
158
  /** Normalize raw split weights to percentages summing ~100 for `defaultSize`. */
114
159
  function toPercentages(sizes) {
115
160
  const total = sizes.reduce((a, b) => a + (b > 0 ? b : 0), 0);
@@ -120,11 +165,11 @@ function toPercentages(sizes) {
120
165
  return sizes.map((s) => (s > 0 ? s : 0) / total * 100);
121
166
  }
122
167
  /**
123
- * Compute the `defaultSize` percentage for each FLEXIBLE (unconstrained) child,
124
- * keyed by its ORIGINAL child index. Fixed (px-pinned) children are excluded
125
- * from the pool entirely so their weight never pollutes the flexible siblings'
126
- * normalization (FIX E1). `constraints[i]` non-null child i is fixed and is
127
- * absent from the returned map.
168
+ * Compute the `defaultSize` percentage for each FLEXIBLE child, keyed by its
169
+ * ORIGINAL child index. Px-sized children (a non-null `constraint` `fixedPx`
170
+ * locked OR `minPx` floored) are EXCLUDED from the pool: their size is px, not a
171
+ * weight, so it must never pollute the flexible siblings' normalization (FIX E1).
172
+ * `constraints[i]` non-null ⇒ child i is px-sized and absent from the returned map.
128
173
  */
129
174
  function computeFlexiblePercentages(sizes, constraints) {
130
175
  const flexibleIndices = sizes.map((_, i) => i).filter((i) => (constraints?.[i] ?? null) === null);
@@ -138,10 +183,12 @@ function computeFlexiblePercentages(sizes, constraints) {
138
183
  function DockView(props) {
139
184
  const { model, registry, renderDomPanel, bindNativeSlot } = props;
140
185
  const [tree, setTree] = useState(() => model.get());
186
+ const [epoch, setEpoch] = useState(0);
141
187
  useEffect(() => {
142
188
  setTree(model.get());
143
189
  return model.subscribe((snap) => {
144
190
  setTree(snap.tree);
191
+ setEpoch(snap.revision);
145
192
  });
146
193
  }, [model]);
147
194
  const handleActivate = useCallback((groupId, panelId) => {
@@ -155,12 +202,25 @@ function DockView(props) {
155
202
  const handleApplyLayout = useCallback((splitId, weights) => {
156
203
  model.apply((t) => setSizes(t, splitId, weights));
157
204
  }, [model]);
158
- const handleRedock = useCallback((groupId, activePanelId, draggedPanelId, zone) => {
205
+ const handleRedock = useCallback((groupId, activePanelId, draggedPanelId, zone, reorderIndex) => {
159
206
  const target = {
160
207
  groupId,
161
208
  panelId: activePanelId
162
209
  };
163
- if (isNoopRedock(draggedPanelId, findPanelGroupId(model.get().root, draggedPanelId), target, zone)) return;
210
+ const draggedGroupId = findPanelGroupId(model.get().root, draggedPanelId);
211
+ if (registry.get(activePanelId)?.draggable === false) return;
212
+ if (registry.get(draggedPanelId)?.dropPolicy === "reorder-only") {
213
+ if (draggedGroupId === void 0 || draggedGroupId !== groupId) return;
214
+ if (zone !== "center") return;
215
+ const group = findGroupById(model.get().root, groupId);
216
+ const index = reorderIndex ?? (group ? group.panels.indexOf(activePanelId) : void 0);
217
+ model.apply((t) => movePanel(t, draggedPanelId, {
218
+ groupId,
219
+ index
220
+ }));
221
+ return;
222
+ }
223
+ if (isNoopRedock(draggedPanelId, draggedGroupId, target, zone)) return;
164
224
  const mutation = dropZoneToMutation(zone, draggedPanelId, target);
165
225
  if (mutation.kind === "move") {
166
226
  model.apply((t) => movePanel(t, mutation.panelId, { groupId: mutation.destGroupId }));
@@ -171,18 +231,21 @@ function DockView(props) {
171
231
  if (findPanelGroupId(tree.root, mutation.atPanelId) === void 0) return t;
172
232
  return splitPanel(tree, mutation.atPanelId, mutation.dir, mutation.newPanelId, mutation.side);
173
233
  });
174
- }, [model]);
175
- return /* @__PURE__ */ jsx(Fragment, { children: renderNode(tree.root, {
176
- registry,
177
- renderDomPanel,
178
- bindNativeSlot,
179
- onActivate: handleActivate,
180
- onApplyLayout: handleApplyLayout,
181
- onRedock: handleRedock,
182
- onClose: handleClose,
183
- canClose,
184
- isPanelInTree
185
- }) });
234
+ }, [model, registry]);
235
+ return /* @__PURE__ */ jsx(LayoutEpochContext.Provider, {
236
+ value: epoch,
237
+ children: renderNode(tree.root, {
238
+ registry,
239
+ renderDomPanel,
240
+ bindNativeSlot,
241
+ onActivate: handleActivate,
242
+ onApplyLayout: handleApplyLayout,
243
+ onRedock: handleRedock,
244
+ onClose: handleClose,
245
+ canClose,
246
+ isPanelInTree
247
+ })
248
+ });
186
249
  }
187
250
  /** The id of the tab group currently holding `panelId`, or `undefined` if the
188
251
  * panel is not in the tree. Used by `handleRedock` to detect a drop back into
@@ -194,6 +257,17 @@ function findPanelGroupId(node, panelId) {
194
257
  if (found !== void 0) return found;
195
258
  }
196
259
  }
260
+ /** The tab-group node with `groupId`, or `undefined`. Used by `handleRedock` to
261
+ * read the live `panels` order of a reorder target (to anchor a same-group
262
+ * reorder on the active panel's current index when the gesture carries no
263
+ * pointer-derived index — e.g. the `__deckHandleDrop` test seam). */
264
+ function findGroupById(node, groupId) {
265
+ if (node.kind === "tabs") return node.id === groupId ? node : void 0;
266
+ for (const child of node.children) {
267
+ const found = findGroupById(child, groupId);
268
+ if (found !== void 0) return found;
269
+ }
270
+ }
197
271
  /** Total panels anywhere in the tree. Drives the last-panel close suppression:
198
272
  * a GroupView only knows its own node, so DockView computes this global count
199
273
  * once and threads the resulting `canClose` boolean down. */
@@ -243,6 +317,36 @@ var LAYOUT_EPSILON = .5;
243
317
  * self-healing (the next, larger resize writes back). If that ever matters,
244
318
  * scale the tolerance down by the flexible child count. */
245
319
  var FLEX_RATIO_TOLERANCE = .1;
320
+ /**
321
+ * The minimum weight a FLEXIBLE child may hold, given how many flexible children
322
+ * share the split (Bug #1). A flexible `<Panel>` has no rrp `minSize` by default
323
+ * (rrp floor is 0%), so a user can drag it to ~0 width; the resize write-back
324
+ * would then persist a ~0 weight and the panel comes back invisible/stuck.
325
+ *
326
+ * The floor is `min(1, floor(90 / flexCount))` — i.e. ~1 weight unit (a flexible
327
+ * split's weights are normalized to percentages downstream, so ~1 reads as ~1%
328
+ * of the flexible pool). With any realistic flexible count this is exactly 1; the
329
+ * `min(1, …)` only matters for a hypothetical 90+ flexible-child split. It is the
330
+ * SAME value used for the rrp `minSize` (A) and the write-back clamp (B) so the
331
+ * two defenses never disagree.
332
+ */
333
+ function flexibleFloor(flexCount) {
334
+ return Math.min(1, Math.max(.5, Math.floor(90 / Math.max(1, flexCount))));
335
+ }
336
+ /**
337
+ * Clamp the FLEXIBLE entries of a full-length weights array up to `floor` (Bug #1
338
+ * defense B). Px-sized children (a non-null constraint) are left untouched — their
339
+ * `sizes[i]` is a preserved placeholder, not a live weight. Returns a new array;
340
+ * a weight already ≥ floor is kept verbatim so a healthy ratio is undisturbed.
341
+ */
342
+ function clampFlexibleWeights(weights, constraints) {
343
+ const flexCount = weights.filter((_, i) => (constraints?.[i] ?? null) === null).length;
344
+ const floor = flexibleFloor(flexCount);
345
+ return weights.map((w, i) => {
346
+ if ((constraints?.[i] ?? null) !== null) return w;
347
+ return Number.isFinite(w) && w >= floor ? w : floor;
348
+ });
349
+ }
246
350
  /** Are two panelId→percentage maps equivalent within `epsilon` percentage
247
351
  * points? Both maps must cover EXACTLY the `ids` key set — a missing key OR an
248
352
  * extra key (a key present in `a`/`b` but absent from `ids`) counts as NOT
@@ -340,6 +444,8 @@ function SplitView(props) {
340
444
  const { node, ctx } = props;
341
445
  const orientation = node.orientation === "row" ? "horizontal" : "vertical";
342
446
  const percentageByIndex = computeFlexiblePercentages(node.sizes, node.constraints);
447
+ const flexCount = node.children.filter((_, i) => (node.constraints?.[i] ?? null) === null).length;
448
+ const flexMinSize = String(flexibleFloor(flexCount));
343
449
  const groupRef = useRef(null);
344
450
  const nodeRef = useRef(node);
345
451
  nodeRef.current = node;
@@ -347,7 +453,7 @@ function SplitView(props) {
347
453
  node.children.forEach((child, i) => {
348
454
  if (i > 0) items.push(/* @__PURE__ */ jsx(Separator, { "data-deck-resize-handle": "" }, `handle-${i}`));
349
455
  const constraint = node.constraints?.[i] ?? null;
350
- if (constraint) {
456
+ if (constraint?.fixedPx != null) {
351
457
  const px = `${constraint.fixedPx}px`;
352
458
  items.push(/* @__PURE__ */ jsx(Panel, {
353
459
  id: child.id,
@@ -357,11 +463,24 @@ function SplitView(props) {
357
463
  groupResizeBehavior: "preserve-pixel-size",
358
464
  children: renderNode(child, ctx)
359
465
  }, panelKey(child)));
360
- } else items.push(/* @__PURE__ */ jsx(Panel, {
361
- id: child.id,
362
- defaultSize: percentageByIndex.get(i),
363
- children: renderNode(child, ctx)
364
- }, panelKey(child)));
466
+ } else if (constraint?.minPx != null) {
467
+ const px = `${constraint.minPx}px`;
468
+ items.push(/* @__PURE__ */ jsx(Panel, {
469
+ id: child.id,
470
+ defaultSize: px,
471
+ minSize: px,
472
+ groupResizeBehavior: "preserve-pixel-size",
473
+ children: renderNode(child, ctx)
474
+ }, panelKey(child)));
475
+ } else {
476
+ const pct = percentageByIndex.get(i);
477
+ items.push(/* @__PURE__ */ jsx(Panel, {
478
+ id: child.id,
479
+ defaultSize: pct != null ? String(pct) : void 0,
480
+ minSize: flexMinSize,
481
+ children: renderNode(child, ctx)
482
+ }, panelKey(child)));
483
+ }
365
484
  });
366
485
  const handleLayoutChanged = (layout) => {
367
486
  const cur = nodeRef.current;
@@ -373,7 +492,7 @@ function SplitView(props) {
373
492
  const modelNorm = incoming.indices.map((i) => modelPctByIndex.get(i) ?? 0);
374
493
  if (!incoming.ratios.some((r, k) => Math.abs(r - modelNorm[k]) > FLEX_RATIO_TOLERANCE)) return;
375
494
  const weights = curChildIds.map((id, i) => isFixedAt(i) ? cur.sizes[i] ?? 1 : layout[id]);
376
- ctx.onApplyLayout(cur.id, weights);
495
+ ctx.onApplyLayout(cur.id, clampFlexibleWeights(weights, cur.constraints));
377
496
  };
378
497
  const setSplitRef = (el) => {
379
498
  if (el) {
@@ -381,7 +500,7 @@ function SplitView(props) {
381
500
  const cur = nodeRef.current;
382
501
  const isFixedAt = (i) => (cur.constraints?.[i] ?? null) !== null;
383
502
  const next = weights.map((w, i) => isFixedAt(i) ? cur.sizes[i] ?? 1 : w);
384
- ctx.onApplyLayout(cur.id, next);
503
+ ctx.onApplyLayout(cur.id, clampFlexibleWeights(next, cur.constraints));
385
504
  };
386
505
  Object.defineProperty(el, "__deckGroupApi", {
387
506
  configurable: true,
@@ -401,9 +520,15 @@ function SplitView(props) {
401
520
  api.setLayout(targetMap);
402
521
  return true;
403
522
  }, []);
523
+ const sizesKey = node.sizes.join(",");
524
+ const childCountKey = node.children.length;
404
525
  useEffect(() => {
405
526
  runSync();
406
- }, [node.sizes.join(","), runSync]);
527
+ }, [
528
+ sizesKey,
529
+ childCountKey,
530
+ runSync
531
+ ]);
407
532
  return /* @__PURE__ */ jsx("div", {
408
533
  ref: setSplitRef,
409
534
  "data-deck-split": node.id,
@@ -422,7 +547,7 @@ function SplitView(props) {
422
547
  height: "100%"
423
548
  },
424
549
  children: items
425
- })
550
+ }, node.children.length)
426
551
  });
427
552
  }
428
553
  /** Stable React key for a child node (split id or group id). */
@@ -479,8 +604,37 @@ function GroupView(props) {
479
604
  setDropZone(null);
480
605
  const dragged = e.dataTransfer.getData(DRAG_PANEL_MIME) || e.dataTransfer.getData("text/plain");
481
606
  if (!dragged || ctx.registry.get(dragged) === void 0 || !ctx.isPanelInTree(dragged)) return;
482
- ctx.onRedock(node.id, node.active, dragged, zone);
607
+ const reorderIndex = computeReorderIndex(Array.from(e.currentTarget.querySelectorAll("[data-deck-tab]")).map((el) => {
608
+ const r = el.getBoundingClientRect();
609
+ return {
610
+ left: r.left,
611
+ width: r.width
612
+ };
613
+ }), e.clientX);
614
+ ctx.onRedock(node.id, node.active, dragged, zone, reorderIndex);
615
+ };
616
+ const handleTabStripDragOver = (e) => {
617
+ if (!e.dataTransfer.types.includes(DRAG_PANEL_MIME)) return;
618
+ e.preventDefault();
619
+ e.stopPropagation();
620
+ setDropZone(null);
621
+ };
622
+ const handleTabStripDrop = (e) => {
623
+ e.preventDefault();
624
+ e.stopPropagation();
625
+ setDropZone(null);
626
+ const dragged = e.dataTransfer.getData(DRAG_PANEL_MIME) || e.dataTransfer.getData("text/plain");
627
+ if (!dragged || ctx.registry.get(dragged) === void 0 || !ctx.isPanelInTree(dragged)) return;
628
+ const reorderIndex = computeReorderIndex(Array.from(e.currentTarget.querySelectorAll("[data-deck-tab]")).map((el) => {
629
+ const r = el.getBoundingClientRect();
630
+ return {
631
+ left: r.left,
632
+ width: r.width
633
+ };
634
+ }), e.clientX);
635
+ ctx.onRedock(node.id, node.active, dragged, "center", reorderIndex);
483
636
  };
637
+ const visibleTabs = node.panels.filter((panelId) => !ctx.registry.get(panelId)?.hideTab);
484
638
  return /* @__PURE__ */ jsxs("div", {
485
639
  ref: setGroupRef,
486
640
  "data-deck-group": node.id,
@@ -497,16 +651,19 @@ function GroupView(props) {
497
651
  minHeight: 0
498
652
  },
499
653
  children: [
500
- /* @__PURE__ */ jsx("div", {
654
+ visibleTabs.length > 0 ? /* @__PURE__ */ jsx("div", {
501
655
  role: "tablist",
502
656
  style: { flexShrink: 0 },
503
- children: node.panels.map((panelId) => {
657
+ onDragOver: handleTabStripDragOver,
658
+ onDrop: handleTabStripDrop,
659
+ children: visibleTabs.map((panelId) => {
504
660
  const active = panelId === node.active;
505
- const title = ctx.registry.get(panelId)?.title ?? panelId;
661
+ const descriptor = ctx.registry.get(panelId);
662
+ const title = descriptor?.title ?? panelId;
506
663
  return /* @__PURE__ */ jsxs("button", {
507
664
  type: "button",
508
665
  role: "tab",
509
- draggable: "true",
666
+ draggable: descriptor?.draggable !== false ? "true" : void 0,
510
667
  "data-deck-tab": panelId,
511
668
  "data-active": active ? "true" : "false",
512
669
  onDragStart: (e) => {
@@ -543,7 +700,7 @@ function GroupView(props) {
543
700
  }) : null]
544
701
  }, panelId);
545
702
  })
546
- }),
703
+ }) : null,
547
704
  renderActiveBody(node, ctx),
548
705
  dropZone ? /* @__PURE__ */ jsx(DropIndicator, { zone: dropZone }) : null
549
706
  ]
@@ -633,7 +790,8 @@ function renderActiveBody(node, ctx) {
633
790
  return /* @__PURE__ */ jsx("div", {
634
791
  "data-deck-panel-body": panelId,
635
792
  style: {
636
- display: active ? void 0 : "none",
793
+ display: active ? "flex" : "none",
794
+ flexDirection: "column",
637
795
  flex: 1,
638
796
  minWidth: 0,
639
797
  minHeight: 0
@@ -666,6 +824,6 @@ function NativeSlot(props) {
666
824
  });
667
825
  }
668
826
  //#endregion
669
- export { DockView, computeDropZone, computeFlexiblePercentages, dropZoneToMutation, isNoopRedock, layoutsEquivalent };
827
+ export { DockView, computeDropZone, computeFlexiblePercentages, computeReorderIndex, dropZoneToMutation, isNoopRedock, layoutsEquivalent, useDockLayoutEpoch };
670
828
 
671
829
  //# sourceMappingURL=index.js.map