@surdeddd/wmkit 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +80 -16
  2. package/README.ru.md +33 -9
  3. package/dist/angular.cjs +53 -0
  4. package/dist/angular.cjs.map +1 -0
  5. package/dist/angular.d.cts +15 -0
  6. package/dist/angular.d.ts +15 -0
  7. package/dist/angular.js +48 -0
  8. package/dist/angular.js.map +1 -0
  9. package/dist/{binder-BY-saDUB.d.cts → binder-B2A2IZGh.d.ts} +19 -2
  10. package/dist/{binder-D9UEE1SM.d.ts → binder-Dl07SXIM.d.cts} +19 -2
  11. package/dist/{chunk-KQCVZ2W7.cjs → chunk-3KZZOLHW.cjs} +691 -156
  12. package/dist/chunk-3KZZOLHW.cjs.map +1 -0
  13. package/dist/{chunk-2TVOEDRG.js → chunk-FPZDAKNP.js} +689 -157
  14. package/dist/chunk-FPZDAKNP.js.map +1 -0
  15. package/dist/index.cjs +27 -15
  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/light.css +202 -0
  50. package/dist/themes/retro.css +154 -0
  51. package/dist/{types-Bkrq2djr.d.cts → types-t6pyVYKN.d.cts} +38 -2
  52. package/dist/{types-Bkrq2djr.d.ts → types-t6pyVYKN.d.ts} +38 -2
  53. package/dist/vue.cjs +4 -3
  54. package/dist/vue.cjs.map +1 -1
  55. package/dist/vue.d.cts +2 -2
  56. package/dist/vue.d.ts +2 -2
  57. package/dist/vue.js +2 -1
  58. package/dist/vue.js.map +1 -1
  59. package/package.json +34 -7
  60. package/dist/chunk-2TVOEDRG.js.map +0 -1
  61. package/dist/chunk-KQCVZ2W7.cjs.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":
@@ -97,6 +126,42 @@ function detectSnapZone(x, y, viewport, options = {}) {
97
126
  }
98
127
  return null;
99
128
  }
129
+ function nearestEdge(low, high, targetLow, targetHigh) {
130
+ return [targetLow - low, targetHigh - low, targetLow - high, targetHigh - high];
131
+ }
132
+ function magnetize(bounds, targets, threshold) {
133
+ const result = { x: bounds.x, y: bounds.y, snappedX: false, snappedY: false };
134
+ if (threshold <= 0 || targets.length === 0) return result;
135
+ let bestX = threshold + 1;
136
+ let bestY = threshold + 1;
137
+ for (const target of targets) {
138
+ for (const delta of nearestEdge(
139
+ bounds.x,
140
+ bounds.x + bounds.width,
141
+ target.x,
142
+ target.x + target.width
143
+ )) {
144
+ if (Math.abs(delta) <= threshold && Math.abs(delta) < Math.abs(bestX)) bestX = delta;
145
+ }
146
+ for (const delta of nearestEdge(
147
+ bounds.y,
148
+ bounds.y + bounds.height,
149
+ target.y,
150
+ target.y + target.height
151
+ )) {
152
+ if (Math.abs(delta) <= threshold && Math.abs(delta) < Math.abs(bestY)) bestY = delta;
153
+ }
154
+ }
155
+ if (Math.abs(bestX) <= threshold) {
156
+ result.x = bounds.x + bestX;
157
+ result.snappedX = true;
158
+ }
159
+ if (Math.abs(bestY) <= threshold) {
160
+ result.y = bounds.y + bestY;
161
+ result.snappedY = true;
162
+ }
163
+ return result;
164
+ }
100
165
 
101
166
  // src/core/manager.ts
102
167
  var LAYER_RANK = { normal: 0, floating: 1, modal: 2 };
@@ -110,8 +175,14 @@ var ZONES = [
110
175
  "top-left",
111
176
  "top-right",
112
177
  "bottom-left",
113
- "bottom-right"
178
+ "bottom-right",
179
+ "left-third",
180
+ "center-third",
181
+ "right-third"
114
182
  ];
183
+ function normalizeWorkspace(value, fallback) {
184
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : fallback;
185
+ }
115
186
  function createWindowManager(options = {}) {
116
187
  const emitter = createEmitter();
117
188
  const keepInViewport = options.keepInViewport ?? true;
@@ -120,26 +191,41 @@ function createWindowManager(options = {}) {
120
191
  const cascadeOffset = options.cascadeOffset ?? 32;
121
192
  const cascadeOrigin = options.cascadeOrigin ?? { x: 32, y: 32 };
122
193
  const idPrefix = options.idPrefix ?? "wm";
194
+ const historyLimit = options.historyLimit ?? 50;
123
195
  let windows = {};
124
196
  let order = [];
125
197
  let focusedId = null;
126
198
  let viewport = options.viewport ?? { width: 0, height: 0 };
199
+ let workspace = normalizeWorkspace(options.workspace, 0);
127
200
  let seq = 0;
128
201
  let idCounter = 0;
129
202
  let cascadeIndex = 0;
130
203
  let snapshot = null;
131
204
  let batchDepth = 0;
132
205
  let batchDirty = false;
206
+ let historyDirty = false;
133
207
  const pendingEvents = [];
208
+ const past = [];
209
+ let future = [];
210
+ let interactionDepth = 0;
211
+ let interactionRecorded = false;
212
+ let skipNextHistory = false;
213
+ let pendingHistory = null;
214
+ const layouts = /* @__PURE__ */ new Map();
134
215
  function getState() {
135
216
  if (!snapshot) {
136
- snapshot = { windows, order, focusedId, viewport };
217
+ snapshot = { windows, order, focusedId, viewport, workspace };
137
218
  }
138
219
  return snapshot;
139
220
  }
140
221
  function commit() {
141
222
  snapshot = null;
142
223
  batchDirty = true;
224
+ historyDirty = true;
225
+ }
226
+ function commitQuiet() {
227
+ snapshot = null;
228
+ batchDirty = true;
143
229
  }
144
230
  function flushEvents() {
145
231
  while (pendingEvents.length > 0) {
@@ -177,10 +263,14 @@ function createWindowManager(options = {}) {
177
263
  order = next;
178
264
  return changed;
179
265
  }
266
+ function onActiveWorkspace(win) {
267
+ return win.workspace === workspace;
268
+ }
180
269
  function topModalId() {
181
270
  for (let i = order.length - 1; i >= 0; i -= 1) {
182
271
  const win = windows[order[i]];
183
- if (win.layer === "modal" && win.stage !== "minimized") return win.id;
272
+ if (win.layer === "modal" && win.stage !== "minimized" && onActiveWorkspace(win))
273
+ return win.id;
184
274
  }
185
275
  return null;
186
276
  }
@@ -188,7 +278,7 @@ function createWindowManager(options = {}) {
188
278
  const modal = topModalId();
189
279
  return order.filter((id) => {
190
280
  const win = windows[id];
191
- if (win.stage === "minimized") return false;
281
+ if (win.stage === "minimized" || !onActiveWorkspace(win)) return false;
192
282
  if (modal && win.layer !== "modal") return false;
193
283
  return true;
194
284
  });
@@ -200,8 +290,9 @@ function createWindowManager(options = {}) {
200
290
  function emitFocus(win, previous) {
201
291
  queueEvent(() => emitter.emit("focus", { window: win, previous }));
202
292
  }
203
- function normalizeSize(size, win) {
204
- 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);
205
296
  }
206
297
  function positionForOpen(init) {
207
298
  if (init.x !== void 0 && init.y !== void 0) return { x: init.x, y: init.y };
@@ -223,11 +314,12 @@ function createWindowManager(options = {}) {
223
314
  width: init.maxWidth ?? Number.POSITIVE_INFINITY,
224
315
  height: init.maxHeight ?? Number.POSITIVE_INFINITY
225
316
  } : null;
226
- const size = clampSize(
227
- { width: init.width ?? defaultSize.width, height: init.height ?? defaultSize.height },
228
- minSize,
229
- maxSize
230
- );
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");
231
323
  const position = positionForOpen(init);
232
324
  let bounds = { ...position, ...size };
233
325
  if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
@@ -242,8 +334,10 @@ function createWindowManager(options = {}) {
242
334
  stage: "normal",
243
335
  snapZone: null,
244
336
  layer: init.layer ?? "normal",
337
+ workspace: normalizeWorkspace(init.workspace, workspace),
245
338
  minSize,
246
339
  maxSize,
340
+ aspectRatio,
247
341
  openedSeq: ++seq,
248
342
  draggable: init.draggable ?? true,
249
343
  resizable: init.resizable ?? true,
@@ -261,7 +355,7 @@ function createWindowManager(options = {}) {
261
355
  setWindow(win);
262
356
  raise(id);
263
357
  let focusPayload = null;
264
- if (stage !== "minimized") {
358
+ if (stage !== "minimized" && onActiveWorkspace(win)) {
265
359
  const previous = focusedId;
266
360
  const modal = topModalId();
267
361
  if (!modal || win.layer === "modal") {
@@ -299,11 +393,12 @@ function createWindowManager(options = {}) {
299
393
  function focus(id) {
300
394
  const win = windows[id];
301
395
  if (!win) return false;
396
+ if (!onActiveWorkspace(win)) setWorkspace(win.workspace);
302
397
  const modal = topModalId();
303
398
  if (modal && modal !== id && win.layer !== "modal") {
304
399
  const modalWin = windows[modal];
305
400
  if (modalWin) queueEvent(() => emitter.emit("modalblocked", { window: modalWin }));
306
- commit();
401
+ commitQuiet();
307
402
  return false;
308
403
  }
309
404
  if (win.stage === "minimized") {
@@ -338,6 +433,14 @@ function createWindowManager(options = {}) {
338
433
  function fullBounds() {
339
434
  return { x: 0, y: 0, width: viewport.width, height: viewport.height };
340
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
+ }
341
444
  function applyStage(id, build) {
342
445
  const win = windows[id];
343
446
  if (!win) return false;
@@ -364,7 +467,7 @@ function createWindowManager(options = {}) {
364
467
  function maximize(id) {
365
468
  const result = applyStage(id, (win) => {
366
469
  if (win.stage === "maximized") return null;
367
- const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds;
470
+ const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds ?? win.bounds;
368
471
  return {
369
472
  ...win,
370
473
  stage: "maximized",
@@ -385,22 +488,23 @@ function createWindowManager(options = {}) {
385
488
  function restore(id) {
386
489
  const result = applyStage(id, (win) => {
387
490
  if (win.stage === "minimized") {
388
- const target = win.restoreStage ?? "normal";
389
- if (target === "maximized") {
491
+ const target2 = win.restoreStage ?? "normal";
492
+ if (target2 === "maximized") {
390
493
  return { ...win, stage: "maximized", restoreStage: null, bounds: fullBounds() };
391
494
  }
392
- if (target === "snapped" && win.snapZone) {
495
+ if (target2 === "snapped" && win.snapZone) {
393
496
  return {
394
497
  ...win,
395
498
  stage: "snapped",
396
499
  restoreStage: null,
397
- bounds: zoneBounds(win.snapZone, viewport)
500
+ bounds: snapBounds(win, win.snapZone)
398
501
  };
399
502
  }
400
503
  return { ...win, stage: "normal", restoreStage: null };
401
504
  }
402
505
  if (win.stage === "normal") return null;
403
- 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) };
404
508
  return {
405
509
  ...win,
406
510
  stage: "normal",
@@ -410,7 +514,7 @@ function createWindowManager(options = {}) {
410
514
  bounds: keepInViewport ? clampToViewport(bounds, viewport, minVisible) : bounds
411
515
  };
412
516
  });
413
- if (result) focus(id);
517
+ if (result) focusUnlessBlocked(id);
414
518
  return result;
415
519
  }
416
520
  function restoreTo(id, bounds) {
@@ -426,19 +530,19 @@ function createWindowManager(options = {}) {
426
530
  bounds: keepInViewport ? clampToViewport(next, viewport, minVisible) : next
427
531
  };
428
532
  });
429
- if (result) focus(id);
533
+ if (result) focusUnlessBlocked(id);
430
534
  return result;
431
535
  }
432
536
  function snap(id, zone) {
433
537
  const result = applyStage(id, (win) => {
434
- const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds;
538
+ const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds ?? win.bounds;
435
539
  return {
436
540
  ...win,
437
541
  stage: "snapped",
438
542
  snapZone: zone,
439
543
  restoreBounds,
440
544
  restoreStage: null,
441
- bounds: zoneBounds(zone, viewport)
545
+ bounds: snapBounds(win, zone)
442
546
  };
443
547
  });
444
548
  if (result) focus(id);
@@ -465,7 +569,8 @@ function createWindowManager(options = {}) {
465
569
  const win = windows[id];
466
570
  if (!win || win.stage === "maximized" || win.stage === "minimized") return false;
467
571
  const merged = { ...win.bounds, ...patch };
468
- 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);
469
574
  let bounds = { x: merged.x, y: merged.y, ...size };
470
575
  if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
471
576
  const becameNormal = win.stage === "snapped";
@@ -486,6 +591,7 @@ function createWindowManager(options = {}) {
486
591
  layer: patch.layer ?? win.layer,
487
592
  minSize: patch.minSize ?? win.minSize,
488
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,
489
595
  draggable: patch.draggable ?? win.draggable,
490
596
  resizable: patch.resizable ?? win.resizable,
491
597
  closable: patch.closable ?? win.closable,
@@ -515,47 +621,242 @@ function createWindowManager(options = {}) {
515
621
  function setViewport(next) {
516
622
  if (next.width === viewport.width && next.height === viewport.height) return;
517
623
  viewport = { ...next };
518
- transact(() => {
519
- for (const id of order) {
520
- const win = windows[id];
521
- if (win.stage === "maximized") {
522
- const updated = { ...win, bounds: fullBounds() };
523
- setWindow(updated);
524
- queueEvent(() => emitter.emit("resize", { window: updated }));
525
- } else if (win.stage === "snapped" && win.snapZone) {
526
- const updated = { ...win, bounds: zoneBounds(win.snapZone, viewport) };
527
- setWindow(updated);
528
- queueEvent(() => emitter.emit("resize", { window: updated }));
529
- } else if (win.stage === "normal" && keepInViewport) {
530
- const bounds = clampToViewport(win.bounds, viewport, minVisible);
531
- if (!boundsEqual(bounds, win.bounds)) {
532
- const updated = { ...win, bounds };
533
- setWindow(updated);
534
- queueEvent(() => emitter.emit("move", { window: updated }));
535
- }
536
- }
537
- }
538
- snapshot = null;
539
- batchDirty = true;
540
- });
624
+ reflowViewport();
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
+ );
541
685
  }
542
686
  function minimized() {
543
- 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);
688
+ }
689
+ function captureEntry() {
690
+ return { windows, order, focusedId, workspace };
691
+ }
692
+ function recordHistory() {
693
+ if (!historyDirty) return;
694
+ if (skipNextHistory) {
695
+ skipNextHistory = false;
696
+ return;
697
+ }
698
+ if (historyLimit <= 0) return;
699
+ if (interactionDepth > 0) {
700
+ if (interactionRecorded) return;
701
+ interactionRecorded = true;
702
+ }
703
+ past.push(pendingHistory);
704
+ if (past.length > historyLimit) past.shift();
705
+ future = [];
544
706
  }
545
707
  function transact(run) {
708
+ if (batchDepth === 0) pendingHistory = captureEntry();
546
709
  batchDepth += 1;
547
710
  try {
548
711
  return run();
549
712
  } finally {
550
713
  batchDepth -= 1;
551
- if (batchDepth === 0 && batchDirty) {
552
- batchDirty = false;
553
- snapshot = null;
554
- flushEvents();
555
- emitter.emit("change", { state: getState() });
714
+ if (batchDepth === 0) {
715
+ if (batchDirty) {
716
+ batchDirty = false;
717
+ recordHistory();
718
+ historyDirty = false;
719
+ snapshot = null;
720
+ flushEvents();
721
+ emitter.emit("change", { state: getState() });
722
+ }
723
+ pendingHistory = null;
556
724
  }
557
725
  }
558
726
  }
727
+ function reflowViewport() {
728
+ for (const id of order) {
729
+ const win = windows[id];
730
+ if (win.stage === "maximized") {
731
+ const updated = { ...win, bounds: fullBounds() };
732
+ setWindow(updated);
733
+ queueEvent(() => emitter.emit("resize", { window: updated }));
734
+ } else if (win.stage === "snapped" && win.snapZone) {
735
+ const updated = { ...win, bounds: snapBounds(win, win.snapZone) };
736
+ setWindow(updated);
737
+ queueEvent(() => emitter.emit("resize", { window: updated }));
738
+ } else if (win.stage === "normal" && keepInViewport) {
739
+ const bounds = clampToViewport(win.bounds, viewport, minVisible);
740
+ if (!boundsEqual(bounds, win.bounds)) {
741
+ const updated = { ...win, bounds };
742
+ setWindow(updated);
743
+ queueEvent(() => emitter.emit("move", { window: updated }));
744
+ }
745
+ }
746
+ }
747
+ }
748
+ function applyEntry(entry) {
749
+ windows = entry.windows;
750
+ order = [...entry.order];
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
+ }
757
+ reflowViewport();
758
+ commit();
759
+ }
760
+ function undo() {
761
+ const entry = past.pop();
762
+ if (!entry) return false;
763
+ future.push(captureEntry());
764
+ skipNextHistory = true;
765
+ applyEntry(entry);
766
+ return true;
767
+ }
768
+ function redo() {
769
+ const entry = future.pop();
770
+ if (!entry) return false;
771
+ past.push(captureEntry());
772
+ skipNextHistory = true;
773
+ applyEntry(entry);
774
+ return true;
775
+ }
776
+ function beginInteraction() {
777
+ interactionDepth += 1;
778
+ if (interactionDepth === 1) interactionRecorded = false;
779
+ }
780
+ function endInteraction() {
781
+ if (interactionDepth > 0) interactionDepth -= 1;
782
+ }
783
+ function abortInteraction() {
784
+ if (interactionDepth > 0 && interactionRecorded) {
785
+ past.pop();
786
+ interactionRecorded = false;
787
+ }
788
+ endInteraction();
789
+ }
790
+ function clearHistory() {
791
+ past.length = 0;
792
+ future = [];
793
+ }
794
+ function saveLayout(name) {
795
+ const data = serialize();
796
+ layouts.set(name, structuredClone(data));
797
+ return data;
798
+ }
799
+ function loadLayout(name) {
800
+ const data = layouts.get(name);
801
+ if (!data) return false;
802
+ return hydrate(structuredClone(data));
803
+ }
804
+ function getLayout(name) {
805
+ const data = layouts.get(name);
806
+ return data ? structuredClone(data) : void 0;
807
+ }
808
+ function setLayout(name, data) {
809
+ if (!isValidSerialized(data)) return false;
810
+ layouts.set(name, structuredClone(data));
811
+ return true;
812
+ }
813
+ function deleteLayout(name) {
814
+ return layouts.delete(name);
815
+ }
816
+ function layoutNames() {
817
+ return [...layouts.keys()];
818
+ }
819
+ function arrange(mode) {
820
+ const ids = order.filter((id) => {
821
+ const win = windows[id];
822
+ return win.stage !== "minimized" && onActiveWorkspace(win);
823
+ });
824
+ if (ids.length === 0) return;
825
+ if (mode === "cascade") {
826
+ ids.forEach((id, index) => {
827
+ const win = windows[id];
828
+ const size = win.restoreBounds ?? win.bounds;
829
+ restoreTo(id, {
830
+ x: cascadeOrigin.x + index % 10 * cascadeOffset,
831
+ y: cascadeOrigin.y + index % 10 * cascadeOffset,
832
+ width: size.width,
833
+ height: size.height
834
+ });
835
+ });
836
+ return;
837
+ }
838
+ if (viewport.width <= 0 || viewport.height <= 0) return;
839
+ const cols = Math.ceil(Math.sqrt(ids.length));
840
+ const rows = Math.ceil(ids.length / cols);
841
+ const cellWidth = Math.floor(viewport.width / cols);
842
+ const cellHeight = Math.floor(viewport.height / rows);
843
+ ids.forEach((id, index) => {
844
+ restoreTo(id, {
845
+ x: index % cols * cellWidth,
846
+ y: Math.floor(index / cols) * cellHeight,
847
+ width: cellWidth,
848
+ height: cellHeight
849
+ });
850
+ });
851
+ }
852
+ function minimizeAll() {
853
+ for (const id of [...order]) {
854
+ if (onActiveWorkspace(windows[id])) minimize(id);
855
+ }
856
+ }
857
+ function restoreAll() {
858
+ for (const win of minimized()) restore(win.id);
859
+ }
559
860
  function serialize() {
560
861
  return {
561
862
  version: 1,
@@ -571,7 +872,8 @@ function createWindowManager(options = {}) {
571
872
  meta: { ...win.meta }
572
873
  })),
573
874
  order: [...order],
574
- focusedId
875
+ focusedId,
876
+ workspace
575
877
  };
576
878
  }
577
879
  function isBounds(value) {
@@ -602,10 +904,16 @@ function createWindowManager(options = {}) {
602
904
  const win = value;
603
905
  return typeof win.id === "string" && win.id.length > 0 && isBounds(win.bounds) && STAGES.includes(win.stage) && LAYERS.includes(win.layer);
604
906
  }
605
- function hydrate(data) {
907
+ function isValidSerialized(data) {
606
908
  if (typeof data !== "object" || data === null) return false;
607
909
  if (data.version !== 1 || !Array.isArray(data.windows)) return false;
608
910
  if (!data.windows.every(isSerializedWindow)) return false;
911
+ const ids = new Set(data.windows.map((win) => win.id));
912
+ return ids.size === data.windows.length;
913
+ }
914
+ function hydrate(data) {
915
+ if (!isValidSerialized(data)) return false;
916
+ const previousWindows = windows;
609
917
  const nextWindows = {};
610
918
  let maxSeq = 0;
611
919
  for (const raw of data.windows) {
@@ -619,8 +927,10 @@ function createWindowManager(options = {}) {
619
927
  stage: raw.stage,
620
928
  snapZone: ZONES.includes(raw.snapZone) ? raw.snapZone : null,
621
929
  layer: raw.layer,
930
+ workspace: normalizeWorkspace(raw.workspace, 0),
622
931
  minSize: isSize(raw.minSize) ? { ...raw.minSize } : { width: 160, height: 100 },
623
932
  maxSize: readMaxSize(raw.maxSize),
933
+ aspectRatio: typeof raw.aspectRatio === "number" && Number.isFinite(raw.aspectRatio) && raw.aspectRatio > 0 ? raw.aspectRatio : null,
624
934
  openedSeq: typeof raw.openedSeq === "number" ? raw.openedSeq : ++maxSeq,
625
935
  draggable: raw.draggable !== false,
626
936
  resizable: raw.resizable !== false,
@@ -649,14 +959,38 @@ function createWindowManager(options = {}) {
649
959
  order = nextOrder;
650
960
  order = sortByLayer(order);
651
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
+ }
652
967
  focusedId = data.focusedId && windows[data.focusedId] ? data.focusedId : null;
653
- 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
+ }
654
972
  if (!focusedId) focusTop();
655
973
  const modal = topModalId();
656
974
  if (modal) {
657
975
  const focusedWin = focusedId ? windows[focusedId] : null;
658
976
  if (focusedWin?.layer !== "modal") focusedId = modal;
659
977
  }
978
+ for (const id of Object.keys(previousWindows)) {
979
+ if (!windows[id]) {
980
+ const closed = previousWindows[id];
981
+ queueEvent(() => emitter.emit("close", { window: closed }));
982
+ }
983
+ }
984
+ for (const id of order) {
985
+ if (!previousWindows[id]) {
986
+ const opened = windows[id];
987
+ queueEvent(() => emitter.emit("open", { window: opened }));
988
+ }
989
+ }
990
+ if (viewport.width > 0 && viewport.height > 0) reflowViewport();
991
+ queueEvent(() => emitter.emit("order", { order }));
992
+ clearHistory();
993
+ skipNextHistory = true;
660
994
  commit();
661
995
  return true;
662
996
  }
@@ -678,15 +1012,37 @@ function createWindowManager(options = {}) {
678
1012
  snap: (id, zone) => transact(() => snap(id, zone)),
679
1013
  move: (id, x, y) => transact(() => move(id, x, y)),
680
1014
  moveBy: (id, dx, dy) => transact(() => moveBy(id, dx, dy)),
1015
+ center: (id) => transact(() => center(id)),
1016
+ sendToBack: (id) => transact(() => sendToBack(id)),
681
1017
  resize: (id, patch) => transact(() => resize(id, patch)),
682
1018
  update: (id, patch) => transact(() => update(id, patch)),
683
1019
  get: (id) => windows[id],
684
1020
  getState,
685
1021
  minimized,
686
1022
  setViewport: (next) => transact(() => setViewport(next)),
1023
+ workspace: () => workspace,
1024
+ setWorkspace: (next) => transact(() => setWorkspace(next)),
1025
+ moveToWorkspace: (id, next) => transact(() => moveToWorkspace(id, next)),
687
1026
  batch: (run) => transact(run),
688
1027
  serialize,
689
1028
  hydrate: (data) => transact(() => hydrate(data)),
1029
+ undo: () => transact(undo),
1030
+ redo: () => transact(redo),
1031
+ canUndo: () => past.length > 0,
1032
+ canRedo: () => future.length > 0,
1033
+ beginInteraction,
1034
+ endInteraction,
1035
+ abortInteraction,
1036
+ clearHistory,
1037
+ saveLayout,
1038
+ loadLayout: (name) => transact(() => loadLayout(name)),
1039
+ getLayout,
1040
+ setLayout,
1041
+ deleteLayout,
1042
+ layoutNames,
1043
+ arrange: (mode) => transact(() => arrange(mode)),
1044
+ minimizeAll: () => transact(minimizeAll),
1045
+ restoreAll: () => transact(restoreAll),
690
1046
  subscribe: (listener) => emitter.on("change", ({ state }) => listener(state)),
691
1047
  on: (event, listener) => emitter.on(event, listener),
692
1048
  destroy
@@ -697,6 +1053,34 @@ function createWindowManager(options = {}) {
697
1053
  function prefersReducedMotion(win) {
698
1054
  return win.matchMedia("(prefers-reduced-motion: reduce)").matches;
699
1055
  }
1056
+ function flipFromTarget(source, target, options = {}) {
1057
+ const view = source.ownerDocument.defaultView;
1058
+ if (!view || prefersReducedMotion(view)) return;
1059
+ if (typeof source.animate !== "function") return;
1060
+ const from = target.getBoundingClientRect();
1061
+ const to = source.getBoundingClientRect();
1062
+ if (from.width === 0 || to.width === 0) return;
1063
+ const ghost = source.ownerDocument.createElement("div");
1064
+ const style = view.getComputedStyle(source);
1065
+ ghost.style.cssText = `position:fixed;left:${to.left}px;top:${to.top}px;width:${to.width}px;height:${to.height}px;margin:0;pointer-events:none;z-index:2147483647;border-radius:${style.borderRadius};background:${style.backgroundColor};box-shadow:${style.boxShadow};transform-origin:top left;will-change:transform,opacity`;
1066
+ source.ownerDocument.body.append(ghost);
1067
+ const dx = from.left + from.width / 2 - (to.left + to.width / 2);
1068
+ const dy = from.top + from.height / 2 - (to.top + to.height / 2);
1069
+ const scaleX = Math.max(from.width / to.width, 0.05);
1070
+ const scaleY = Math.max(from.height / to.height, 0.05);
1071
+ const animation = ghost.animate(
1072
+ [
1073
+ { transform: `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`, opacity: 0.2 },
1074
+ { transform: "translate(0, 0) scale(1, 1)", opacity: 0.9 }
1075
+ ],
1076
+ {
1077
+ duration: options.duration ?? 260,
1078
+ easing: options.easing ?? "cubic-bezier(0.32, 0.72, 0, 1)"
1079
+ }
1080
+ );
1081
+ animation.onfinish = () => ghost.remove();
1082
+ animation.oncancel = () => ghost.remove();
1083
+ }
700
1084
  function flipToTarget(source, target, options = {}) {
701
1085
  const view = source.ownerDocument.defaultView;
702
1086
  if (!view || prefersReducedMotion(view)) return;
@@ -734,7 +1118,8 @@ var defaultMessages = {
734
1118
  restored: (title) => `${title} restored`,
735
1119
  maximized: (title) => `${title} maximized`,
736
1120
  snapped: (title, zone) => `${title} snapped to ${zone.replace("-", " ")}`,
737
- focused: (title) => `${title} focused`
1121
+ focused: (title) => `${title} focused`,
1122
+ workspace: (index) => `workspace ${index + 1}`
738
1123
  };
739
1124
  function createAnnouncer(wm, container, messages = {}) {
740
1125
  const dict = { ...defaultMessages, ...messages };
@@ -745,7 +1130,10 @@ function createAnnouncer(wm, container, messages = {}) {
745
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";
746
1131
  container.append(element);
747
1132
  let clearTimer;
1133
+ let lastMessage = "";
1134
+ let detailed = false;
748
1135
  function announce(message) {
1136
+ lastMessage = message;
749
1137
  element.textContent = message;
750
1138
  if (clearTimer !== void 0) clearTimeout(clearTimer);
751
1139
  clearTimer = setTimeout(() => {
@@ -761,6 +1149,19 @@ function createAnnouncer(wm, container, messages = {}) {
761
1149
  else if (win.stage === "snapped" && win.snapZone)
762
1150
  announce(dict.snapped(win.title, win.snapZone));
763
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;
764
1165
  })
765
1166
  ];
766
1167
  return {
@@ -839,7 +1240,29 @@ function createDragStarter(ctx) {
839
1240
  }
840
1241
  return;
841
1242
  }
842
- wm.move(id, session.pendingX - session.grabDX, session.pendingY - session.grabDY);
1243
+ let nextX = session.pendingX - session.grabDX;
1244
+ let nextY = session.pendingY - session.grabDY;
1245
+ if (ctx.magnetThreshold > 0) {
1246
+ const state = wm.getState();
1247
+ const targets = [
1248
+ { x: 0, y: 0, width: state.viewport.width, height: state.viewport.height }
1249
+ ];
1250
+ for (const otherId of state.order) {
1251
+ if (otherId === id) continue;
1252
+ const other = state.windows[otherId];
1253
+ if (other && other.stage !== "minimized" && other.workspace === state.workspace) {
1254
+ targets.push(other.bounds);
1255
+ }
1256
+ }
1257
+ const magnet = magnetize(
1258
+ { x: nextX, y: nextY, width: current.bounds.width, height: current.bounds.height },
1259
+ targets,
1260
+ ctx.magnetThreshold
1261
+ );
1262
+ nextX = magnet.x;
1263
+ nextY = magnet.y;
1264
+ }
1265
+ wm.move(id, nextX, nextY);
843
1266
  if (ctx.snapEnabled && current.snappable) {
844
1267
  const viewport = wm.getState().viewport;
845
1268
  const rawZone = detectSnapZone(session.pendingX, session.pendingY, viewport, ctx.snapDetect);
@@ -863,7 +1286,7 @@ function createDragStarter(ctx) {
863
1286
  const movePoint = ctx.toLocal(moveEvent);
864
1287
  if (!session.moved) {
865
1288
  const travelled = Math.abs(movePoint.x - (session.startBounds.x + session.grabDX)) + Math.abs(movePoint.y - (session.startBounds.y + session.grabDY));
866
- if (travelled < 3 && session.restored) return;
1289
+ if (travelled < 3) return;
867
1290
  session.moved = true;
868
1291
  if (el) {
869
1292
  el.dataset.wmDragging = "";
@@ -891,42 +1314,46 @@ function createDragStarter(ctx) {
891
1314
  }
892
1315
  function finish(cancelled) {
893
1316
  if (ctx.currentDrag() !== session) return;
894
- if (session.raf !== 0) view.cancelAnimationFrame(session.raf);
895
- if (session.hasPending && !cancelled) flush();
896
- ctx.releaseDrag(session);
897
- releaseRect();
898
- handle.removeEventListener("pointermove", onMove);
899
- handle.removeEventListener("pointerup", onUp);
900
- handle.removeEventListener("pointercancel", onCancel);
901
- doc.removeEventListener("keydown", onKeydown, true);
902
- if (handle.hasPointerCapture(session.pointerId)) {
903
- handle.releasePointerCapture(session.pointerId);
904
- }
905
- if (el) {
906
- delete el.dataset.wmDragging;
907
- el.style.willChange = "";
908
- }
909
- ctx.hidePreview();
910
- if (cancelled) {
911
- if (session.moved && session.restored) {
912
- if (session.startStage === "maximized") {
913
- wm.restoreTo(id, session.startRestoreBounds ?? session.startBounds);
914
- wm.maximize(id);
915
- } else if (session.startStage === "snapped" && session.startZone) {
916
- wm.restoreTo(id, session.startRestoreBounds ?? session.startBounds);
917
- wm.snap(id, session.startZone);
918
- } else {
919
- 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
+ }
920
1331
  }
1332
+ } else if (session.moved && session.zone) {
1333
+ if (session.zone === "maximize") wm.maximize(id);
1334
+ else wm.snap(id, session.zone);
921
1335
  }
922
- return;
923
- }
924
- if (session.moved && session.zone) {
925
- if (session.zone === "maximize") wm.maximize(id);
926
- 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();
927
1353
  }
928
1354
  }
929
1355
  session.finish = finish;
1356
+ wm.beginInteraction();
930
1357
  ctx.claimDrag(session);
931
1358
  handle.setPointerCapture(event.pointerId);
932
1359
  handle.addEventListener("pointermove", onMove);
@@ -956,40 +1383,42 @@ function createResizeStarter(ctx) {
956
1383
  event.preventDefault();
957
1384
  event.stopPropagation();
958
1385
  const handleEl = event.currentTarget;
1386
+ const session = { id, finish: (cancelled) => finish(cancelled) };
1387
+ ctx.claimDrag(session);
1388
+ wm.beginInteraction();
959
1389
  const releaseRect = ctx.trackRect();
960
1390
  const startPoint = ctx.toLocal(event);
961
1391
  const start = win.bounds;
1392
+ const startStage = win.stage;
1393
+ const startZone = win.snapZone;
1394
+ const startRestore = win.restoreBounds;
962
1395
  const minSize = win.minSize;
963
1396
  const maxSize = win.maxSize;
1397
+ const aspect = win.aspectRatio;
1398
+ const drive = direction === "n" || direction === "s" ? "height" : "width";
964
1399
  let raf = 0;
965
1400
  let pendingX = 0;
966
1401
  let pendingY = 0;
967
1402
  let hasPending = false;
968
1403
  const el = ctx.windowElement(id);
969
1404
  if (el) el.dataset.wmResizing = direction;
970
- function clampWidth(width) {
971
- return clamp(width, minSize.width, maxSize ? maxSize.width : Number.POSITIVE_INFINITY);
972
- }
973
- function clampHeight(height) {
974
- return clamp(height, minSize.height, maxSize ? maxSize.height : Number.POSITIVE_INFINITY);
975
- }
976
1405
  function flush() {
977
1406
  if (!hasPending) return;
978
1407
  hasPending = false;
979
1408
  raf = 0;
980
1409
  const dx = pendingX - startPoint.x;
981
1410
  const dy = pendingY - startPoint.y;
982
- const next = { ...start };
983
- if (direction.includes("e")) next.width = clampWidth(start.width + dx);
984
- if (direction.includes("s")) next.height = clampHeight(start.height + dy);
985
- if (direction.includes("w")) {
986
- next.width = clampWidth(start.width - dx);
987
- next.x = start.x + (start.width - next.width);
988
- }
989
- if (direction.includes("n")) {
990
- next.height = clampHeight(start.height - dy);
991
- next.y = start.y + (start.height - next.height);
992
- }
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
+ };
993
1422
  wm.resize(id, next);
994
1423
  }
995
1424
  function onMove(moveEvent) {
@@ -1001,18 +1430,32 @@ function createResizeStarter(ctx) {
1001
1430
  if (raf === 0) raf = view.requestAnimationFrame(flush);
1002
1431
  }
1003
1432
  function finish(cancelled) {
1004
- if (raf !== 0) view.cancelAnimationFrame(raf);
1005
- if (hasPending && !cancelled) flush();
1006
- releaseRect();
1007
- handleEl.removeEventListener("pointermove", onMove);
1008
- handleEl.removeEventListener("pointerup", onUp);
1009
- handleEl.removeEventListener("pointercancel", onCancelPointer);
1010
- doc.removeEventListener("keydown", onKeydown, true);
1011
- if (handleEl.hasPointerCapture(event.pointerId)) {
1012
- 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();
1013
1458
  }
1014
- if (el) delete el.dataset.wmResizing;
1015
- if (cancelled) wm.resize(id, start);
1016
1459
  }
1017
1460
  function onUp(upEvent) {
1018
1461
  if (upEvent.pointerId !== event.pointerId) return;
@@ -1057,6 +1500,11 @@ function createResizeHandles(doc, edge, corner) {
1057
1500
  }
1058
1501
 
1059
1502
  // src/dom/controller.ts
1503
+ var SNAP_SHORTCUTS = {
1504
+ ArrowLeft: "left",
1505
+ ArrowRight: "right"
1506
+ };
1507
+ var FOCUSABLE_SELECTOR = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
1060
1508
  function attachDesktop(wm, element, options = {}) {
1061
1509
  const doc = element.ownerDocument;
1062
1510
  const view = windowOf(element);
@@ -1069,9 +1517,13 @@ function attachDesktop(wm, element, options = {}) {
1069
1517
  const keyboardOptions = typeof options.keyboard === "object" ? options.keyboard : {};
1070
1518
  const moveStep = keyboardOptions.moveStep ?? 16;
1071
1519
  const cycleEnabled = keyboardOptions.cycle !== false;
1520
+ const snapShortcuts = keyboardOptions.snapShortcuts !== false;
1521
+ const historyShortcuts = keyboardOptions.historyShortcuts !== false;
1072
1522
  const hitEdge = options.hitAreas?.edge ?? (coarsePointer ? 16 : 8);
1073
1523
  const hitCorner = options.hitAreas?.corner ?? (coarsePointer ? 24 : 12);
1524
+ const magnetThreshold = options.magnetism === false ? 0 : (typeof options.magnetism === "object" ? options.magnetism.threshold : void 0) ?? (coarsePointer ? 12 : 8);
1074
1525
  element.dataset.wmDesktop = "";
1526
+ element.tabIndex = -1;
1075
1527
  if (view.getComputedStyle(element).position === "static") {
1076
1528
  element.style.position = "relative";
1077
1529
  }
@@ -1108,6 +1560,10 @@ function attachDesktop(wm, element, options = {}) {
1108
1560
  function hidePreview() {
1109
1561
  if (preview) preview.style.display = "none";
1110
1562
  }
1563
+ function refreshRect() {
1564
+ const rect = element.getBoundingClientRect();
1565
+ cachedRect = { left: rect.left, top: rect.top };
1566
+ }
1111
1567
  const ctx = {
1112
1568
  wm,
1113
1569
  doc,
@@ -1118,8 +1574,9 @@ function attachDesktop(wm, element, options = {}) {
1118
1574
  },
1119
1575
  trackRect() {
1120
1576
  if (rectUsers === 0) {
1121
- const rect = element.getBoundingClientRect();
1122
- cachedRect = { left: rect.left, top: rect.top };
1577
+ refreshRect();
1578
+ view.addEventListener("scroll", refreshRect, true);
1579
+ view.addEventListener("resize", refreshRect);
1123
1580
  }
1124
1581
  rectUsers += 1;
1125
1582
  let released = false;
@@ -1127,7 +1584,11 @@ function attachDesktop(wm, element, options = {}) {
1127
1584
  if (released) return;
1128
1585
  released = true;
1129
1586
  rectUsers -= 1;
1130
- if (rectUsers === 0) cachedRect = null;
1587
+ if (rectUsers === 0) {
1588
+ view.removeEventListener("scroll", refreshRect, true);
1589
+ view.removeEventListener("resize", refreshRect);
1590
+ cachedRect = null;
1591
+ }
1131
1592
  };
1132
1593
  },
1133
1594
  windowElement: (id) => registry.get(id)?.element,
@@ -1141,6 +1602,7 @@ function attachDesktop(wm, element, options = {}) {
1141
1602
  topEdge,
1142
1603
  hitEdge,
1143
1604
  hitCorner,
1605
+ magnetThreshold,
1144
1606
  currentDrag: () => drag,
1145
1607
  claimDrag(session) {
1146
1608
  drag = session;
@@ -1154,28 +1616,34 @@ function attachDesktop(wm, element, options = {}) {
1154
1616
  if (options.autoViewport !== false) {
1155
1617
  const applyViewport = () => wm.setViewport({ width: element.clientWidth, height: element.clientHeight });
1156
1618
  applyViewport();
1157
- const observer = new ResizeObserver(applyViewport);
1619
+ const observer = new view.ResizeObserver(applyViewport);
1158
1620
  observer.observe(element);
1159
1621
  cleanup.push(() => observer.disconnect());
1160
1622
  }
1161
- function syncWindow(attached, win, zIndex) {
1623
+ function syncWindow(attached, win, zIndex, activeWorkspace) {
1162
1624
  const el = attached.element;
1163
1625
  const firstSync = attached.lastState === null;
1164
1626
  if (firstSync) el.style.transition = "none";
1165
- if (attached.lastState !== win) {
1627
+ if (attached.lastState !== win || attached.lastWorkspace !== activeWorkspace) {
1166
1628
  el.style.transform = `translate3d(${win.bounds.x}px, ${win.bounds.y}px, 0)`;
1167
1629
  el.style.width = `${win.bounds.width}px`;
1168
1630
  el.style.height = `${win.bounds.height}px`;
1169
1631
  el.dataset.wmStage = win.stage;
1170
1632
  el.dataset.wmLayer = win.layer;
1171
- el.hidden = win.stage === "minimized";
1633
+ el.dataset.wmWorkspace = String(win.workspace);
1634
+ const hide = win.stage === "minimized" || win.workspace !== activeWorkspace;
1635
+ if (hide && !el.hidden && el.contains(doc.activeElement)) {
1636
+ element.focus({ preventScroll: true });
1637
+ }
1638
+ el.hidden = hide;
1172
1639
  el.setAttribute("aria-label", win.title);
1173
1640
  if (win.layer === "modal") el.setAttribute("aria-modal", "true");
1174
1641
  else el.removeAttribute("aria-modal");
1175
1642
  for (const handle of attached.handles) {
1176
- handle.style.display = win.resizable && win.stage === "normal" ? "" : "none";
1643
+ handle.style.display = win.resizable && (win.stage === "normal" || win.stage === "snapped") ? "" : "none";
1177
1644
  }
1178
1645
  attached.lastState = win;
1646
+ attached.lastWorkspace = activeWorkspace;
1179
1647
  }
1180
1648
  if (attached.lastZ !== zIndex) {
1181
1649
  el.style.zIndex = String(zIndex + 1);
@@ -1193,7 +1661,9 @@ function attachDesktop(wm, element, options = {}) {
1193
1661
  const attached = registry.get(id);
1194
1662
  const win = state.windows[id];
1195
1663
  if (!attached || !win) return;
1196
- if (orderChanged || attached.lastState !== win) syncWindow(attached, win, index);
1664
+ if (orderChanged || attached.lastState !== win || attached.lastWorkspace !== state.workspace) {
1665
+ syncWindow(attached, win, index, state.workspace);
1666
+ }
1197
1667
  });
1198
1668
  if (state.focusedId !== lastFocused) {
1199
1669
  if (lastFocused) {
@@ -1229,17 +1699,51 @@ function attachDesktop(wm, element, options = {}) {
1229
1699
  );
1230
1700
  cleanup.push(
1231
1701
  wm.on("stage", ({ window: win, previous }) => {
1232
- if (win.stage !== "minimized" || previous === "minimized") return;
1233
1702
  const attached = registry.get(win.id);
1234
- const target = options.minimizeTarget?.(win);
1235
- if (attached && target) flipToTarget(attached.element, target);
1703
+ if (!attached) return;
1704
+ if (win.stage === "minimized" && previous !== "minimized") {
1705
+ const target = options.minimizeTarget?.(win);
1706
+ if (target) flipToTarget(attached.element, target);
1707
+ } else if (previous === "minimized" && win.stage !== "minimized") {
1708
+ const target = options.minimizeTarget?.(win);
1709
+ if (target) view.requestAnimationFrame(() => flipFromTarget(attached.element, target));
1710
+ }
1236
1711
  })
1237
1712
  );
1238
- if (keyboardEnabled && cycleEnabled) {
1713
+ if (keyboardEnabled) {
1239
1714
  const onDesktopKeydown = (event) => {
1240
- if (event.key === "F6") {
1715
+ if (cycleEnabled && event.key === "F6") {
1241
1716
  event.preventDefault();
1242
1717
  wm.cycleFocus(event.shiftKey ? -1 : 1);
1718
+ return;
1719
+ }
1720
+ if (!event.ctrlKey && !event.metaKey) return;
1721
+ const target = event.target;
1722
+ if (target?.closest(INTERACTIVE_SELECTOR)) return;
1723
+ if (historyShortcuts && (event.key === "z" || event.key === "Z")) {
1724
+ event.preventDefault();
1725
+ if (event.shiftKey) wm.redo();
1726
+ else wm.undo();
1727
+ return;
1728
+ }
1729
+ if (!snapShortcuts || !event.altKey) return;
1730
+ const focused = wm.getState().focusedId;
1731
+ const win = focused ? wm.get(focused) : void 0;
1732
+ if (!win) return;
1733
+ const zone = SNAP_SHORTCUTS[event.key];
1734
+ if (zone) {
1735
+ event.preventDefault();
1736
+ if (win.snappable) wm.snap(win.id, zone);
1737
+ } else if (event.key === "ArrowUp") {
1738
+ event.preventDefault();
1739
+ if (win.maximizable) wm.maximize(win.id);
1740
+ } else if (event.key === "ArrowDown") {
1741
+ event.preventDefault();
1742
+ if (win.stage === "normal") {
1743
+ if (win.minimizable) wm.minimize(win.id);
1744
+ } else {
1745
+ wm.restore(win.id);
1746
+ }
1243
1747
  }
1244
1748
  };
1245
1749
  element.addEventListener("keydown", onDesktopKeydown);
@@ -1258,6 +1762,7 @@ function attachDesktop(wm, element, options = {}) {
1258
1762
  handles: [],
1259
1763
  lastState: null,
1260
1764
  lastZ: -1,
1765
+ lastWorkspace: -1,
1261
1766
  cleanup: []
1262
1767
  };
1263
1768
  windowElement.dataset.wmWindow = id;
@@ -1308,6 +1813,16 @@ function attachDesktop(wm, element, options = {}) {
1308
1813
  };
1309
1814
  handle.addEventListener("dblclick", onDoubleClick);
1310
1815
  attached.cleanup.push(() => handle.removeEventListener("dblclick", onDoubleClick));
1816
+ if (options.onTitlebarContextMenu) {
1817
+ const onContextMenu = (event) => {
1818
+ const current = wm.get(id);
1819
+ if (!current) return;
1820
+ event.preventDefault();
1821
+ options.onTitlebarContextMenu?.(current, event);
1822
+ };
1823
+ handle.addEventListener("contextmenu", onContextMenu);
1824
+ attached.cleanup.push(() => handle.removeEventListener("contextmenu", onContextMenu));
1825
+ }
1311
1826
  }
1312
1827
  if (windowOptions.resizeHandles !== false) {
1313
1828
  for (const { element: resizeHandle, direction } of createResizeHandles(
@@ -1333,19 +1848,20 @@ function attachDesktop(wm, element, options = {}) {
1333
1848
  ArrowUp: [0, -1],
1334
1849
  ArrowDown: [0, 1]
1335
1850
  };
1851
+ if (event.ctrlKey || event.metaKey) return;
1336
1852
  const vector = arrows[event.key];
1337
1853
  if (!vector || !keyboardEnabled) return;
1338
- event.preventDefault();
1339
1854
  const step = event.altKey ? 1 : moveStep;
1340
1855
  const [dx, dy] = vector;
1341
1856
  if (event.shiftKey) {
1342
- if (current.resizable) {
1343
- wm.resize(id, {
1344
- width: current.bounds.width + dx * step,
1345
- height: current.bounds.height + dy * step
1346
- });
1347
- }
1857
+ if (!current.resizable) return;
1858
+ event.preventDefault();
1859
+ wm.resize(id, {
1860
+ width: current.bounds.width + dx * step,
1861
+ height: current.bounds.height + dy * step
1862
+ });
1348
1863
  } else if (current.draggable && current.stage === "normal") {
1864
+ event.preventDefault();
1349
1865
  wm.moveBy(id, dx * step, dy * step);
1350
1866
  }
1351
1867
  };
@@ -1355,13 +1871,18 @@ function attachDesktop(wm, element, options = {}) {
1355
1871
  if (event.key !== "Tab") return;
1356
1872
  const current = wm.get(id);
1357
1873
  if (current?.layer !== "modal") return;
1358
- const focusables = windowElement.querySelectorAll(
1359
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
1360
- );
1874
+ const focusables = [
1875
+ ...windowElement.querySelectorAll(FOCUSABLE_SELECTOR)
1876
+ ].filter((node) => node.offsetParent !== null || node === doc.activeElement);
1361
1877
  if (focusables.length === 0) return;
1362
1878
  const first = focusables[0];
1363
1879
  const last = focusables[focusables.length - 1];
1364
1880
  if (!first || !last) return;
1881
+ if (!windowElement.contains(doc.activeElement)) {
1882
+ event.preventDefault();
1883
+ (event.shiftKey ? last : first).focus();
1884
+ return;
1885
+ }
1365
1886
  if (event.shiftKey && doc.activeElement === first) {
1366
1887
  event.preventDefault();
1367
1888
  last.focus();
@@ -1390,6 +1911,9 @@ function attachDesktop(wm, element, options = {}) {
1390
1911
  lastOrder = null;
1391
1912
  lastFocused = null;
1392
1913
  syncAll();
1914
+ if (wm.getState().focusedId === id && !windowElement.contains(doc.activeElement)) {
1915
+ windowElement.focus({ preventScroll: true });
1916
+ }
1393
1917
  return detach;
1394
1918
  }
1395
1919
  return {
@@ -1413,29 +1937,33 @@ function attachDesktop(wm, element, options = {}) {
1413
1937
  // src/dom/binder.ts
1414
1938
  function createDesktopBinder(wm, options = {}) {
1415
1939
  let controller = null;
1940
+ let stopOpen = null;
1416
1941
  const entries = /* @__PURE__ */ new Set();
1417
1942
  function attachEntry(entry) {
1418
1943
  if (controller && !entry.detach && wm.get(entry.id)) {
1419
1944
  entry.detach = controller.attachWindow(entry.id, entry.element, entry.options);
1420
1945
  }
1421
1946
  }
1422
- wm.on("open", ({ window: win }) => {
1423
- for (const entry of entries) {
1424
- if (entry.id === win.id) attachEntry(entry);
1425
- }
1426
- });
1947
+ function unbindDesktop() {
1948
+ stopOpen?.();
1949
+ stopOpen = null;
1950
+ for (const entry of entries) entry.detach = null;
1951
+ controller?.destroy();
1952
+ controller = null;
1953
+ }
1427
1954
  return {
1428
1955
  wm,
1429
1956
  controller: () => controller,
1430
1957
  bindDesktop(element) {
1431
1958
  if (controller) throw new Error("wmkit: desktop is already bound");
1432
1959
  controller = attachDesktop(wm, element, options);
1960
+ stopOpen = wm.on("open", ({ window: win }) => {
1961
+ for (const entry of entries) {
1962
+ if (entry.id === win.id) attachEntry(entry);
1963
+ }
1964
+ });
1433
1965
  for (const entry of entries) attachEntry(entry);
1434
- return () => {
1435
- for (const entry of entries) entry.detach = null;
1436
- controller?.destroy();
1437
- controller = null;
1438
- };
1966
+ return unbindDesktop;
1439
1967
  },
1440
1968
  bindWindow(id, element, windowOptions) {
1441
1969
  const entry = { id, element, options: windowOptions, detach: null };
@@ -1446,10 +1974,14 @@ function createDesktopBinder(wm, options = {}) {
1446
1974
  entry.detach = null;
1447
1975
  entries.delete(entry);
1448
1976
  };
1977
+ },
1978
+ destroy() {
1979
+ unbindDesktop();
1980
+ entries.clear();
1449
1981
  }
1450
1982
  };
1451
1983
  }
1452
1984
 
1453
- export { attachDesktop, boundsEqual, clamp, clampSize, clampToViewport, createAnnouncer, createDesktopBinder, createEmitter, createWindowManager, defaultMessages, detectSnapZone, flipToTarget, prefersReducedMotion, zoneBounds };
1454
- //# sourceMappingURL=chunk-2TVOEDRG.js.map
1455
- //# sourceMappingURL=chunk-2TVOEDRG.js.map
1985
+ export { applyAspect, attachDesktop, boundsEqual, clamp, clampSize, clampToViewport, createAnnouncer, createDesktopBinder, createEmitter, createWindowManager, defaultMessages, detectSnapZone, flipFromTarget, flipToTarget, magnetize, prefersReducedMotion, zoneBounds };
1986
+ //# sourceMappingURL=chunk-FPZDAKNP.js.map
1987
+ //# sourceMappingURL=chunk-FPZDAKNP.js.map