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