@surdeddd/wmkit 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -195,6 +195,12 @@ function createWindowManager(options = {}) {
195
195
  const idPrefix = options.idPrefix ?? "wm";
196
196
  const historyLimit = options.historyLimit ?? 50;
197
197
  let windows = {};
198
+ let windowsShared = false;
199
+ let modalCount = 0;
200
+ let cachedGroups = null;
201
+ let cachedOrder = null;
202
+ let cachedTabs = null;
203
+ let groupsDirty = false;
198
204
  let order = [];
199
205
  let focusedId = null;
200
206
  let viewport = options.viewport ?? { width: 0, height: 0 };
@@ -219,6 +225,7 @@ function createWindowManager(options = {}) {
219
225
  const layouts = /* @__PURE__ */ new Map();
220
226
  function getState() {
221
227
  if (!snapshot) {
228
+ windowsShared = true;
222
229
  snapshot = { windows, order, focusedId, viewport, workspace, groups: buildGroups() };
223
230
  }
224
231
  return snapshot;
@@ -240,30 +247,58 @@ function createWindowManager(options = {}) {
240
247
  function queueEvent(emit) {
241
248
  pendingEvents.push(emit);
242
249
  }
250
+ function ownWindows() {
251
+ if (windowsShared) {
252
+ windows = { ...windows };
253
+ windowsShared = false;
254
+ }
255
+ return windows;
256
+ }
243
257
  function setWindow(next) {
244
- windows = { ...windows, [next.id]: next };
258
+ const map = ownWindows();
259
+ const previous = map[next.id];
260
+ const wasModal = previous?.layer === "modal";
261
+ if (wasModal !== (next.layer === "modal")) modalCount += wasModal ? -1 : 1;
262
+ if (previous?.groupId !== next.groupId) groupsDirty = true;
263
+ map[next.id] = next;
245
264
  }
246
265
  function removeWindow(id) {
247
- const { [id]: _removed, ...rest } = windows;
248
- windows = rest;
266
+ const map = ownWindows();
267
+ const previous = map[id];
268
+ if (previous?.layer === "modal") modalCount -= 1;
269
+ if (previous?.groupId != null) groupsDirty = true;
270
+ delete map[id];
249
271
  order = order.filter((entry) => entry !== id);
250
272
  }
273
+ function countModals() {
274
+ let total = 0;
275
+ for (const id of order) {
276
+ if (windows[id].layer === "modal") total += 1;
277
+ }
278
+ return total;
279
+ }
251
280
  function layerRankOf(id) {
252
281
  return LAYER_RANK[windows[id].layer];
253
282
  }
254
283
  function sortByLayer(ids) {
255
284
  return ids.map((id, index) => ({ id, index, rank: layerRankOf(id) })).sort((a, b) => a.rank !== b.rank ? a.rank - b.rank : a.index - b.index).map((entry) => entry.id);
256
285
  }
286
+ function blockOf(id) {
287
+ const win = windows[id];
288
+ if (win.groupId === null) return [id];
289
+ return [...membersOf(win.groupId).filter((entry) => entry !== id), id];
290
+ }
257
291
  function raise(id) {
258
292
  const win = windows[id];
259
- const without = order.filter((entry) => entry !== id);
293
+ const block = blockOf(id);
294
+ const without = order.filter((entry) => !block.includes(entry));
260
295
  const rank = LAYER_RANK[win.layer];
261
296
  let insertAt = without.length;
262
297
  for (let i = without.length - 1; i >= 0; i -= 1) {
263
298
  if (layerRankOf(without[i]) > rank) insertAt = i;
264
299
  else break;
265
300
  }
266
- const next = [...without.slice(0, insertAt), id, ...without.slice(insertAt)];
301
+ const next = [...without.slice(0, insertAt), ...block, ...without.slice(insertAt)];
267
302
  const changed = next.length !== order.length || next.some((entry, i) => entry !== order[i]);
268
303
  order = next;
269
304
  return changed;
@@ -278,31 +313,57 @@ function createWindowManager(options = {}) {
278
313
  return win.groupId === null || activeTabs[win.groupId] === win.id;
279
314
  }
280
315
  function buildGroups() {
281
- const result = {};
316
+ if (cachedGroups && !groupsDirty && cachedOrder === order && cachedTabs === activeTabs) {
317
+ return cachedGroups;
318
+ }
319
+ const result = /* @__PURE__ */ Object.create(null);
282
320
  for (const [groupId, activeId] of Object.entries(activeTabs)) {
283
321
  result[groupId] = { id: groupId, activeId, members: membersOf(groupId) };
284
322
  }
323
+ cachedGroups = result;
324
+ cachedOrder = order;
325
+ cachedTabs = activeTabs;
326
+ groupsDirty = false;
285
327
  return result;
286
328
  }
329
+ function sharedSize(ids, size) {
330
+ let shared = size;
331
+ for (const id of ids) {
332
+ const fit = normalizeSize(shared, windows[id]);
333
+ shared = {
334
+ width: Math.max(shared.width, fit.width),
335
+ height: Math.max(shared.height, fit.height)
336
+ };
337
+ }
338
+ return shared;
339
+ }
287
340
  function syncGroup(source) {
288
- if (source.groupId === null) return;
289
- for (const id of membersOf(source.groupId)) {
290
- if (id === source.id) continue;
341
+ if (source.groupId === null) return source;
342
+ const members = membersOf(source.groupId);
343
+ const fitted = sharedSize(members, source.bounds);
344
+ let effective = source;
345
+ if (fitted.width !== source.bounds.width || fitted.height !== source.bounds.height) {
346
+ effective = { ...source, bounds: { ...source.bounds, ...fitted } };
347
+ setWindow(effective);
348
+ }
349
+ for (const id of members) {
350
+ if (id === effective.id) continue;
291
351
  const member = windows[id];
292
- if (boundsEqual(member.bounds, source.bounds) && member.stage === source.stage && member.snapZone === source.snapZone && member.layer === source.layer && member.workspace === source.workspace) {
352
+ if (boundsEqual(member.bounds, effective.bounds) && member.stage === effective.stage && member.snapZone === effective.snapZone && member.layer === effective.layer && member.workspace === effective.workspace) {
293
353
  continue;
294
354
  }
295
355
  setWindow({
296
356
  ...member,
297
- bounds: source.bounds,
298
- stage: source.stage,
299
- snapZone: source.snapZone,
300
- restoreBounds: source.restoreBounds,
301
- restoreStage: source.restoreStage,
302
- layer: source.layer,
303
- workspace: source.workspace
357
+ bounds: effective.bounds,
358
+ stage: effective.stage,
359
+ snapZone: effective.snapZone,
360
+ restoreBounds: effective.restoreBounds,
361
+ restoreStage: effective.restoreStage,
362
+ layer: effective.layer,
363
+ workspace: effective.workspace
304
364
  });
305
365
  }
366
+ return effective;
306
367
  }
307
368
  function dissolveIfOrphaned(groupId) {
308
369
  const members = membersOf(groupId);
@@ -314,6 +375,7 @@ function createWindowManager(options = {}) {
314
375
  activeTabs = rest;
315
376
  }
316
377
  function topModalId() {
378
+ if (modalCount === 0) return null;
317
379
  for (let i = order.length - 1; i >= 0; i -= 1) {
318
380
  const win = windows[order[i]];
319
381
  if (win.layer === "modal" && win.stage !== "minimized" && onActiveWorkspace(win) && isVisibleTab(win)) {
@@ -332,8 +394,21 @@ function createWindowManager(options = {}) {
332
394
  });
333
395
  }
334
396
  function focusTop() {
335
- const targets = focusTargets();
336
- focusedId = targets.length > 0 ? targets[targets.length - 1] : null;
397
+ for (let i = order.length - 1; i >= 0; i -= 1) {
398
+ const win = windows[order[i]];
399
+ if (win.stage === "minimized" || !onActiveWorkspace(win) || !isVisibleTab(win)) continue;
400
+ focusedId = win.id;
401
+ return;
402
+ }
403
+ focusedId = null;
404
+ }
405
+ function reconcileFocus() {
406
+ if (focusedId === null) return;
407
+ const win = windows[focusedId];
408
+ if (win && win.stage !== "minimized" && onActiveWorkspace(win) && isVisibleTab(win)) return;
409
+ const previous = focusedId;
410
+ focusTop();
411
+ if (focusedId) emitFocus(windows[focusedId], previous);
337
412
  }
338
413
  function emitFocus(win, previous) {
339
414
  queueEvent(() => emitter.emit("focus", { window: win, previous }));
@@ -456,7 +531,6 @@ function createWindowManager(options = {}) {
456
531
  const win = windows[id];
457
532
  if (!win) return false;
458
533
  if (!onActiveWorkspace(win)) setWorkspace(win.workspace);
459
- if (!isVisibleTab(win)) activateTab(id);
460
534
  const modal = topModalId();
461
535
  if (modal && modal !== id && win.layer !== "modal") {
462
536
  const modalWin = windows[modal];
@@ -464,6 +538,7 @@ function createWindowManager(options = {}) {
464
538
  commitQuiet();
465
539
  return false;
466
540
  }
541
+ if (!isVisibleTab(win)) activateTab(id);
467
542
  if (win.stage === "minimized") {
468
543
  restore(id);
469
544
  return focusedId === id;
@@ -516,8 +591,9 @@ function createWindowManager(options = {}) {
516
591
  const next = build(win);
517
592
  if (!next) return false;
518
593
  setWindow(next);
519
- syncGroup(next);
520
- queueEvent(() => emitter.emit("stage", { window: next, previous: win.stage }));
594
+ const synced = syncGroup(next);
595
+ queueEvent(() => emitter.emit("stage", { window: synced, previous: win.stage }));
596
+ if (next.groupId !== null) reconcileFocus();
521
597
  commit();
522
598
  return true;
523
599
  }
@@ -648,9 +724,11 @@ function createWindowManager(options = {}) {
648
724
  if (boundsEqual(bounds, win.bounds) && !becameNormal) return true;
649
725
  const next = becameNormal ? { ...win, stage: "normal", snapZone: null, restoreBounds: null, bounds } : { ...win, bounds };
650
726
  setWindow(next);
651
- syncGroup(next);
652
- if (becameNormal) queueEvent(() => emitter.emit("stage", { window: next, previous: "snapped" }));
653
- queueEvent(() => emitter.emit("resize", { window: next }));
727
+ const synced = syncGroup(next);
728
+ if (becameNormal) {
729
+ queueEvent(() => emitter.emit("stage", { window: synced, previous: "snapped" }));
730
+ }
731
+ queueEvent(() => emitter.emit("resize", { window: synced }));
654
732
  commit();
655
733
  return true;
656
734
  }
@@ -676,18 +754,18 @@ function createWindowManager(options = {}) {
676
754
  const resized = size.width !== next.bounds.width || size.height !== next.bounds.height;
677
755
  const finalWin = resized ? { ...next, bounds: { ...next.bounds, ...size } } : next;
678
756
  setWindow(finalWin);
679
- syncGroup(finalWin);
757
+ const synced = syncGroup(finalWin);
680
758
  if (patch.layer && patch.layer !== win.layer) {
681
759
  order = sortByLayer(order);
682
760
  queueEvent(() => emitter.emit("order", { order }));
683
761
  if (patch.layer === "modal" && topModalId() === id && focusedId !== id) {
684
762
  const previous = focusedId;
685
763
  focusedId = id;
686
- emitFocus(finalWin, previous);
764
+ emitFocus(synced, previous);
687
765
  }
688
766
  }
689
- queueEvent(() => emitter.emit("update", { window: finalWin }));
690
- if (resized) queueEvent(() => emitter.emit("resize", { window: finalWin }));
767
+ queueEvent(() => emitter.emit("update", { window: synced }));
768
+ if (resized) queueEvent(() => emitter.emit("resize", { window: synced }));
691
769
  commit();
692
770
  return true;
693
771
  }
@@ -722,6 +800,8 @@ function createWindowManager(options = {}) {
722
800
  if (!onActiveWorkspace(updated) && focusedId === id) {
723
801
  focusTop();
724
802
  if (focusedId) emitFocus(windows[focusedId], id);
803
+ } else if (updated.groupId !== null && !onActiveWorkspace(updated)) {
804
+ reconcileFocus();
725
805
  } else if (onActiveWorkspace(updated) && topModalId() === id && focusedId !== id) {
726
806
  const previous = focusedId;
727
807
  focusedId = id;
@@ -747,7 +827,16 @@ function createWindowManager(options = {}) {
747
827
  for (const entry of siblings) if (!expanded.includes(entry)) expanded.push(entry);
748
828
  }
749
829
  if (expanded.length < 2) return null;
750
- const groupId = `${idPrefix}-group-${++groupCounter}`;
830
+ let groupId = `${idPrefix}-group-${++groupCounter}`;
831
+ while (activeTabs[groupId]) groupId = `${idPrefix}-group-${++groupCounter}`;
832
+ const shared = expanded.reduce(
833
+ (size, id) => {
834
+ const fit = normalizeSize(size, windows[id]);
835
+ return { width: Math.max(size.width, fit.width), height: Math.max(size.height, fit.height) };
836
+ },
837
+ { width: host.bounds.width, height: host.bounds.height }
838
+ );
839
+ const bounds = { x: host.bounds.x, y: host.bounds.y, ...shared };
751
840
  const retired = /* @__PURE__ */ new Set();
752
841
  for (const id of expanded) {
753
842
  const win = windows[id];
@@ -755,7 +844,7 @@ function createWindowManager(options = {}) {
755
844
  setWindow({
756
845
  ...win,
757
846
  groupId,
758
- bounds: host.bounds,
847
+ bounds,
759
848
  stage: host.stage,
760
849
  snapZone: host.snapZone,
761
850
  restoreBounds: host.restoreBounds,
@@ -770,8 +859,11 @@ function createWindowManager(options = {}) {
770
859
  raise(host.id);
771
860
  emitGroup(groupId, null);
772
861
  queueEvent(() => emitter.emit("order", { order }));
862
+ const hostVisible = onActiveWorkspace(host) && host.stage !== "minimized";
863
+ commit();
864
+ if (hostVisible) focus(host.id);
865
+ reconcileFocus();
773
866
  commit();
774
- if (onActiveWorkspace(host) && host.stage !== "minimized") focus(host.id);
775
867
  return groupId;
776
868
  }
777
869
  function ungroup(id) {
@@ -785,7 +877,7 @@ function createWindowManager(options = {}) {
785
877
  dissolveIfOrphaned(groupId);
786
878
  if (membersOf(groupId).length >= 2) emitGroup(groupId, wasActive ? id : null);
787
879
  commit();
788
- focus(id);
880
+ if (onActiveWorkspace(win) && win.stage !== "minimized") focus(id);
789
881
  return true;
790
882
  }
791
883
  function activateTab(id) {
@@ -795,6 +887,10 @@ function createWindowManager(options = {}) {
795
887
  const previous = activeTabs[groupId];
796
888
  if (previous === id) return false;
797
889
  activeTabs = { ...activeTabs, [groupId]: id };
890
+ if (focusedId === previous) {
891
+ focusedId = id;
892
+ emitFocus(win, previous);
893
+ }
798
894
  emitGroup(groupId, previous);
799
895
  commit();
800
896
  return true;
@@ -805,14 +901,15 @@ function createWindowManager(options = {}) {
805
901
  function sendToBack(id) {
806
902
  const win = windows[id];
807
903
  if (!win) return false;
808
- const without = order.filter((entry) => entry !== id);
904
+ const block = blockOf(id);
905
+ const without = order.filter((entry) => !block.includes(entry));
809
906
  const rank = LAYER_RANK[win.layer];
810
907
  let insertAt = 0;
811
908
  for (const entry of without) {
812
909
  if (layerRankOf(entry) < rank) insertAt += 1;
813
910
  else break;
814
911
  }
815
- const next = [...without.slice(0, insertAt), id, ...without.slice(insertAt)];
912
+ const next = [...without.slice(0, insertAt), ...block, ...without.slice(insertAt)];
816
913
  if (next.every((entry, i) => entry === order[i])) return false;
817
914
  order = next;
818
915
  queueEvent(() => emitter.emit("order", { order }));
@@ -836,6 +933,7 @@ function createWindowManager(options = {}) {
836
933
  return Object.values(windows).filter((win) => win.stage === "minimized" && onActiveWorkspace(win) && isVisibleTab(win)).sort((a, b) => a.openedSeq - b.openedSeq);
837
934
  }
838
935
  function captureEntry() {
936
+ windowsShared = true;
839
937
  return { windows, order, focusedId, workspace, activeTabs };
840
938
  }
841
939
  function recordHistory() {
@@ -876,19 +974,23 @@ function createWindowManager(options = {}) {
876
974
  function reflowViewport() {
877
975
  for (const id of order) {
878
976
  const win = windows[id];
977
+ if (!isVisibleTab(win)) continue;
879
978
  if (win.stage === "maximized") {
880
979
  const updated = { ...win, bounds: fullBounds() };
881
980
  setWindow(updated);
882
- queueEvent(() => emitter.emit("resize", { window: updated }));
981
+ const synced = syncGroup(updated);
982
+ queueEvent(() => emitter.emit("resize", { window: synced }));
883
983
  } else if (win.stage === "snapped" && win.snapZone) {
884
984
  const updated = { ...win, bounds: snapBounds(win, win.snapZone) };
885
985
  setWindow(updated);
886
- queueEvent(() => emitter.emit("resize", { window: updated }));
986
+ const synced = syncGroup(updated);
987
+ queueEvent(() => emitter.emit("resize", { window: synced }));
887
988
  } else if (win.stage === "normal" && keepInViewport) {
888
989
  const bounds = clampToViewport(win.bounds, viewport, minVisible);
889
990
  if (!boundsEqual(bounds, win.bounds)) {
890
991
  const updated = { ...win, bounds };
891
992
  setWindow(updated);
993
+ syncGroup(updated);
892
994
  queueEvent(() => emitter.emit("move", { window: updated }));
893
995
  }
894
996
  }
@@ -897,6 +999,8 @@ function createWindowManager(options = {}) {
897
999
  function applyEntry(entry) {
898
1000
  windows = entry.windows;
899
1001
  order = [...entry.order];
1002
+ modalCount = countModals();
1003
+ groupsDirty = true;
900
1004
  focusedId = entry.focusedId;
901
1005
  activeTabs = entry.activeTabs;
902
1006
  const previousWorkspace = workspace;
@@ -975,7 +1079,7 @@ function createWindowManager(options = {}) {
975
1079
  function arrange(mode) {
976
1080
  const ids = order.filter((id) => {
977
1081
  const win = windows[id];
978
- return win.stage !== "minimized" && onActiveWorkspace(win);
1082
+ return win.stage !== "minimized" && onActiveWorkspace(win) && isVisibleTab(win);
979
1083
  });
980
1084
  if (ids.length === 0) return;
981
1085
  if (mode === "cascade") {
@@ -1007,7 +1111,8 @@ function createWindowManager(options = {}) {
1007
1111
  }
1008
1112
  function minimizeAll() {
1009
1113
  for (const id of [...order]) {
1010
- if (onActiveWorkspace(windows[id])) minimize(id);
1114
+ const win = windows[id];
1115
+ if (onActiveWorkspace(win) && isVisibleTab(win)) minimize(id);
1011
1116
  }
1012
1117
  }
1013
1118
  function restoreAll() {
@@ -1114,8 +1219,11 @@ function createWindowManager(options = {}) {
1114
1219
  if (!seen.has(id)) nextOrder.push(id);
1115
1220
  }
1116
1221
  windows = nextWindows;
1222
+ windowsShared = false;
1117
1223
  order = nextOrder;
1118
1224
  order = sortByLayer(order);
1225
+ modalCount = countModals();
1226
+ groupsDirty = true;
1119
1227
  seq = maxSeq;
1120
1228
  const previousWorkspace = workspace;
1121
1229
  workspace = normalizeWorkspace(data.workspace, 0);
@@ -1127,7 +1235,7 @@ function createWindowManager(options = {}) {
1127
1235
  if (focusedCandidate && (focusedCandidate.stage === "minimized" || !onActiveWorkspace(focusedCandidate))) {
1128
1236
  focusedId = null;
1129
1237
  }
1130
- const restoredTabs = {};
1238
+ const restoredTabs = /* @__PURE__ */ Object.create(null);
1131
1239
  const rawTabs = data.activeTabs;
1132
1240
  for (const id of order) {
1133
1241
  const groupId = windows[id].groupId;
@@ -1224,6 +1332,7 @@ function createWindowManager(options = {}) {
1224
1332
 
1225
1333
  // src/dom/animate.ts
1226
1334
  function prefersReducedMotion(win) {
1335
+ if (typeof win.matchMedia !== "function") return false;
1227
1336
  return win.matchMedia("(prefers-reduced-motion: reduce)").matches;
1228
1337
  }
1229
1338
  function flipFromTarget(source, target, options = {}) {
@@ -1352,14 +1461,6 @@ function createAnnouncer(wm, container, messages = {}) {
1352
1461
  };
1353
1462
  }
1354
1463
 
1355
- // src/dom/shared.ts
1356
- var INTERACTIVE_SELECTOR = "button, input, select, textarea, a[href], [contenteditable], [data-wm-close], [data-wm-minimize], [data-wm-maximize]";
1357
- function windowOf(element) {
1358
- const view = element.ownerDocument.defaultView;
1359
- if (!view) throw new Error("wmkit: desktop element is not attached to a document");
1360
- return view;
1361
- }
1362
-
1363
1464
  // src/dom/drag.ts
1364
1465
  function createDragStarter(ctx) {
1365
1466
  const { wm, doc, view } = ctx;
@@ -1367,7 +1468,7 @@ function createDragStarter(ctx) {
1367
1468
  const win = wm.get(id);
1368
1469
  if (!win?.draggable || event.button !== 0 || ctx.currentDrag()) return;
1369
1470
  const target = event.target;
1370
- if (target?.closest(INTERACTIVE_SELECTOR)) return;
1471
+ if (target?.closest(ctx.interactiveSelector)) return;
1371
1472
  event.preventDefault();
1372
1473
  const releaseRect = ctx.trackRect();
1373
1474
  const point = ctx.toLocal(event);
@@ -1400,6 +1501,8 @@ function createDragStarter(ctx) {
1400
1501
  const el = ctx.windowElement(id);
1401
1502
  function armGroup(target2) {
1402
1503
  session.groupTarget = target2;
1504
+ session.zone = null;
1505
+ ctx.hidePreview();
1403
1506
  ctx.markGroupTarget(target2);
1404
1507
  }
1405
1508
  function clearHover() {
@@ -1444,9 +1547,9 @@ function createDragStarter(ctx) {
1444
1547
  for (const otherId of state.order) {
1445
1548
  if (otherId === id) continue;
1446
1549
  const other = state.windows[otherId];
1447
- if (other && other.stage !== "minimized" && other.workspace === state.workspace) {
1448
- targets.push(other.bounds);
1449
- }
1550
+ if (!other || other.stage === "minimized" || other.workspace !== state.workspace) continue;
1551
+ if (other.groupId !== null && state.groups[other.groupId]?.activeId !== otherId) continue;
1552
+ targets.push(other.bounds);
1450
1553
  }
1451
1554
  const magnet = magnetize(
1452
1555
  { x: nextX, y: nextY, width: current.bounds.width, height: current.bounds.height },
@@ -1720,6 +1823,99 @@ function createResizeHandles(doc, edge, corner) {
1720
1823
  });
1721
1824
  }
1722
1825
 
1826
+ // src/dom/shared.ts
1827
+ var INTERACTIVE_SELECTOR = "button, input, select, textarea, a[href], [contenteditable], [data-wm-close], [data-wm-minimize], [data-wm-maximize]";
1828
+ function windowOf(element) {
1829
+ const view = element.ownerDocument.defaultView;
1830
+ if (!view) throw new Error("wmkit: desktop element is not attached to a document");
1831
+ return view;
1832
+ }
1833
+
1834
+ // src/dom/stacking.ts
1835
+ var UNASSIGNED = Number.NEGATIVE_INFINITY;
1836
+ var tails = new Int32Array(0);
1837
+ var parents = new Int32Array(0);
1838
+ var kept = new Uint8Array(0);
1839
+ function reserve(length) {
1840
+ if (tails.length >= length) return;
1841
+ const size = Math.max(length, 64);
1842
+ tails = new Int32Array(size);
1843
+ parents = new Int32Array(size);
1844
+ kept = new Uint8Array(size);
1845
+ }
1846
+ function markLongestIncreasing(target) {
1847
+ const length = target.length;
1848
+ reserve(length);
1849
+ kept.fill(0, 0, length);
1850
+ let size = 0;
1851
+ for (let i = 0; i < length; i += 1) {
1852
+ const value = target.zAt(i);
1853
+ parents[i] = -1;
1854
+ if (!Number.isFinite(value)) continue;
1855
+ let low = 0;
1856
+ let high = size;
1857
+ while (low < high) {
1858
+ const mid = low + high >> 1;
1859
+ if (target.zAt(tails[mid]) < value) low = mid + 1;
1860
+ else high = mid;
1861
+ }
1862
+ if (low > 0) parents[i] = tails[low - 1];
1863
+ tails[low] = i;
1864
+ if (low === size) size += 1;
1865
+ }
1866
+ let cursor = size > 0 ? tails[size - 1] : -1;
1867
+ let count = 0;
1868
+ while (cursor !== -1) {
1869
+ kept[cursor] = 1;
1870
+ count += 1;
1871
+ cursor = parents[cursor];
1872
+ }
1873
+ return count;
1874
+ }
1875
+ function between(low, high, base, gap) {
1876
+ if (!Number.isFinite(high)) return Number.isFinite(low) ? low + gap : base + gap;
1877
+ if (!Number.isFinite(low)) {
1878
+ const candidate2 = high - gap > base ? high - gap : Math.floor((base + high) / 2);
1879
+ return candidate2 > base && candidate2 < high ? candidate2 : null;
1880
+ }
1881
+ const candidate = Math.floor((low + high) / 2);
1882
+ return candidate > low && candidate < high ? candidate : null;
1883
+ }
1884
+ function renormalize(target, base, gap) {
1885
+ let writes = 0;
1886
+ for (let i = 0; i < target.length; i += 1) {
1887
+ const z = base + (i + 1) * gap;
1888
+ if (target.zAt(i) === z) continue;
1889
+ target.assign(i, z);
1890
+ writes += 1;
1891
+ }
1892
+ return writes;
1893
+ }
1894
+ function restack(target, options = {}) {
1895
+ const base = options.base ?? 0;
1896
+ const gap = options.gap ?? 32;
1897
+ const length = target.length;
1898
+ if (length === 0) return 0;
1899
+ const keepCount = markLongestIncreasing(target);
1900
+ if (length - keepCount > length / 3) return renormalize(target, base, gap);
1901
+ let writes = 0;
1902
+ for (let i = 0; i < length; i += 1) {
1903
+ if (kept[i] === 1) continue;
1904
+ const low = i > 0 ? target.zAt(i - 1) : UNASSIGNED;
1905
+ let high = Number.POSITIVE_INFINITY;
1906
+ for (let j = i + 1; j < length; j += 1) {
1907
+ if (kept[j] !== 1) continue;
1908
+ high = target.zAt(j);
1909
+ break;
1910
+ }
1911
+ const z = between(low, high, base, gap);
1912
+ if (z === null) return renormalize(target, base, gap);
1913
+ target.assign(i, z);
1914
+ writes += 1;
1915
+ }
1916
+ return writes;
1917
+ }
1918
+
1723
1919
  // src/dom/controller.ts
1724
1920
  var SNAP_SHORTCUTS = {
1725
1921
  ArrowLeft: "left",
@@ -1746,17 +1942,24 @@ function attachDesktop(wm, element, options = {}) {
1746
1942
  const hitEdge = options.hitAreas?.edge ?? (coarsePointer ? 16 : 8);
1747
1943
  const hitCorner = options.hitAreas?.corner ?? (coarsePointer ? 24 : 12);
1748
1944
  const magnetThreshold = options.magnetism === false ? 0 : (typeof options.magnetism === "object" ? options.magnetism.threshold : void 0) ?? (coarsePointer ? 12 : 8);
1945
+ const zIndexBase = options.stacking?.base ?? 0;
1946
+ const zIndexGap = options.stacking?.gap ?? 32;
1947
+ const interactiveSelector = options.interactiveSelector ?? INTERACTIVE_SELECTOR;
1948
+ const animationEnabled = options.animation !== false;
1949
+ const animationOptions = typeof options.animation === "object" ? options.animation : {};
1749
1950
  element.dataset.wmDesktop = "";
1750
1951
  element.tabIndex = -1;
1751
1952
  if (view.getComputedStyle(element).position === "static") {
1752
1953
  element.style.position = "relative";
1753
1954
  }
1955
+ if (options.stacking?.isolate !== false) element.style.isolation = "isolate";
1754
1956
  const registry = /* @__PURE__ */ new Map();
1755
1957
  const cleanup = [];
1756
1958
  let lastOrder = null;
1757
1959
  let lastFocused = null;
1758
1960
  let drag = null;
1759
1961
  let groupTargetId = null;
1962
+ let groupTargetEl = null;
1760
1963
  let cachedRect = null;
1761
1964
  let rectUsers = 0;
1762
1965
  let announcer = null;
@@ -1827,6 +2030,7 @@ function attachDesktop(wm, element, options = {}) {
1827
2030
  topEdge,
1828
2031
  hitEdge,
1829
2032
  hitCorner,
2033
+ interactiveSelector,
1830
2034
  magnetThreshold,
1831
2035
  groupDwell,
1832
2036
  groupTarget(clientX, clientY, selfId) {
@@ -1835,21 +2039,16 @@ function attachDesktop(wm, element, options = {}) {
1835
2039
  const handle = node.closest?.("[data-wm-drag]");
1836
2040
  const host = handle?.closest("[data-wm-window]");
1837
2041
  const id = host?.dataset.wmWindow;
1838
- if (id && id !== selfId && registry.has(id)) return id;
2042
+ if (id && id !== selfId && registry.get(id)?.element === host) return id;
1839
2043
  }
1840
2044
  return null;
1841
2045
  },
1842
2046
  markGroupTarget(id) {
1843
2047
  if (groupTargetId === id) return;
1844
- if (groupTargetId) {
1845
- const previous = registry.get(groupTargetId);
1846
- if (previous) delete previous.element.dataset.wmTabTarget;
1847
- }
2048
+ if (groupTargetEl) delete groupTargetEl.dataset.wmTabTarget;
1848
2049
  groupTargetId = id;
1849
- if (id) {
1850
- const next = registry.get(id);
1851
- if (next) next.element.dataset.wmTabTarget = "";
1852
- }
2050
+ groupTargetEl = id ? registry.get(id)?.element ?? null : null;
2051
+ if (groupTargetEl) groupTargetEl.dataset.wmTabTarget = "";
1853
2052
  },
1854
2053
  currentDrag: () => drag,
1855
2054
  claimDrag(session) {
@@ -1868,7 +2067,7 @@ function attachDesktop(wm, element, options = {}) {
1868
2067
  observer.observe(element);
1869
2068
  cleanup.push(() => observer.disconnect());
1870
2069
  }
1871
- function syncWindow(attached, win, zIndex, activeWorkspace, activeTab) {
2070
+ function syncWindow(attached, win, activeWorkspace, activeTab) {
1872
2071
  const el = attached.element;
1873
2072
  const firstSync = attached.lastState === null;
1874
2073
  if (firstSync) el.style.transition = "none";
@@ -1906,27 +2105,44 @@ function attachDesktop(wm, element, options = {}) {
1906
2105
  attached.lastWorkspace = activeWorkspace;
1907
2106
  attached.lastTab = activeTab;
1908
2107
  }
1909
- if (attached.lastZ !== zIndex) {
1910
- el.style.zIndex = String(zIndex + 1);
1911
- attached.lastZ = zIndex;
1912
- }
1913
2108
  if (firstSync) {
1914
2109
  void el.offsetWidth;
1915
2110
  el.style.transition = "";
1916
2111
  }
1917
2112
  }
2113
+ const stackRows = [];
2114
+ const stackTarget = {
2115
+ length: 0,
2116
+ zAt: (index) => stackRows[index].lastZ,
2117
+ assign(index, z) {
2118
+ const attached = stackRows[index];
2119
+ attached.lastZ = z;
2120
+ attached.element.style.zIndex = String(z);
2121
+ }
2122
+ };
2123
+ function applyStacking(order) {
2124
+ let length = 0;
2125
+ for (const id of order) {
2126
+ const attached = registry.get(id);
2127
+ if (attached) stackRows[length++] = attached;
2128
+ }
2129
+ stackTarget.length = length;
2130
+ restack(stackTarget, { base: zIndexBase, gap: zIndexGap });
2131
+ stackRows.length = length;
2132
+ }
1918
2133
  function syncAll() {
1919
2134
  const state = wm.getState();
1920
2135
  const orderChanged = state.order !== lastOrder;
1921
- state.order.forEach((id, index) => {
2136
+ for (const id of state.order) {
1922
2137
  const attached = registry.get(id);
1923
2138
  const win = state.windows[id];
1924
- if (!attached || !win) return;
2139
+ if (!attached || !win) continue;
1925
2140
  const activeTab = win.groupId === null || state.groups[win.groupId]?.activeId === win.id;
1926
- if (orderChanged || attached.lastState !== win || attached.lastWorkspace !== state.workspace || attached.lastTab !== activeTab) {
1927
- syncWindow(attached, win, index, state.workspace, activeTab);
2141
+ if (attached.lastState !== win || attached.lastWorkspace !== state.workspace || attached.lastTab !== activeTab) {
2142
+ syncWindow(attached, win, state.workspace, activeTab);
1928
2143
  }
1929
- });
2144
+ }
2145
+ if (orderChanged) applyStacking(state.order);
1930
2146
  if (state.focusedId !== lastFocused) {
1931
2147
  if (lastFocused) {
1932
2148
  const prev = registry.get(lastFocused);
@@ -1964,12 +2180,17 @@ function attachDesktop(wm, element, options = {}) {
1964
2180
  wm.on("stage", ({ window: win, previous }) => {
1965
2181
  const attached = registry.get(win.id);
1966
2182
  if (!attached) return;
2183
+ if (!animationEnabled) return;
1967
2184
  if (win.stage === "minimized" && previous !== "minimized") {
1968
2185
  const target = options.minimizeTarget?.(win);
1969
- if (target) flipToTarget(attached.element, target);
2186
+ if (target) flipToTarget(attached.element, target, animationOptions);
1970
2187
  } else if (previous === "minimized" && win.stage !== "minimized") {
1971
2188
  const target = options.minimizeTarget?.(win);
1972
- if (target) view.requestAnimationFrame(() => flipFromTarget(attached.element, target));
2189
+ if (target) {
2190
+ view.requestAnimationFrame(
2191
+ () => flipFromTarget(attached.element, target, animationOptions)
2192
+ );
2193
+ }
1973
2194
  }
1974
2195
  })
1975
2196
  );
@@ -1983,7 +2204,7 @@ function attachDesktop(wm, element, options = {}) {
1983
2204
  if (!event.ctrlKey && !event.metaKey) return;
1984
2205
  if (drag) return;
1985
2206
  const target = event.target;
1986
- if (target?.closest(INTERACTIVE_SELECTOR)) return;
2207
+ if (target?.closest(interactiveSelector)) return;
1987
2208
  if (historyShortcuts && (event.key === "z" || event.key === "Z")) {
1988
2209
  event.preventDefault();
1989
2210
  if (event.shiftKey) wm.redo();
@@ -2025,7 +2246,7 @@ function attachDesktop(wm, element, options = {}) {
2025
2246
  handle: null,
2026
2247
  handles: [],
2027
2248
  lastState: null,
2028
- lastZ: -1,
2249
+ lastZ: UNASSIGNED,
2029
2250
  lastWorkspace: -1,
2030
2251
  lastTab: true,
2031
2252
  cleanup: []
@@ -2059,7 +2280,7 @@ function attachDesktop(wm, element, options = {}) {
2059
2280
  const current = wm.get(id);
2060
2281
  if (!current) return;
2061
2282
  if (target.closest("[data-wm-close]")) {
2062
- if (current.closable) wm.close(id);
2283
+ if (current.closable && options.beforeClose?.(current) !== false) wm.close(id);
2063
2284
  } else if (target.closest("[data-wm-minimize]")) {
2064
2285
  if (current.minimizable) wm.minimize(id);
2065
2286
  } else if (target.closest("[data-wm-maximize]")) {
@@ -2075,7 +2296,7 @@ function attachDesktop(wm, element, options = {}) {
2075
2296
  attached.cleanup.push(() => handle.removeEventListener("pointerdown", onHandleDown));
2076
2297
  const onDoubleClick = (event) => {
2077
2298
  const target = event.target;
2078
- if (target?.closest(INTERACTIVE_SELECTOR)) return;
2299
+ if (target?.closest(interactiveSelector)) return;
2079
2300
  const current = wm.get(id);
2080
2301
  if (current?.maximizable) wm.toggleMaximize(id);
2081
2302
  };
@@ -2107,7 +2328,7 @@ function attachDesktop(wm, element, options = {}) {
2107
2328
  }
2108
2329
  const onWindowKeydown = (event) => {
2109
2330
  const target = event.target;
2110
- if (target?.closest(INTERACTIVE_SELECTOR)) return;
2331
+ if (target?.closest(interactiveSelector)) return;
2111
2332
  const current = wm.get(id);
2112
2333
  if (!current) return;
2113
2334
  const arrows = {
@@ -2267,5 +2488,5 @@ exports.flipToTarget = flipToTarget;
2267
2488
  exports.magnetize = magnetize;
2268
2489
  exports.prefersReducedMotion = prefersReducedMotion;
2269
2490
  exports.zoneBounds = zoneBounds;
2270
- //# sourceMappingURL=chunk-RXAMUBUJ.cjs.map
2271
- //# sourceMappingURL=chunk-RXAMUBUJ.cjs.map
2491
+ //# sourceMappingURL=chunk-DYZQY3BE.cjs.map
2492
+ //# sourceMappingURL=chunk-DYZQY3BE.cjs.map