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