@surdeddd/wmkit 0.3.0 → 0.4.1

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 (60) hide show
  1. package/README.md +42 -16
  2. package/README.ru.md +154 -10
  3. package/dist/angular.cjs +4 -3
  4. package/dist/angular.cjs.map +1 -1
  5. package/dist/angular.d.cts +2 -2
  6. package/dist/angular.d.ts +2 -2
  7. package/dist/angular.js +2 -1
  8. package/dist/angular.js.map +1 -1
  9. package/dist/{binder-0Oku22Nc.d.ts → binder-B2A2IZGh.d.ts} +7 -2
  10. package/dist/{binder-Dq_AZRbo.d.cts → binder-Dl07SXIM.d.cts} +7 -2
  11. package/dist/{chunk-YCHJXSTC.js → chunk-OOEDFA7K.js} +404 -135
  12. package/dist/chunk-OOEDFA7K.js.map +1 -0
  13. package/dist/{chunk-B35DANTX.cjs → chunk-TJ6QOOJ2.cjs} +405 -134
  14. package/dist/chunk-TJ6QOOJ2.cjs.map +1 -0
  15. package/dist/index.cjs +24 -16
  16. package/dist/index.d.cts +6 -5
  17. package/dist/index.d.ts +6 -5
  18. package/dist/index.js +1 -1
  19. package/dist/persist.cjs +22 -3
  20. package/dist/persist.cjs.map +1 -1
  21. package/dist/persist.d.cts +3 -1
  22. package/dist/persist.d.ts +3 -1
  23. package/dist/persist.js +22 -3
  24. package/dist/persist.js.map +1 -1
  25. package/dist/popout.cjs +3 -4
  26. package/dist/popout.cjs.map +1 -1
  27. package/dist/popout.d.cts +1 -1
  28. package/dist/popout.d.ts +1 -1
  29. package/dist/popout.js +3 -4
  30. package/dist/popout.js.map +1 -1
  31. package/dist/react.cjs +4 -3
  32. package/dist/react.cjs.map +1 -1
  33. package/dist/react.d.cts +2 -2
  34. package/dist/react.d.ts +2 -2
  35. package/dist/react.js +2 -1
  36. package/dist/react.js.map +1 -1
  37. package/dist/solid.cjs +4 -3
  38. package/dist/solid.cjs.map +1 -1
  39. package/dist/solid.d.cts +2 -2
  40. package/dist/solid.d.ts +2 -2
  41. package/dist/solid.js +2 -1
  42. package/dist/solid.js.map +1 -1
  43. package/dist/svelte.cjs +8 -5
  44. package/dist/svelte.cjs.map +1 -1
  45. package/dist/svelte.d.cts +2 -2
  46. package/dist/svelte.d.ts +2 -2
  47. package/dist/svelte.js +6 -3
  48. package/dist/svelte.js.map +1 -1
  49. package/dist/themes/retro.css +2 -0
  50. package/dist/{types-DdCtDHgt.d.cts → types-t6pyVYKN.d.cts} +21 -7
  51. package/dist/{types-DdCtDHgt.d.ts → types-t6pyVYKN.d.ts} +21 -7
  52. package/dist/vue.cjs +4 -3
  53. package/dist/vue.cjs.map +1 -1
  54. package/dist/vue.d.cts +2 -2
  55. package/dist/vue.d.ts +2 -2
  56. package/dist/vue.js +2 -1
  57. package/dist/vue.js.map +1 -1
  58. package/package.json +7 -7
  59. package/dist/chunk-B35DANTX.cjs.map +0 -1
  60. package/dist/chunk-YCHJXSTC.js.map +0 -1
@@ -38,19 +38,48 @@ function clampSize(size, min, max) {
38
38
  }
39
39
  function clampToViewport(bounds, viewport, minVisible) {
40
40
  if (viewport.width <= 0 || viewport.height <= 0) return bounds;
41
- const x = clamp(bounds.x, minVisible - bounds.width, viewport.width - minVisible);
42
- const y = clamp(bounds.y, 0, Math.max(0, viewport.height - minVisible));
41
+ const visibleX = Math.min(minVisible, bounds.width);
42
+ const visibleY = Math.min(minVisible, bounds.height);
43
+ const x = clamp(bounds.x, visibleX - bounds.width, Math.max(0, viewport.width - visibleX));
44
+ const y = clamp(bounds.y, 0, Math.max(0, viewport.height - visibleY));
43
45
  return x === bounds.x && y === bounds.y ? bounds : { ...bounds, x, y };
44
46
  }
45
47
  function boundsEqual(a, b) {
46
48
  return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
47
49
  }
50
+ function applyAspect(size, ratio, min, max, drive) {
51
+ if (!Number.isFinite(ratio) || ratio <= 0) return clampSize(size, min, max);
52
+ const maxWidth = max ? max.width : Number.POSITIVE_INFINITY;
53
+ const maxHeight = max ? max.height : Number.POSITIVE_INFINITY;
54
+ if (drive === "height") {
55
+ const height = clamp(
56
+ size.height,
57
+ Math.max(min.height, min.width / ratio),
58
+ Math.min(maxHeight, maxWidth / ratio)
59
+ );
60
+ return { width: height * ratio, height };
61
+ }
62
+ const width = clamp(
63
+ size.width,
64
+ Math.max(min.width, min.height * ratio),
65
+ Math.min(maxWidth, maxHeight * ratio)
66
+ );
67
+ return { width, height: width / ratio };
68
+ }
48
69
  function zoneBounds(zone, viewport) {
49
70
  const w = viewport.width;
50
71
  const h = viewport.height;
51
72
  const halfW = Math.round(w / 2);
52
73
  const halfH = Math.round(h / 2);
74
+ const oneThird = Math.round(w / 3);
75
+ const twoThirds = Math.round(w * 2 / 3);
53
76
  switch (zone) {
77
+ case "left-third":
78
+ return { x: 0, y: 0, width: oneThird, height: h };
79
+ case "center-third":
80
+ return { x: oneThird, y: 0, width: twoThirds - oneThird, height: h };
81
+ case "right-third":
82
+ return { x: twoThirds, y: 0, width: w - twoThirds, height: h };
54
83
  case "left":
55
84
  return { x: 0, y: 0, width: halfW, height: h };
56
85
  case "right":
@@ -146,8 +175,14 @@ var ZONES = [
146
175
  "top-left",
147
176
  "top-right",
148
177
  "bottom-left",
149
- "bottom-right"
178
+ "bottom-right",
179
+ "left-third",
180
+ "center-third",
181
+ "right-third"
150
182
  ];
183
+ function normalizeWorkspace(value, fallback) {
184
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : fallback;
185
+ }
151
186
  function createWindowManager(options = {}) {
152
187
  const emitter = createEmitter();
153
188
  const keepInViewport = options.keepInViewport ?? true;
@@ -161,12 +196,14 @@ function createWindowManager(options = {}) {
161
196
  let order = [];
162
197
  let focusedId = null;
163
198
  let viewport = options.viewport ?? { width: 0, height: 0 };
199
+ let workspace = normalizeWorkspace(options.workspace, 0);
164
200
  let seq = 0;
165
201
  let idCounter = 0;
166
202
  let cascadeIndex = 0;
167
203
  let snapshot = null;
168
204
  let batchDepth = 0;
169
205
  let batchDirty = false;
206
+ let historyDirty = false;
170
207
  const pendingEvents = [];
171
208
  const past = [];
172
209
  let future = [];
@@ -177,13 +214,18 @@ function createWindowManager(options = {}) {
177
214
  const layouts = /* @__PURE__ */ new Map();
178
215
  function getState() {
179
216
  if (!snapshot) {
180
- snapshot = { windows, order, focusedId, viewport };
217
+ snapshot = { windows, order, focusedId, viewport, workspace };
181
218
  }
182
219
  return snapshot;
183
220
  }
184
221
  function commit() {
185
222
  snapshot = null;
186
223
  batchDirty = true;
224
+ historyDirty = true;
225
+ }
226
+ function commitQuiet() {
227
+ snapshot = null;
228
+ batchDirty = true;
187
229
  }
188
230
  function flushEvents() {
189
231
  while (pendingEvents.length > 0) {
@@ -221,10 +263,14 @@ function createWindowManager(options = {}) {
221
263
  order = next;
222
264
  return changed;
223
265
  }
266
+ function onActiveWorkspace(win) {
267
+ return win.workspace === workspace;
268
+ }
224
269
  function topModalId() {
225
270
  for (let i = order.length - 1; i >= 0; i -= 1) {
226
271
  const win = windows[order[i]];
227
- if (win.layer === "modal" && win.stage !== "minimized") return win.id;
272
+ if (win.layer === "modal" && win.stage !== "minimized" && onActiveWorkspace(win))
273
+ return win.id;
228
274
  }
229
275
  return null;
230
276
  }
@@ -232,7 +278,7 @@ function createWindowManager(options = {}) {
232
278
  const modal = topModalId();
233
279
  return order.filter((id) => {
234
280
  const win = windows[id];
235
- if (win.stage === "minimized") return false;
281
+ if (win.stage === "minimized" || !onActiveWorkspace(win)) return false;
236
282
  if (modal && win.layer !== "modal") return false;
237
283
  return true;
238
284
  });
@@ -244,8 +290,9 @@ function createWindowManager(options = {}) {
244
290
  function emitFocus(win, previous) {
245
291
  queueEvent(() => emitter.emit("focus", { window: win, previous }));
246
292
  }
247
- function normalizeSize(size, win) {
248
- return clampSize(size, win.minSize, win.maxSize);
293
+ function normalizeSize(size, win, drive = "width") {
294
+ if (win.aspectRatio === null) return clampSize(size, win.minSize, win.maxSize);
295
+ return applyAspect(size, win.aspectRatio, win.minSize, win.maxSize, drive);
249
296
  }
250
297
  function positionForOpen(init) {
251
298
  if (init.x !== void 0 && init.y !== void 0) return { x: init.x, y: init.y };
@@ -267,11 +314,12 @@ function createWindowManager(options = {}) {
267
314
  width: init.maxWidth ?? Number.POSITIVE_INFINITY,
268
315
  height: init.maxHeight ?? Number.POSITIVE_INFINITY
269
316
  } : null;
270
- const size = clampSize(
271
- { width: init.width ?? defaultSize.width, height: init.height ?? defaultSize.height },
272
- minSize,
273
- maxSize
274
- );
317
+ const aspectRatio = init.aspectRatio !== void 0 && Number.isFinite(init.aspectRatio) && init.aspectRatio > 0 ? init.aspectRatio : null;
318
+ const requested = {
319
+ width: init.width ?? defaultSize.width,
320
+ height: init.height ?? defaultSize.height
321
+ };
322
+ const size = aspectRatio === null ? clampSize(requested, minSize, maxSize) : applyAspect(requested, aspectRatio, minSize, maxSize, "width");
275
323
  const position = positionForOpen(init);
276
324
  let bounds = { ...position, ...size };
277
325
  if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
@@ -286,8 +334,10 @@ function createWindowManager(options = {}) {
286
334
  stage: "normal",
287
335
  snapZone: null,
288
336
  layer: init.layer ?? "normal",
337
+ workspace: normalizeWorkspace(init.workspace, workspace),
289
338
  minSize,
290
339
  maxSize,
340
+ aspectRatio,
291
341
  openedSeq: ++seq,
292
342
  draggable: init.draggable ?? true,
293
343
  resizable: init.resizable ?? true,
@@ -305,7 +355,7 @@ function createWindowManager(options = {}) {
305
355
  setWindow(win);
306
356
  raise(id);
307
357
  let focusPayload = null;
308
- if (stage !== "minimized") {
358
+ if (stage !== "minimized" && onActiveWorkspace(win)) {
309
359
  const previous = focusedId;
310
360
  const modal = topModalId();
311
361
  if (!modal || win.layer === "modal") {
@@ -343,11 +393,12 @@ function createWindowManager(options = {}) {
343
393
  function focus(id) {
344
394
  const win = windows[id];
345
395
  if (!win) return false;
396
+ if (!onActiveWorkspace(win)) setWorkspace(win.workspace);
346
397
  const modal = topModalId();
347
398
  if (modal && modal !== id && win.layer !== "modal") {
348
399
  const modalWin = windows[modal];
349
400
  if (modalWin) queueEvent(() => emitter.emit("modalblocked", { window: modalWin }));
350
- commit();
401
+ commitQuiet();
351
402
  return false;
352
403
  }
353
404
  if (win.stage === "minimized") {
@@ -382,6 +433,14 @@ function createWindowManager(options = {}) {
382
433
  function fullBounds() {
383
434
  return { x: 0, y: 0, width: viewport.width, height: viewport.height };
384
435
  }
436
+ function snapBounds(win, zone) {
437
+ const rect = zoneBounds(zone, viewport);
438
+ return { x: rect.x, y: rect.y, ...normalizeSize(rect, win) };
439
+ }
440
+ function focusUnlessBlocked(id) {
441
+ const modal = topModalId();
442
+ if (modal === null || modal === id) focus(id);
443
+ }
385
444
  function applyStage(id, build) {
386
445
  const win = windows[id];
387
446
  if (!win) return false;
@@ -408,7 +467,7 @@ function createWindowManager(options = {}) {
408
467
  function maximize(id) {
409
468
  const result = applyStage(id, (win) => {
410
469
  if (win.stage === "maximized") return null;
411
- const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds;
470
+ const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds ?? win.bounds;
412
471
  return {
413
472
  ...win,
414
473
  stage: "maximized",
@@ -429,22 +488,23 @@ function createWindowManager(options = {}) {
429
488
  function restore(id) {
430
489
  const result = applyStage(id, (win) => {
431
490
  if (win.stage === "minimized") {
432
- const target = win.restoreStage ?? "normal";
433
- if (target === "maximized") {
491
+ const target2 = win.restoreStage ?? "normal";
492
+ if (target2 === "maximized") {
434
493
  return { ...win, stage: "maximized", restoreStage: null, bounds: fullBounds() };
435
494
  }
436
- if (target === "snapped" && win.snapZone) {
495
+ if (target2 === "snapped" && win.snapZone) {
437
496
  return {
438
497
  ...win,
439
498
  stage: "snapped",
440
499
  restoreStage: null,
441
- bounds: zoneBounds(win.snapZone, viewport)
500
+ bounds: snapBounds(win, win.snapZone)
442
501
  };
443
502
  }
444
503
  return { ...win, stage: "normal", restoreStage: null };
445
504
  }
446
505
  if (win.stage === "normal") return null;
447
- const bounds = win.restoreBounds ?? win.bounds;
506
+ const target = win.restoreBounds ?? win.bounds;
507
+ const bounds = { x: target.x, y: target.y, ...normalizeSize(target, win) };
448
508
  return {
449
509
  ...win,
450
510
  stage: "normal",
@@ -454,7 +514,7 @@ function createWindowManager(options = {}) {
454
514
  bounds: keepInViewport ? clampToViewport(bounds, viewport, minVisible) : bounds
455
515
  };
456
516
  });
457
- if (result) focus(id);
517
+ if (result) focusUnlessBlocked(id);
458
518
  return result;
459
519
  }
460
520
  function restoreTo(id, bounds) {
@@ -470,19 +530,19 @@ function createWindowManager(options = {}) {
470
530
  bounds: keepInViewport ? clampToViewport(next, viewport, minVisible) : next
471
531
  };
472
532
  });
473
- if (result) focus(id);
533
+ if (result) focusUnlessBlocked(id);
474
534
  return result;
475
535
  }
476
536
  function snap(id, zone) {
477
537
  const result = applyStage(id, (win) => {
478
- const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds;
538
+ const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds ?? win.bounds;
479
539
  return {
480
540
  ...win,
481
541
  stage: "snapped",
482
542
  snapZone: zone,
483
543
  restoreBounds,
484
544
  restoreStage: null,
485
- bounds: zoneBounds(zone, viewport)
545
+ bounds: snapBounds(win, zone)
486
546
  };
487
547
  });
488
548
  if (result) focus(id);
@@ -509,7 +569,8 @@ function createWindowManager(options = {}) {
509
569
  const win = windows[id];
510
570
  if (!win || win.stage === "maximized" || win.stage === "minimized") return false;
511
571
  const merged = { ...win.bounds, ...patch };
512
- const size = normalizeSize(merged, win);
572
+ const drive = Math.abs(merged.height - win.bounds.height) > Math.abs(merged.width - win.bounds.width) ? "height" : "width";
573
+ const size = normalizeSize(merged, win, drive);
513
574
  let bounds = { x: merged.x, y: merged.y, ...size };
514
575
  if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
515
576
  const becameNormal = win.stage === "snapped";
@@ -530,6 +591,7 @@ function createWindowManager(options = {}) {
530
591
  layer: patch.layer ?? win.layer,
531
592
  minSize: patch.minSize ?? win.minSize,
532
593
  maxSize: patch.maxSize === void 0 ? win.maxSize : patch.maxSize,
594
+ aspectRatio: patch.aspectRatio === void 0 ? win.aspectRatio : patch.aspectRatio !== null && Number.isFinite(patch.aspectRatio) && patch.aspectRatio > 0 ? patch.aspectRatio : null,
533
595
  draggable: patch.draggable ?? win.draggable,
534
596
  resizable: patch.resizable ?? win.resizable,
535
597
  closable: patch.closable ?? win.closable,
@@ -560,16 +622,75 @@ function createWindowManager(options = {}) {
560
622
  if (next.width === viewport.width && next.height === viewport.height) return;
561
623
  viewport = { ...next };
562
624
  reflowViewport();
563
- snapshot = null;
564
- batchDirty = true;
625
+ commitQuiet();
626
+ }
627
+ function setWorkspace(next) {
628
+ const target = normalizeWorkspace(next, workspace);
629
+ if (target === workspace) return false;
630
+ const previous = workspace;
631
+ workspace = target;
632
+ const previousFocus = focusedId;
633
+ focusTop();
634
+ queueEvent(() => emitter.emit("workspace", { workspace: target, previous }));
635
+ if (focusedId && focusedId !== previousFocus) {
636
+ emitFocus(windows[focusedId], previousFocus);
637
+ }
638
+ commit();
639
+ return true;
640
+ }
641
+ function moveToWorkspace(id, next) {
642
+ const win = windows[id];
643
+ if (!win) return false;
644
+ const target = normalizeWorkspace(next, win.workspace);
645
+ if (target === win.workspace) return false;
646
+ const updated = { ...win, workspace: target };
647
+ setWindow(updated);
648
+ if (!onActiveWorkspace(updated) && focusedId === id) {
649
+ focusTop();
650
+ if (focusedId) emitFocus(windows[focusedId], id);
651
+ }
652
+ queueEvent(() => emitter.emit("update", { window: updated }));
653
+ commit();
654
+ return true;
655
+ }
656
+ function sendToBack(id) {
657
+ const win = windows[id];
658
+ if (!win) return false;
659
+ const without = order.filter((entry) => entry !== id);
660
+ const rank = LAYER_RANK[win.layer];
661
+ let insertAt = 0;
662
+ for (const entry of without) {
663
+ if (layerRankOf(entry) < rank) insertAt += 1;
664
+ else break;
665
+ }
666
+ const next = [...without.slice(0, insertAt), id, ...without.slice(insertAt)];
667
+ if (next.every((entry, i) => entry === order[i])) return false;
668
+ order = next;
669
+ queueEvent(() => emitter.emit("order", { order }));
670
+ if (focusedId === id) {
671
+ focusTop();
672
+ if (focusedId) emitFocus(windows[focusedId], id);
673
+ }
674
+ commit();
675
+ return true;
676
+ }
677
+ function center(id) {
678
+ const win = windows[id];
679
+ if (win?.stage !== "normal") return false;
680
+ return move(
681
+ id,
682
+ Math.round((viewport.width - win.bounds.width) / 2),
683
+ Math.round((viewport.height - win.bounds.height) / 2)
684
+ );
565
685
  }
566
686
  function minimized() {
567
- return Object.values(windows).filter((win) => win.stage === "minimized").sort((a, b) => a.openedSeq - b.openedSeq);
687
+ return Object.values(windows).filter((win) => win.stage === "minimized" && onActiveWorkspace(win)).sort((a, b) => a.openedSeq - b.openedSeq);
568
688
  }
569
689
  function captureEntry() {
570
- return { windows, order, focusedId };
690
+ return { windows, order, focusedId, workspace };
571
691
  }
572
692
  function recordHistory() {
693
+ if (!historyDirty) return;
573
694
  if (skipNextHistory) {
574
695
  skipNextHistory = false;
575
696
  return;
@@ -594,6 +715,7 @@ function createWindowManager(options = {}) {
594
715
  if (batchDirty) {
595
716
  batchDirty = false;
596
717
  recordHistory();
718
+ historyDirty = false;
597
719
  snapshot = null;
598
720
  flushEvents();
599
721
  emitter.emit("change", { state: getState() });
@@ -610,7 +732,7 @@ function createWindowManager(options = {}) {
610
732
  setWindow(updated);
611
733
  queueEvent(() => emitter.emit("resize", { window: updated }));
612
734
  } else if (win.stage === "snapped" && win.snapZone) {
613
- const updated = { ...win, bounds: zoneBounds(win.snapZone, viewport) };
735
+ const updated = { ...win, bounds: snapBounds(win, win.snapZone) };
614
736
  setWindow(updated);
615
737
  queueEvent(() => emitter.emit("resize", { window: updated }));
616
738
  } else if (win.stage === "normal" && keepInViewport) {
@@ -627,6 +749,11 @@ function createWindowManager(options = {}) {
627
749
  windows = entry.windows;
628
750
  order = [...entry.order];
629
751
  focusedId = entry.focusedId;
752
+ const previousWorkspace = workspace;
753
+ workspace = entry.workspace;
754
+ if (workspace !== previousWorkspace) {
755
+ queueEvent(() => emitter.emit("workspace", { workspace, previous: previousWorkspace }));
756
+ }
630
757
  reflowViewport();
631
758
  commit();
632
759
  }
@@ -653,6 +780,13 @@ function createWindowManager(options = {}) {
653
780
  function endInteraction() {
654
781
  if (interactionDepth > 0) interactionDepth -= 1;
655
782
  }
783
+ function abortInteraction() {
784
+ if (interactionDepth > 0 && interactionRecorded) {
785
+ past.pop();
786
+ interactionRecorded = false;
787
+ }
788
+ endInteraction();
789
+ }
656
790
  function clearHistory() {
657
791
  past.length = 0;
658
792
  future = [];
@@ -683,7 +817,10 @@ function createWindowManager(options = {}) {
683
817
  return [...layouts.keys()];
684
818
  }
685
819
  function arrange(mode) {
686
- const ids = order.filter((id) => windows[id].stage !== "minimized");
820
+ const ids = order.filter((id) => {
821
+ const win = windows[id];
822
+ return win.stage !== "minimized" && onActiveWorkspace(win);
823
+ });
687
824
  if (ids.length === 0) return;
688
825
  if (mode === "cascade") {
689
826
  ids.forEach((id, index) => {
@@ -698,6 +835,7 @@ function createWindowManager(options = {}) {
698
835
  });
699
836
  return;
700
837
  }
838
+ if (viewport.width <= 0 || viewport.height <= 0) return;
701
839
  const cols = Math.ceil(Math.sqrt(ids.length));
702
840
  const rows = Math.ceil(ids.length / cols);
703
841
  const cellWidth = Math.floor(viewport.width / cols);
@@ -712,7 +850,9 @@ function createWindowManager(options = {}) {
712
850
  });
713
851
  }
714
852
  function minimizeAll() {
715
- for (const id of [...order]) minimize(id);
853
+ for (const id of [...order]) {
854
+ if (onActiveWorkspace(windows[id])) minimize(id);
855
+ }
716
856
  }
717
857
  function restoreAll() {
718
858
  for (const win of minimized()) restore(win.id);
@@ -732,7 +872,8 @@ function createWindowManager(options = {}) {
732
872
  meta: { ...win.meta }
733
873
  })),
734
874
  order: [...order],
735
- focusedId
875
+ focusedId,
876
+ workspace
736
877
  };
737
878
  }
738
879
  function isBounds(value) {
@@ -786,8 +927,10 @@ function createWindowManager(options = {}) {
786
927
  stage: raw.stage,
787
928
  snapZone: ZONES.includes(raw.snapZone) ? raw.snapZone : null,
788
929
  layer: raw.layer,
930
+ workspace: normalizeWorkspace(raw.workspace, 0),
789
931
  minSize: isSize(raw.minSize) ? { ...raw.minSize } : { width: 160, height: 100 },
790
932
  maxSize: readMaxSize(raw.maxSize),
933
+ aspectRatio: typeof raw.aspectRatio === "number" && Number.isFinite(raw.aspectRatio) && raw.aspectRatio > 0 ? raw.aspectRatio : null,
791
934
  openedSeq: typeof raw.openedSeq === "number" ? raw.openedSeq : ++maxSeq,
792
935
  draggable: raw.draggable !== false,
793
936
  resizable: raw.resizable !== false,
@@ -816,8 +959,16 @@ function createWindowManager(options = {}) {
816
959
  order = nextOrder;
817
960
  order = sortByLayer(order);
818
961
  seq = maxSeq;
962
+ const previousWorkspace = workspace;
963
+ workspace = normalizeWorkspace(data.workspace, 0);
964
+ if (workspace !== previousWorkspace) {
965
+ queueEvent(() => emitter.emit("workspace", { workspace, previous: previousWorkspace }));
966
+ }
819
967
  focusedId = data.focusedId && windows[data.focusedId] ? data.focusedId : null;
820
- if (focusedId && windows[focusedId]?.stage === "minimized") focusedId = null;
968
+ const focusedCandidate = focusedId ? windows[focusedId] : void 0;
969
+ if (focusedCandidate && (focusedCandidate.stage === "minimized" || !onActiveWorkspace(focusedCandidate))) {
970
+ focusedId = null;
971
+ }
821
972
  if (!focusedId) focusTop();
822
973
  const modal = topModalId();
823
974
  if (modal) {
@@ -836,6 +987,7 @@ function createWindowManager(options = {}) {
836
987
  queueEvent(() => emitter.emit("open", { window: opened }));
837
988
  }
838
989
  }
990
+ if (viewport.width > 0 && viewport.height > 0) reflowViewport();
839
991
  queueEvent(() => emitter.emit("order", { order }));
840
992
  clearHistory();
841
993
  skipNextHistory = true;
@@ -860,12 +1012,17 @@ function createWindowManager(options = {}) {
860
1012
  snap: (id, zone) => transact(() => snap(id, zone)),
861
1013
  move: (id, x, y) => transact(() => move(id, x, y)),
862
1014
  moveBy: (id, dx, dy) => transact(() => moveBy(id, dx, dy)),
1015
+ center: (id) => transact(() => center(id)),
1016
+ sendToBack: (id) => transact(() => sendToBack(id)),
863
1017
  resize: (id, patch) => transact(() => resize(id, patch)),
864
1018
  update: (id, patch) => transact(() => update(id, patch)),
865
1019
  get: (id) => windows[id],
866
1020
  getState,
867
1021
  minimized,
868
1022
  setViewport: (next) => transact(() => setViewport(next)),
1023
+ workspace: () => workspace,
1024
+ setWorkspace: (next) => transact(() => setWorkspace(next)),
1025
+ moveToWorkspace: (id, next) => transact(() => moveToWorkspace(id, next)),
869
1026
  batch: (run) => transact(run),
870
1027
  serialize,
871
1028
  hydrate: (data) => transact(() => hydrate(data)),
@@ -875,6 +1032,8 @@ function createWindowManager(options = {}) {
875
1032
  canRedo: () => future.length > 0,
876
1033
  beginInteraction,
877
1034
  endInteraction,
1035
+ abortInteraction,
1036
+ clearHistory,
878
1037
  saveLayout,
879
1038
  loadLayout: (name) => transact(() => loadLayout(name)),
880
1039
  getLayout,
@@ -959,7 +1118,8 @@ var defaultMessages = {
959
1118
  restored: (title) => `${title} restored`,
960
1119
  maximized: (title) => `${title} maximized`,
961
1120
  snapped: (title, zone) => `${title} snapped to ${zone.replace("-", " ")}`,
962
- focused: (title) => `${title} focused`
1121
+ focused: (title) => `${title} focused`,
1122
+ workspace: (index) => `workspace ${index + 1}`
963
1123
  };
964
1124
  function createAnnouncer(wm, container, messages = {}) {
965
1125
  const dict = { ...defaultMessages, ...messages };
@@ -970,7 +1130,10 @@ function createAnnouncer(wm, container, messages = {}) {
970
1130
  element.style.cssText = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0";
971
1131
  container.append(element);
972
1132
  let clearTimer;
1133
+ let lastMessage = "";
1134
+ let detailed = false;
973
1135
  function announce(message) {
1136
+ lastMessage = message;
974
1137
  element.textContent = message;
975
1138
  if (clearTimer !== void 0) clearTimeout(clearTimer);
976
1139
  clearTimer = setTimeout(() => {
@@ -986,6 +1149,19 @@ function createAnnouncer(wm, container, messages = {}) {
986
1149
  else if (win.stage === "snapped" && win.snapZone)
987
1150
  announce(dict.snapped(win.title, win.snapZone));
988
1151
  else if (win.stage === "normal" && previous !== "normal") announce(dict.restored(win.title));
1152
+ else return;
1153
+ detailed = true;
1154
+ }),
1155
+ wm.on("focus", ({ window: win, previous }) => {
1156
+ if (detailed || previous === null || lastMessage === dict.opened(win.title)) return;
1157
+ announce(dict.focused(win.title));
1158
+ }),
1159
+ wm.on("workspace", ({ workspace }) => {
1160
+ announce(dict.workspace(workspace));
1161
+ detailed = true;
1162
+ }),
1163
+ wm.subscribe(() => {
1164
+ detailed = false;
989
1165
  })
990
1166
  ];
991
1167
  return {
@@ -1074,7 +1250,9 @@ function createDragStarter(ctx) {
1074
1250
  for (const otherId of state.order) {
1075
1251
  if (otherId === id) continue;
1076
1252
  const other = state.windows[otherId];
1077
- if (other && other.stage !== "minimized") targets.push(other.bounds);
1253
+ if (other && other.stage !== "minimized" && other.workspace === state.workspace) {
1254
+ targets.push(other.bounds);
1255
+ }
1078
1256
  }
1079
1257
  const magnet = magnetize(
1080
1258
  { x: nextX, y: nextY, width: current.bounds.width, height: current.bounds.height },
@@ -1108,7 +1286,7 @@ function createDragStarter(ctx) {
1108
1286
  const movePoint = ctx.toLocal(moveEvent);
1109
1287
  if (!session.moved) {
1110
1288
  const travelled = Math.abs(movePoint.x - (session.startBounds.x + session.grabDX)) + Math.abs(movePoint.y - (session.startBounds.y + session.grabDY));
1111
- if (travelled < 3 && session.restored) return;
1289
+ if (travelled < 3) return;
1112
1290
  session.moved = true;
1113
1291
  if (el) {
1114
1292
  el.dataset.wmDragging = "";
@@ -1136,42 +1314,43 @@ function createDragStarter(ctx) {
1136
1314
  }
1137
1315
  function finish(cancelled) {
1138
1316
  if (ctx.currentDrag() !== session) return;
1139
- if (session.raf !== 0) view.cancelAnimationFrame(session.raf);
1140
- if (session.hasPending && !cancelled) flush();
1141
- ctx.releaseDrag(session);
1142
- releaseRect();
1143
- handle.removeEventListener("pointermove", onMove);
1144
- handle.removeEventListener("pointerup", onUp);
1145
- handle.removeEventListener("pointercancel", onCancel);
1146
- doc.removeEventListener("keydown", onKeydown, true);
1147
- if (handle.hasPointerCapture(session.pointerId)) {
1148
- handle.releasePointerCapture(session.pointerId);
1149
- }
1150
- if (el) {
1151
- delete el.dataset.wmDragging;
1152
- el.style.willChange = "";
1153
- }
1154
- ctx.hidePreview();
1155
- if (cancelled) {
1156
- if (session.moved && session.restored) {
1157
- if (session.startStage === "maximized") {
1158
- wm.restoreTo(id, session.startRestoreBounds ?? session.startBounds);
1159
- wm.maximize(id);
1160
- } else if (session.startStage === "snapped" && session.startZone) {
1161
- wm.restoreTo(id, session.startRestoreBounds ?? session.startBounds);
1162
- wm.snap(id, session.startZone);
1163
- } else {
1164
- wm.move(id, session.startBounds.x, session.startBounds.y);
1317
+ try {
1318
+ if (session.raf !== 0) view.cancelAnimationFrame(session.raf);
1319
+ if (session.hasPending && !cancelled) flush();
1320
+ if (cancelled) {
1321
+ if (session.moved && session.restored) {
1322
+ if (session.startStage === "maximized") {
1323
+ wm.restoreTo(id, session.startRestoreBounds ?? session.startBounds);
1324
+ wm.maximize(id);
1325
+ } else if (session.startStage === "snapped" && session.startZone) {
1326
+ wm.restoreTo(id, session.startRestoreBounds ?? session.startBounds);
1327
+ wm.snap(id, session.startZone);
1328
+ } else {
1329
+ wm.move(id, session.startBounds.x, session.startBounds.y);
1330
+ }
1165
1331
  }
1332
+ } else if (session.moved && session.zone) {
1333
+ if (session.zone === "maximize") wm.maximize(id);
1334
+ else wm.snap(id, session.zone);
1166
1335
  }
1167
- wm.endInteraction();
1168
- return;
1169
- }
1170
- if (session.moved && session.zone) {
1171
- if (session.zone === "maximize") wm.maximize(id);
1172
- else wm.snap(id, session.zone);
1336
+ } finally {
1337
+ ctx.releaseDrag(session);
1338
+ releaseRect();
1339
+ handle.removeEventListener("pointermove", onMove);
1340
+ handle.removeEventListener("pointerup", onUp);
1341
+ handle.removeEventListener("pointercancel", onCancel);
1342
+ doc.removeEventListener("keydown", onKeydown, true);
1343
+ if (handle.hasPointerCapture(session.pointerId)) {
1344
+ handle.releasePointerCapture(session.pointerId);
1345
+ }
1346
+ if (el) {
1347
+ delete el.dataset.wmDragging;
1348
+ el.style.willChange = "";
1349
+ }
1350
+ ctx.hidePreview();
1351
+ if (cancelled) wm.abortInteraction();
1352
+ else wm.endInteraction();
1173
1353
  }
1174
- wm.endInteraction();
1175
1354
  }
1176
1355
  session.finish = finish;
1177
1356
  wm.beginInteraction();
@@ -1204,41 +1383,42 @@ function createResizeStarter(ctx) {
1204
1383
  event.preventDefault();
1205
1384
  event.stopPropagation();
1206
1385
  const handleEl = event.currentTarget;
1386
+ const session = { id, finish: (cancelled) => finish(cancelled) };
1387
+ ctx.claimDrag(session);
1207
1388
  wm.beginInteraction();
1208
1389
  const releaseRect = ctx.trackRect();
1209
1390
  const startPoint = ctx.toLocal(event);
1210
1391
  const start = win.bounds;
1392
+ const startStage = win.stage;
1393
+ const startZone = win.snapZone;
1394
+ const startRestore = win.restoreBounds;
1211
1395
  const minSize = win.minSize;
1212
1396
  const maxSize = win.maxSize;
1397
+ const aspect = win.aspectRatio;
1398
+ const drive = direction === "n" || direction === "s" ? "height" : "width";
1213
1399
  let raf = 0;
1214
1400
  let pendingX = 0;
1215
1401
  let pendingY = 0;
1216
1402
  let hasPending = false;
1217
1403
  const el = ctx.windowElement(id);
1218
1404
  if (el) el.dataset.wmResizing = direction;
1219
- function clampWidth(width) {
1220
- return clamp(width, minSize.width, maxSize ? maxSize.width : Number.POSITIVE_INFINITY);
1221
- }
1222
- function clampHeight(height) {
1223
- return clamp(height, minSize.height, maxSize ? maxSize.height : Number.POSITIVE_INFINITY);
1224
- }
1225
1405
  function flush() {
1226
1406
  if (!hasPending) return;
1227
1407
  hasPending = false;
1228
1408
  raf = 0;
1229
1409
  const dx = pendingX - startPoint.x;
1230
1410
  const dy = pendingY - startPoint.y;
1231
- const next = { ...start };
1232
- if (direction.includes("e")) next.width = clampWidth(start.width + dx);
1233
- if (direction.includes("s")) next.height = clampHeight(start.height + dy);
1234
- if (direction.includes("w")) {
1235
- next.width = clampWidth(start.width - dx);
1236
- next.x = start.x + (start.width - next.width);
1237
- }
1238
- if (direction.includes("n")) {
1239
- next.height = clampHeight(start.height - dy);
1240
- next.y = start.y + (start.height - next.height);
1241
- }
1411
+ const top = direction.includes("n") ? Math.max(0, start.y + dy) : start.y;
1412
+ const raw = {
1413
+ width: direction.includes("e") ? start.width + dx : direction.includes("w") ? start.width - dx : start.width,
1414
+ height: direction.includes("s") ? start.height + dy : direction.includes("n") ? start.y + start.height - top : start.height
1415
+ };
1416
+ const size = aspect === null ? clampSize(raw, minSize, maxSize) : applyAspect(raw, aspect, minSize, maxSize, drive);
1417
+ const next = {
1418
+ x: direction.includes("w") ? start.x + start.width - size.width : start.x,
1419
+ y: direction.includes("n") ? start.y + start.height - size.height : start.y,
1420
+ ...size
1421
+ };
1242
1422
  wm.resize(id, next);
1243
1423
  }
1244
1424
  function onMove(moveEvent) {
@@ -1250,19 +1430,32 @@ function createResizeStarter(ctx) {
1250
1430
  if (raf === 0) raf = view.requestAnimationFrame(flush);
1251
1431
  }
1252
1432
  function finish(cancelled) {
1253
- if (raf !== 0) view.cancelAnimationFrame(raf);
1254
- if (hasPending && !cancelled) flush();
1255
- releaseRect();
1256
- handleEl.removeEventListener("pointermove", onMove);
1257
- handleEl.removeEventListener("pointerup", onUp);
1258
- handleEl.removeEventListener("pointercancel", onCancelPointer);
1259
- doc.removeEventListener("keydown", onKeydown, true);
1260
- if (handleEl.hasPointerCapture(event.pointerId)) {
1261
- handleEl.releasePointerCapture(event.pointerId);
1433
+ if (ctx.currentDrag() !== session) return;
1434
+ try {
1435
+ if (raf !== 0) view.cancelAnimationFrame(raf);
1436
+ if (hasPending && !cancelled) flush();
1437
+ if (cancelled) {
1438
+ if (startStage === "snapped" && startZone) {
1439
+ wm.restoreTo(id, startRestore ?? start);
1440
+ wm.snap(id, startZone);
1441
+ } else {
1442
+ wm.resize(id, start);
1443
+ }
1444
+ }
1445
+ } finally {
1446
+ ctx.releaseDrag(session);
1447
+ releaseRect();
1448
+ handleEl.removeEventListener("pointermove", onMove);
1449
+ handleEl.removeEventListener("pointerup", onUp);
1450
+ handleEl.removeEventListener("pointercancel", onCancelPointer);
1451
+ doc.removeEventListener("keydown", onKeydown, true);
1452
+ if (handleEl.hasPointerCapture(event.pointerId)) {
1453
+ handleEl.releasePointerCapture(event.pointerId);
1454
+ }
1455
+ if (el) delete el.dataset.wmResizing;
1456
+ if (cancelled) wm.abortInteraction();
1457
+ else wm.endInteraction();
1262
1458
  }
1263
- if (el) delete el.dataset.wmResizing;
1264
- if (cancelled) wm.resize(id, start);
1265
- wm.endInteraction();
1266
1459
  }
1267
1460
  function onUp(upEvent) {
1268
1461
  if (upEvent.pointerId !== event.pointerId) return;
@@ -1307,6 +1500,12 @@ function createResizeHandles(doc, edge, corner) {
1307
1500
  }
1308
1501
 
1309
1502
  // src/dom/controller.ts
1503
+ var SNAP_SHORTCUTS = {
1504
+ ArrowLeft: "left",
1505
+ ArrowRight: "right"
1506
+ };
1507
+ var LANDMARK_TAGS = /* @__PURE__ */ new Set(["header", "footer", "aside", "nav"]);
1508
+ var FOCUSABLE_SELECTOR = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
1310
1509
  function attachDesktop(wm, element, options = {}) {
1311
1510
  const doc = element.ownerDocument;
1312
1511
  const view = windowOf(element);
@@ -1319,10 +1518,13 @@ function attachDesktop(wm, element, options = {}) {
1319
1518
  const keyboardOptions = typeof options.keyboard === "object" ? options.keyboard : {};
1320
1519
  const moveStep = keyboardOptions.moveStep ?? 16;
1321
1520
  const cycleEnabled = keyboardOptions.cycle !== false;
1521
+ const snapShortcuts = keyboardOptions.snapShortcuts !== false;
1522
+ const historyShortcuts = keyboardOptions.historyShortcuts !== false;
1322
1523
  const hitEdge = options.hitAreas?.edge ?? (coarsePointer ? 16 : 8);
1323
1524
  const hitCorner = options.hitAreas?.corner ?? (coarsePointer ? 24 : 12);
1324
1525
  const magnetThreshold = options.magnetism === false ? 0 : (typeof options.magnetism === "object" ? options.magnetism.threshold : void 0) ?? (coarsePointer ? 12 : 8);
1325
1526
  element.dataset.wmDesktop = "";
1527
+ element.tabIndex = -1;
1326
1528
  if (view.getComputedStyle(element).position === "static") {
1327
1529
  element.style.position = "relative";
1328
1530
  }
@@ -1359,6 +1561,10 @@ function attachDesktop(wm, element, options = {}) {
1359
1561
  function hidePreview() {
1360
1562
  if (preview) preview.style.display = "none";
1361
1563
  }
1564
+ function refreshRect() {
1565
+ const rect = element.getBoundingClientRect();
1566
+ cachedRect = { left: rect.left, top: rect.top };
1567
+ }
1362
1568
  const ctx = {
1363
1569
  wm,
1364
1570
  doc,
@@ -1369,8 +1575,9 @@ function attachDesktop(wm, element, options = {}) {
1369
1575
  },
1370
1576
  trackRect() {
1371
1577
  if (rectUsers === 0) {
1372
- const rect = element.getBoundingClientRect();
1373
- cachedRect = { left: rect.left, top: rect.top };
1578
+ refreshRect();
1579
+ view.addEventListener("scroll", refreshRect, true);
1580
+ view.addEventListener("resize", refreshRect);
1374
1581
  }
1375
1582
  rectUsers += 1;
1376
1583
  let released = false;
@@ -1378,7 +1585,11 @@ function attachDesktop(wm, element, options = {}) {
1378
1585
  if (released) return;
1379
1586
  released = true;
1380
1587
  rectUsers -= 1;
1381
- if (rectUsers === 0) cachedRect = null;
1588
+ if (rectUsers === 0) {
1589
+ view.removeEventListener("scroll", refreshRect, true);
1590
+ view.removeEventListener("resize", refreshRect);
1591
+ cachedRect = null;
1592
+ }
1382
1593
  };
1383
1594
  },
1384
1595
  windowElement: (id) => registry.get(id)?.element,
@@ -1406,28 +1617,34 @@ function attachDesktop(wm, element, options = {}) {
1406
1617
  if (options.autoViewport !== false) {
1407
1618
  const applyViewport = () => wm.setViewport({ width: element.clientWidth, height: element.clientHeight });
1408
1619
  applyViewport();
1409
- const observer = new ResizeObserver(applyViewport);
1620
+ const observer = new view.ResizeObserver(applyViewport);
1410
1621
  observer.observe(element);
1411
1622
  cleanup.push(() => observer.disconnect());
1412
1623
  }
1413
- function syncWindow(attached, win, zIndex) {
1624
+ function syncWindow(attached, win, zIndex, activeWorkspace) {
1414
1625
  const el = attached.element;
1415
1626
  const firstSync = attached.lastState === null;
1416
1627
  if (firstSync) el.style.transition = "none";
1417
- if (attached.lastState !== win) {
1628
+ if (attached.lastState !== win || attached.lastWorkspace !== activeWorkspace) {
1418
1629
  el.style.transform = `translate3d(${win.bounds.x}px, ${win.bounds.y}px, 0)`;
1419
1630
  el.style.width = `${win.bounds.width}px`;
1420
1631
  el.style.height = `${win.bounds.height}px`;
1421
1632
  el.dataset.wmStage = win.stage;
1422
1633
  el.dataset.wmLayer = win.layer;
1423
- el.hidden = win.stage === "minimized";
1634
+ el.dataset.wmWorkspace = String(win.workspace);
1635
+ const hide = win.stage === "minimized" || win.workspace !== activeWorkspace;
1636
+ if (hide && !el.hidden && el.contains(doc.activeElement)) {
1637
+ element.focus({ preventScroll: true });
1638
+ }
1639
+ el.hidden = hide;
1424
1640
  el.setAttribute("aria-label", win.title);
1425
1641
  if (win.layer === "modal") el.setAttribute("aria-modal", "true");
1426
1642
  else el.removeAttribute("aria-modal");
1427
1643
  for (const handle of attached.handles) {
1428
- handle.style.display = win.resizable && win.stage === "normal" ? "" : "none";
1644
+ handle.style.display = win.resizable && (win.stage === "normal" || win.stage === "snapped") ? "" : "none";
1429
1645
  }
1430
1646
  attached.lastState = win;
1647
+ attached.lastWorkspace = activeWorkspace;
1431
1648
  }
1432
1649
  if (attached.lastZ !== zIndex) {
1433
1650
  el.style.zIndex = String(zIndex + 1);
@@ -1445,7 +1662,9 @@ function attachDesktop(wm, element, options = {}) {
1445
1662
  const attached = registry.get(id);
1446
1663
  const win = state.windows[id];
1447
1664
  if (!attached || !win) return;
1448
- if (orderChanged || attached.lastState !== win) syncWindow(attached, win, index);
1665
+ if (orderChanged || attached.lastState !== win || attached.lastWorkspace !== state.workspace) {
1666
+ syncWindow(attached, win, index, state.workspace);
1667
+ }
1449
1668
  });
1450
1669
  if (state.focusedId !== lastFocused) {
1451
1670
  if (lastFocused) {
@@ -1488,15 +1707,44 @@ function attachDesktop(wm, element, options = {}) {
1488
1707
  if (target) flipToTarget(attached.element, target);
1489
1708
  } else if (previous === "minimized" && win.stage !== "minimized") {
1490
1709
  const target = options.minimizeTarget?.(win);
1491
- if (target) flipFromTarget(attached.element, target);
1710
+ if (target) view.requestAnimationFrame(() => flipFromTarget(attached.element, target));
1492
1711
  }
1493
1712
  })
1494
1713
  );
1495
- if (keyboardEnabled && cycleEnabled) {
1714
+ if (keyboardEnabled) {
1496
1715
  const onDesktopKeydown = (event) => {
1497
- if (event.key === "F6") {
1716
+ if (cycleEnabled && event.key === "F6") {
1498
1717
  event.preventDefault();
1499
1718
  wm.cycleFocus(event.shiftKey ? -1 : 1);
1719
+ return;
1720
+ }
1721
+ if (!event.ctrlKey && !event.metaKey) return;
1722
+ const target = event.target;
1723
+ if (target?.closest(INTERACTIVE_SELECTOR)) return;
1724
+ if (historyShortcuts && (event.key === "z" || event.key === "Z")) {
1725
+ event.preventDefault();
1726
+ if (event.shiftKey) wm.redo();
1727
+ else wm.undo();
1728
+ return;
1729
+ }
1730
+ if (!snapShortcuts || !event.altKey) return;
1731
+ const focused = wm.getState().focusedId;
1732
+ const win = focused ? wm.get(focused) : void 0;
1733
+ if (!win) return;
1734
+ const zone = SNAP_SHORTCUTS[event.key];
1735
+ if (zone) {
1736
+ event.preventDefault();
1737
+ if (win.snappable) wm.snap(win.id, zone);
1738
+ } else if (event.key === "ArrowUp") {
1739
+ event.preventDefault();
1740
+ if (win.maximizable) wm.maximize(win.id);
1741
+ } else if (event.key === "ArrowDown") {
1742
+ event.preventDefault();
1743
+ if (win.stage === "normal") {
1744
+ if (win.minimizable) wm.minimize(win.id);
1745
+ } else {
1746
+ wm.restore(win.id);
1747
+ }
1500
1748
  }
1501
1749
  };
1502
1750
  element.addEventListener("keydown", onDesktopKeydown);
@@ -1515,6 +1763,7 @@ function attachDesktop(wm, element, options = {}) {
1515
1763
  handles: [],
1516
1764
  lastState: null,
1517
1765
  lastZ: -1,
1766
+ lastWorkspace: -1,
1518
1767
  cleanup: []
1519
1768
  };
1520
1769
  windowElement.dataset.wmWindow = id;
@@ -1530,6 +1779,9 @@ function attachDesktop(wm, element, options = {}) {
1530
1779
  }
1531
1780
  const handle = typeof windowOptions.handle === "string" ? windowElement.querySelector(windowOptions.handle) : windowOptions.handle ?? windowElement.querySelector("[data-wm-drag]");
1532
1781
  attached.handle = handle;
1782
+ if (handle && !handle.hasAttribute("role") && LANDMARK_TAGS.has(handle.localName)) {
1783
+ handle.setAttribute("role", "presentation");
1784
+ }
1533
1785
  const onPointerDownFocus = () => {
1534
1786
  wm.focus(id);
1535
1787
  };
@@ -1600,19 +1852,20 @@ function attachDesktop(wm, element, options = {}) {
1600
1852
  ArrowUp: [0, -1],
1601
1853
  ArrowDown: [0, 1]
1602
1854
  };
1855
+ if (event.ctrlKey || event.metaKey) return;
1603
1856
  const vector = arrows[event.key];
1604
1857
  if (!vector || !keyboardEnabled) return;
1605
- event.preventDefault();
1606
1858
  const step = event.altKey ? 1 : moveStep;
1607
1859
  const [dx, dy] = vector;
1608
1860
  if (event.shiftKey) {
1609
- if (current.resizable) {
1610
- wm.resize(id, {
1611
- width: current.bounds.width + dx * step,
1612
- height: current.bounds.height + dy * step
1613
- });
1614
- }
1861
+ if (!current.resizable) return;
1862
+ event.preventDefault();
1863
+ wm.resize(id, {
1864
+ width: current.bounds.width + dx * step,
1865
+ height: current.bounds.height + dy * step
1866
+ });
1615
1867
  } else if (current.draggable && current.stage === "normal") {
1868
+ event.preventDefault();
1616
1869
  wm.moveBy(id, dx * step, dy * step);
1617
1870
  }
1618
1871
  };
@@ -1622,13 +1875,18 @@ function attachDesktop(wm, element, options = {}) {
1622
1875
  if (event.key !== "Tab") return;
1623
1876
  const current = wm.get(id);
1624
1877
  if (current?.layer !== "modal") return;
1625
- const focusables = windowElement.querySelectorAll(
1626
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
1627
- );
1878
+ const focusables = [
1879
+ ...windowElement.querySelectorAll(FOCUSABLE_SELECTOR)
1880
+ ].filter((node) => node.offsetParent !== null || node === doc.activeElement);
1628
1881
  if (focusables.length === 0) return;
1629
1882
  const first = focusables[0];
1630
1883
  const last = focusables[focusables.length - 1];
1631
1884
  if (!first || !last) return;
1885
+ if (!windowElement.contains(doc.activeElement)) {
1886
+ event.preventDefault();
1887
+ (event.shiftKey ? last : first).focus();
1888
+ return;
1889
+ }
1632
1890
  if (event.shiftKey && doc.activeElement === first) {
1633
1891
  event.preventDefault();
1634
1892
  last.focus();
@@ -1657,6 +1915,9 @@ function attachDesktop(wm, element, options = {}) {
1657
1915
  lastOrder = null;
1658
1916
  lastFocused = null;
1659
1917
  syncAll();
1918
+ if (wm.getState().focusedId === id && !windowElement.contains(doc.activeElement)) {
1919
+ windowElement.focus({ preventScroll: true });
1920
+ }
1660
1921
  return detach;
1661
1922
  }
1662
1923
  return {
@@ -1680,29 +1941,33 @@ function attachDesktop(wm, element, options = {}) {
1680
1941
  // src/dom/binder.ts
1681
1942
  function createDesktopBinder(wm, options = {}) {
1682
1943
  let controller = null;
1944
+ let stopOpen = null;
1683
1945
  const entries = /* @__PURE__ */ new Set();
1684
1946
  function attachEntry(entry) {
1685
1947
  if (controller && !entry.detach && wm.get(entry.id)) {
1686
1948
  entry.detach = controller.attachWindow(entry.id, entry.element, entry.options);
1687
1949
  }
1688
1950
  }
1689
- wm.on("open", ({ window: win }) => {
1690
- for (const entry of entries) {
1691
- if (entry.id === win.id) attachEntry(entry);
1692
- }
1693
- });
1951
+ function unbindDesktop() {
1952
+ stopOpen?.();
1953
+ stopOpen = null;
1954
+ for (const entry of entries) entry.detach = null;
1955
+ controller?.destroy();
1956
+ controller = null;
1957
+ }
1694
1958
  return {
1695
1959
  wm,
1696
1960
  controller: () => controller,
1697
1961
  bindDesktop(element) {
1698
1962
  if (controller) throw new Error("wmkit: desktop is already bound");
1699
1963
  controller = attachDesktop(wm, element, options);
1964
+ stopOpen = wm.on("open", ({ window: win }) => {
1965
+ for (const entry of entries) {
1966
+ if (entry.id === win.id) attachEntry(entry);
1967
+ }
1968
+ });
1700
1969
  for (const entry of entries) attachEntry(entry);
1701
- return () => {
1702
- for (const entry of entries) entry.detach = null;
1703
- controller?.destroy();
1704
- controller = null;
1705
- };
1970
+ return unbindDesktop;
1706
1971
  },
1707
1972
  bindWindow(id, element, windowOptions) {
1708
1973
  const entry = { id, element, options: windowOptions, detach: null };
@@ -1713,10 +1978,14 @@ function createDesktopBinder(wm, options = {}) {
1713
1978
  entry.detach = null;
1714
1979
  entries.delete(entry);
1715
1980
  };
1981
+ },
1982
+ destroy() {
1983
+ unbindDesktop();
1984
+ entries.clear();
1716
1985
  }
1717
1986
  };
1718
1987
  }
1719
1988
 
1720
- export { attachDesktop, boundsEqual, clamp, clampSize, clampToViewport, createAnnouncer, createDesktopBinder, createEmitter, createWindowManager, defaultMessages, detectSnapZone, flipToTarget, magnetize, prefersReducedMotion, zoneBounds };
1721
- //# sourceMappingURL=chunk-YCHJXSTC.js.map
1722
- //# sourceMappingURL=chunk-YCHJXSTC.js.map
1989
+ export { applyAspect, attachDesktop, boundsEqual, clamp, clampSize, clampToViewport, createAnnouncer, createDesktopBinder, createEmitter, createWindowManager, defaultMessages, detectSnapZone, flipFromTarget, flipToTarget, magnetize, prefersReducedMotion, zoneBounds };
1990
+ //# sourceMappingURL=chunk-OOEDFA7K.js.map
1991
+ //# sourceMappingURL=chunk-OOEDFA7K.js.map