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