@surdeddd/wmkit 0.1.1 → 0.3.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 (62) hide show
  1. package/README.md +58 -12
  2. package/README.ru.md +21 -12
  3. package/dist/angular.cjs +52 -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 +47 -0
  8. package/dist/angular.js.map +1 -0
  9. package/dist/{controller-1uedRvQw.d.ts → binder-0Oku22Nc.d.ts} +21 -3
  10. package/dist/{controller-BfUF1k04.d.cts → binder-Dq_AZRbo.d.cts} +21 -3
  11. package/dist/{chunk-TXINVEK3.cjs → chunk-B35DANTX.cjs} +596 -241
  12. package/dist/chunk-B35DANTX.cjs.map +1 -0
  13. package/dist/{chunk-N5KNWRKU.js → chunk-YCHJXSTC.js} +596 -242
  14. package/dist/chunk-YCHJXSTC.js.map +1 -0
  15. package/dist/index.cjs +19 -15
  16. package/dist/index.d.cts +7 -4
  17. package/dist/index.d.ts +7 -4
  18. package/dist/index.js +1 -1
  19. package/dist/persist.cjs +1 -1
  20. package/dist/persist.cjs.map +1 -1
  21. package/dist/persist.d.cts +1 -1
  22. package/dist/persist.d.ts +1 -1
  23. package/dist/persist.js +1 -1
  24. package/dist/persist.js.map +1 -1
  25. package/dist/popout.cjs +1 -1
  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 +1 -1
  30. package/dist/popout.js.map +1 -1
  31. package/dist/react.cjs +3 -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 +1 -1
  36. package/dist/react.js.map +1 -1
  37. package/dist/solid.cjs +3 -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 +1 -1
  42. package/dist/solid.js.map +1 -1
  43. package/dist/svelte.cjs +4 -4
  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 +2 -2
  48. package/dist/svelte.js.map +1 -1
  49. package/dist/themes/glass.css +0 -1
  50. package/dist/themes/light.css +202 -0
  51. package/dist/themes/retro.css +152 -0
  52. package/dist/{types-BYx7gCuT.d.cts → types-DdCtDHgt.d.cts} +23 -1
  53. package/dist/{types-BYx7gCuT.d.ts → types-DdCtDHgt.d.ts} +23 -1
  54. package/dist/vue.cjs +3 -3
  55. package/dist/vue.cjs.map +1 -1
  56. package/dist/vue.d.cts +2 -2
  57. package/dist/vue.d.ts +2 -2
  58. package/dist/vue.js +1 -1
  59. package/dist/vue.js.map +1 -1
  60. package/package.json +34 -7
  61. package/dist/chunk-N5KNWRKU.js.map +0 -1
  62. package/dist/chunk-TXINVEK3.cjs.map +0 -1
@@ -99,6 +99,42 @@ function detectSnapZone(x, y, viewport, options = {}) {
99
99
  }
100
100
  return null;
101
101
  }
102
+ function nearestEdge(low, high, targetLow, targetHigh) {
103
+ return [targetLow - low, targetHigh - low, targetLow - high, targetHigh - high];
104
+ }
105
+ function magnetize(bounds, targets, threshold) {
106
+ const result = { x: bounds.x, y: bounds.y, snappedX: false, snappedY: false };
107
+ if (threshold <= 0 || targets.length === 0) return result;
108
+ let bestX = threshold + 1;
109
+ let bestY = threshold + 1;
110
+ for (const target of targets) {
111
+ for (const delta of nearestEdge(
112
+ bounds.x,
113
+ bounds.x + bounds.width,
114
+ target.x,
115
+ target.x + target.width
116
+ )) {
117
+ if (Math.abs(delta) <= threshold && Math.abs(delta) < Math.abs(bestX)) bestX = delta;
118
+ }
119
+ for (const delta of nearestEdge(
120
+ bounds.y,
121
+ bounds.y + bounds.height,
122
+ target.y,
123
+ target.y + target.height
124
+ )) {
125
+ if (Math.abs(delta) <= threshold && Math.abs(delta) < Math.abs(bestY)) bestY = delta;
126
+ }
127
+ }
128
+ if (Math.abs(bestX) <= threshold) {
129
+ result.x = bounds.x + bestX;
130
+ result.snappedX = true;
131
+ }
132
+ if (Math.abs(bestY) <= threshold) {
133
+ result.y = bounds.y + bestY;
134
+ result.snappedY = true;
135
+ }
136
+ return result;
137
+ }
102
138
 
103
139
  // src/core/manager.ts
104
140
  var LAYER_RANK = { normal: 0, floating: 1, modal: 2 };
@@ -122,6 +158,7 @@ function createWindowManager(options = {}) {
122
158
  const cascadeOffset = options.cascadeOffset ?? 32;
123
159
  const cascadeOrigin = options.cascadeOrigin ?? { x: 32, y: 32 };
124
160
  const idPrefix = options.idPrefix ?? "wm";
161
+ const historyLimit = options.historyLimit ?? 50;
125
162
  let windows = {};
126
163
  let order = [];
127
164
  let focusedId = null;
@@ -133,6 +170,13 @@ function createWindowManager(options = {}) {
133
170
  let batchDepth = 0;
134
171
  let batchDirty = false;
135
172
  const pendingEvents = [];
173
+ const past = [];
174
+ let future = [];
175
+ let interactionDepth = 0;
176
+ let interactionRecorded = false;
177
+ let skipNextHistory = false;
178
+ let pendingHistory = null;
179
+ const layouts = /* @__PURE__ */ new Map();
136
180
  function getState() {
137
181
  if (!snapshot) {
138
182
  snapshot = { windows, order, focusedId, viewport };
@@ -517,47 +561,164 @@ function createWindowManager(options = {}) {
517
561
  function setViewport(next) {
518
562
  if (next.width === viewport.width && next.height === viewport.height) return;
519
563
  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
- });
564
+ reflowViewport();
565
+ snapshot = null;
566
+ batchDirty = true;
543
567
  }
544
568
  function minimized() {
545
569
  return Object.values(windows).filter((win) => win.stage === "minimized").sort((a, b) => a.openedSeq - b.openedSeq);
546
570
  }
571
+ function captureEntry() {
572
+ return { windows, order, focusedId };
573
+ }
574
+ function recordHistory() {
575
+ if (skipNextHistory) {
576
+ skipNextHistory = false;
577
+ return;
578
+ }
579
+ if (historyLimit <= 0) return;
580
+ if (interactionDepth > 0) {
581
+ if (interactionRecorded) return;
582
+ interactionRecorded = true;
583
+ }
584
+ past.push(pendingHistory);
585
+ if (past.length > historyLimit) past.shift();
586
+ future = [];
587
+ }
547
588
  function transact(run) {
589
+ if (batchDepth === 0) pendingHistory = captureEntry();
548
590
  batchDepth += 1;
549
591
  try {
550
592
  return run();
551
593
  } finally {
552
594
  batchDepth -= 1;
553
- if (batchDepth === 0 && batchDirty) {
554
- batchDirty = false;
555
- snapshot = null;
556
- flushEvents();
557
- emitter.emit("change", { state: getState() });
595
+ if (batchDepth === 0) {
596
+ if (batchDirty) {
597
+ batchDirty = false;
598
+ recordHistory();
599
+ snapshot = null;
600
+ flushEvents();
601
+ emitter.emit("change", { state: getState() });
602
+ }
603
+ pendingHistory = null;
604
+ }
605
+ }
606
+ }
607
+ function reflowViewport() {
608
+ for (const id of order) {
609
+ const win = windows[id];
610
+ if (win.stage === "maximized") {
611
+ const updated = { ...win, bounds: fullBounds() };
612
+ setWindow(updated);
613
+ queueEvent(() => emitter.emit("resize", { window: updated }));
614
+ } else if (win.stage === "snapped" && win.snapZone) {
615
+ const updated = { ...win, bounds: zoneBounds(win.snapZone, viewport) };
616
+ setWindow(updated);
617
+ queueEvent(() => emitter.emit("resize", { window: updated }));
618
+ } else if (win.stage === "normal" && keepInViewport) {
619
+ const bounds = clampToViewport(win.bounds, viewport, minVisible);
620
+ if (!boundsEqual(bounds, win.bounds)) {
621
+ const updated = { ...win, bounds };
622
+ setWindow(updated);
623
+ queueEvent(() => emitter.emit("move", { window: updated }));
624
+ }
558
625
  }
559
626
  }
560
627
  }
628
+ function applyEntry(entry) {
629
+ windows = entry.windows;
630
+ order = [...entry.order];
631
+ focusedId = entry.focusedId;
632
+ reflowViewport();
633
+ commit();
634
+ }
635
+ function undo() {
636
+ const entry = past.pop();
637
+ if (!entry) return false;
638
+ future.push(captureEntry());
639
+ skipNextHistory = true;
640
+ applyEntry(entry);
641
+ return true;
642
+ }
643
+ function redo() {
644
+ const entry = future.pop();
645
+ if (!entry) return false;
646
+ past.push(captureEntry());
647
+ skipNextHistory = true;
648
+ applyEntry(entry);
649
+ return true;
650
+ }
651
+ function beginInteraction() {
652
+ interactionDepth += 1;
653
+ if (interactionDepth === 1) interactionRecorded = false;
654
+ }
655
+ function endInteraction() {
656
+ if (interactionDepth > 0) interactionDepth -= 1;
657
+ }
658
+ function clearHistory() {
659
+ past.length = 0;
660
+ future = [];
661
+ }
662
+ function saveLayout(name) {
663
+ const data = serialize();
664
+ layouts.set(name, structuredClone(data));
665
+ return data;
666
+ }
667
+ function loadLayout(name) {
668
+ const data = layouts.get(name);
669
+ if (!data) return false;
670
+ return hydrate(structuredClone(data));
671
+ }
672
+ function getLayout(name) {
673
+ const data = layouts.get(name);
674
+ return data ? structuredClone(data) : void 0;
675
+ }
676
+ function setLayout(name, data) {
677
+ if (!isValidSerialized(data)) return false;
678
+ layouts.set(name, structuredClone(data));
679
+ return true;
680
+ }
681
+ function deleteLayout(name) {
682
+ return layouts.delete(name);
683
+ }
684
+ function layoutNames() {
685
+ return [...layouts.keys()];
686
+ }
687
+ function arrange(mode) {
688
+ const ids = order.filter((id) => windows[id].stage !== "minimized");
689
+ if (ids.length === 0) return;
690
+ if (mode === "cascade") {
691
+ ids.forEach((id, index) => {
692
+ const win = windows[id];
693
+ const size = win.restoreBounds ?? win.bounds;
694
+ restoreTo(id, {
695
+ x: cascadeOrigin.x + index % 10 * cascadeOffset,
696
+ y: cascadeOrigin.y + index % 10 * cascadeOffset,
697
+ width: size.width,
698
+ height: size.height
699
+ });
700
+ });
701
+ return;
702
+ }
703
+ const cols = Math.ceil(Math.sqrt(ids.length));
704
+ const rows = Math.ceil(ids.length / cols);
705
+ const cellWidth = Math.floor(viewport.width / cols);
706
+ const cellHeight = Math.floor(viewport.height / rows);
707
+ ids.forEach((id, index) => {
708
+ restoreTo(id, {
709
+ x: index % cols * cellWidth,
710
+ y: Math.floor(index / cols) * cellHeight,
711
+ width: cellWidth,
712
+ height: cellHeight
713
+ });
714
+ });
715
+ }
716
+ function minimizeAll() {
717
+ for (const id of [...order]) minimize(id);
718
+ }
719
+ function restoreAll() {
720
+ for (const win of minimized()) restore(win.id);
721
+ }
561
722
  function serialize() {
562
723
  return {
563
724
  version: 1,
@@ -604,10 +765,16 @@ function createWindowManager(options = {}) {
604
765
  const win = value;
605
766
  return typeof win.id === "string" && win.id.length > 0 && isBounds(win.bounds) && STAGES.includes(win.stage) && LAYERS.includes(win.layer);
606
767
  }
607
- function hydrate(data) {
768
+ function isValidSerialized(data) {
608
769
  if (typeof data !== "object" || data === null) return false;
609
770
  if (data.version !== 1 || !Array.isArray(data.windows)) return false;
610
771
  if (!data.windows.every(isSerializedWindow)) return false;
772
+ const ids = new Set(data.windows.map((win) => win.id));
773
+ return ids.size === data.windows.length;
774
+ }
775
+ function hydrate(data) {
776
+ if (!isValidSerialized(data)) return false;
777
+ const previousWindows = windows;
611
778
  const nextWindows = {};
612
779
  let maxSeq = 0;
613
780
  for (const raw of data.windows) {
@@ -659,6 +826,21 @@ function createWindowManager(options = {}) {
659
826
  const focusedWin = focusedId ? windows[focusedId] : null;
660
827
  if (focusedWin?.layer !== "modal") focusedId = modal;
661
828
  }
829
+ for (const id of Object.keys(previousWindows)) {
830
+ if (!windows[id]) {
831
+ const closed = previousWindows[id];
832
+ queueEvent(() => emitter.emit("close", { window: closed }));
833
+ }
834
+ }
835
+ for (const id of order) {
836
+ if (!previousWindows[id]) {
837
+ const opened = windows[id];
838
+ queueEvent(() => emitter.emit("open", { window: opened }));
839
+ }
840
+ }
841
+ queueEvent(() => emitter.emit("order", { order }));
842
+ clearHistory();
843
+ skipNextHistory = true;
662
844
  commit();
663
845
  return true;
664
846
  }
@@ -689,6 +871,21 @@ function createWindowManager(options = {}) {
689
871
  batch: (run) => transact(run),
690
872
  serialize,
691
873
  hydrate: (data) => transact(() => hydrate(data)),
874
+ undo: () => transact(undo),
875
+ redo: () => transact(redo),
876
+ canUndo: () => past.length > 0,
877
+ canRedo: () => future.length > 0,
878
+ beginInteraction,
879
+ endInteraction,
880
+ saveLayout,
881
+ loadLayout: (name) => transact(() => loadLayout(name)),
882
+ getLayout,
883
+ setLayout,
884
+ deleteLayout,
885
+ layoutNames,
886
+ arrange: (mode) => transact(() => arrange(mode)),
887
+ minimizeAll: () => transact(minimizeAll),
888
+ restoreAll: () => transact(restoreAll),
692
889
  subscribe: (listener) => emitter.on("change", ({ state }) => listener(state)),
693
890
  on: (event, listener) => emitter.on(event, listener),
694
891
  destroy
@@ -699,6 +896,34 @@ function createWindowManager(options = {}) {
699
896
  function prefersReducedMotion(win) {
700
897
  return win.matchMedia("(prefers-reduced-motion: reduce)").matches;
701
898
  }
899
+ function flipFromTarget(source, target, options = {}) {
900
+ const view = source.ownerDocument.defaultView;
901
+ if (!view || prefersReducedMotion(view)) return;
902
+ if (typeof source.animate !== "function") return;
903
+ const from = target.getBoundingClientRect();
904
+ const to = source.getBoundingClientRect();
905
+ if (from.width === 0 || to.width === 0) return;
906
+ const ghost = source.ownerDocument.createElement("div");
907
+ const style = view.getComputedStyle(source);
908
+ 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`;
909
+ source.ownerDocument.body.append(ghost);
910
+ const dx = from.left + from.width / 2 - (to.left + to.width / 2);
911
+ const dy = from.top + from.height / 2 - (to.top + to.height / 2);
912
+ const scaleX = Math.max(from.width / to.width, 0.05);
913
+ const scaleY = Math.max(from.height / to.height, 0.05);
914
+ const animation = ghost.animate(
915
+ [
916
+ { transform: `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`, opacity: 0.2 },
917
+ { transform: "translate(0, 0) scale(1, 1)", opacity: 0.9 }
918
+ ],
919
+ {
920
+ duration: options.duration ?? 260,
921
+ easing: options.easing ?? "cubic-bezier(0.32, 0.72, 0, 1)"
922
+ }
923
+ );
924
+ animation.onfinish = () => ghost.remove();
925
+ animation.oncancel = () => ghost.remove();
926
+ }
702
927
  function flipToTarget(source, target, options = {}) {
703
928
  const view = source.ownerDocument.defaultView;
704
929
  if (!view || prefersReducedMotion(view)) return;
@@ -776,175 +1001,25 @@ function createAnnouncer(wm, container, messages = {}) {
776
1001
  };
777
1002
  }
778
1003
 
779
- // src/dom/controller.ts
780
- var RESIZE_DIRECTIONS = ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
781
- var RESIZE_CURSORS = {
782
- n: "ns-resize",
783
- s: "ns-resize",
784
- e: "ew-resize",
785
- w: "ew-resize",
786
- ne: "nesw-resize",
787
- sw: "nesw-resize",
788
- nw: "nwse-resize",
789
- se: "nwse-resize"
790
- };
1004
+ // src/dom/shared.ts
791
1005
  var INTERACTIVE_SELECTOR = "button, input, select, textarea, a[href], [contenteditable], [data-wm-close], [data-wm-minimize], [data-wm-maximize]";
792
1006
  function windowOf(element) {
793
1007
  const view = element.ownerDocument.defaultView;
794
1008
  if (!view) throw new Error("wmkit: desktop element is not attached to a document");
795
1009
  return view;
796
1010
  }
797
- function attachDesktop(wm, element, options = {}) {
798
- const doc = element.ownerDocument;
799
- const view = windowOf(element);
800
- const snapEnabled = options.snap !== false;
801
- const snapOptions = typeof options.snap === "object" ? options.snap : {};
802
- const snapPreviewEnabled = snapOptions.preview !== false;
803
- const topEdge = snapOptions.topEdge ?? "maximize";
804
- const keyboardEnabled = options.keyboard !== false;
805
- const keyboardOptions = typeof options.keyboard === "object" ? options.keyboard : {};
806
- const moveStep = keyboardOptions.moveStep ?? 16;
807
- const cycleEnabled = keyboardOptions.cycle !== false;
808
- element.dataset.wmDesktop = "";
809
- if (view.getComputedStyle(element).position === "static") {
810
- element.style.position = "relative";
811
- }
812
- const registry = /* @__PURE__ */ new Map();
813
- const cleanup = [];
814
- let lastOrder = null;
815
- let lastFocused = null;
816
- let drag = null;
817
- let announcer = null;
818
- if (options.announce !== false) {
819
- announcer = createAnnouncer(
820
- wm,
821
- element,
822
- typeof options.announce === "object" ? options.announce : {}
823
- );
824
- cleanup.push(() => announcer?.destroy());
825
- }
826
- let preview = null;
827
- function showPreview(bounds) {
828
- if (!snapPreviewEnabled) return;
829
- if (!preview) {
830
- preview = doc.createElement("div");
831
- preview.dataset.wmSnapPreview = "";
832
- preview.style.cssText = "position:absolute;left:0;top:0;pointer-events:none;display:none;z-index:2147483646";
833
- element.append(preview);
834
- }
835
- preview.style.display = "block";
836
- preview.style.transform = `translate3d(${bounds.x}px, ${bounds.y}px, 0)`;
837
- preview.style.width = `${bounds.width}px`;
838
- preview.style.height = `${bounds.height}px`;
839
- }
840
- function hidePreview() {
841
- if (preview) preview.style.display = "none";
842
- }
843
- if (options.autoViewport !== false) {
844
- const applyViewport = () => wm.setViewport({ width: element.clientWidth, height: element.clientHeight });
845
- applyViewport();
846
- const observer = new ResizeObserver(applyViewport);
847
- observer.observe(element);
848
- cleanup.push(() => observer.disconnect());
849
- }
850
- function syncWindow(attached, win, zIndex) {
851
- const el = attached.element;
852
- const firstSync = attached.lastState === null;
853
- if (firstSync) el.style.transition = "none";
854
- if (attached.lastState !== win) {
855
- el.style.transform = `translate3d(${win.bounds.x}px, ${win.bounds.y}px, 0)`;
856
- el.style.width = `${win.bounds.width}px`;
857
- el.style.height = `${win.bounds.height}px`;
858
- el.dataset.wmStage = win.stage;
859
- el.dataset.wmLayer = win.layer;
860
- el.hidden = win.stage === "minimized";
861
- el.setAttribute("aria-label", win.title);
862
- if (win.layer === "modal") el.setAttribute("aria-modal", "true");
863
- else el.removeAttribute("aria-modal");
864
- for (const handle of attached.handles) {
865
- handle.style.display = win.resizable && win.stage === "normal" ? "" : "none";
866
- }
867
- attached.lastState = win;
868
- }
869
- el.style.zIndex = String(zIndex + 1);
870
- if (firstSync) {
871
- void el.offsetWidth;
872
- el.style.transition = "";
873
- }
874
- }
875
- function syncAll() {
876
- const state = wm.getState();
877
- const orderChanged = state.order !== lastOrder;
878
- state.order.forEach((id, index) => {
879
- const attached = registry.get(id);
880
- const win = state.windows[id];
881
- if (!attached || !win) return;
882
- if (orderChanged || attached.lastState !== win) syncWindow(attached, win, index);
883
- });
884
- if (state.focusedId !== lastFocused) {
885
- if (lastFocused) {
886
- const prev = registry.get(lastFocused);
887
- if (prev) delete prev.element.dataset.wmFocused;
888
- }
889
- if (state.focusedId) {
890
- const next = registry.get(state.focusedId);
891
- if (next) next.element.dataset.wmFocused = "";
892
- }
893
- lastFocused = state.focusedId;
894
- }
895
- lastOrder = state.order;
896
- }
897
- cleanup.push(wm.subscribe(syncAll));
898
- cleanup.push(
899
- wm.on("focus", ({ window: win }) => {
900
- const attached = registry.get(win.id);
901
- if (!attached) return;
902
- if (!attached.element.contains(doc.activeElement)) {
903
- attached.element.focus({ preventScroll: true });
904
- }
905
- })
906
- );
907
- cleanup.push(
908
- wm.on("modalblocked", ({ window: win }) => {
909
- const attached = registry.get(win.id);
910
- if (!attached) return;
911
- delete attached.element.dataset.wmFlash;
912
- void attached.element.offsetWidth;
913
- attached.element.dataset.wmFlash = "";
914
- })
915
- );
916
- cleanup.push(
917
- wm.on("stage", ({ window: win, previous }) => {
918
- if (win.stage !== "minimized" || previous === "minimized") return;
919
- const attached = registry.get(win.id);
920
- const target = options.minimizeTarget?.(win);
921
- if (attached && target) flipToTarget(attached.element, target);
922
- })
923
- );
924
- if (keyboardEnabled && cycleEnabled) {
925
- const onDesktopKeydown = (event) => {
926
- if (event.key === "F6") {
927
- event.preventDefault();
928
- wm.cycleFocus(event.shiftKey ? -1 : 1);
929
- }
930
- };
931
- element.addEventListener("keydown", onDesktopKeydown);
932
- cleanup.push(() => element.removeEventListener("keydown", onDesktopKeydown));
933
- }
934
- function desktopPoint(event) {
935
- const rect = element.getBoundingClientRect();
936
- return { x: event.clientX - rect.left, y: event.clientY - rect.top };
937
- }
938
- function endDrag(cancelled) {
939
- if (drag) drag.finish(cancelled);
940
- }
941
- function startDrag(id, handle, event) {
1011
+
1012
+ // src/dom/drag.ts
1013
+ function createDragStarter(ctx) {
1014
+ const { wm, doc, view } = ctx;
1015
+ return function startDrag(id, handle, event) {
942
1016
  const win = wm.get(id);
943
- if (!win?.draggable || event.button !== 0 || drag) return;
1017
+ if (!win?.draggable || event.button !== 0 || ctx.currentDrag()) return;
944
1018
  const target = event.target;
945
1019
  if (target?.closest(INTERACTIVE_SELECTOR)) return;
946
1020
  event.preventDefault();
947
- const point = desktopPoint(event);
1021
+ const releaseRect = ctx.trackRect();
1022
+ const point = ctx.toLocal(event);
948
1023
  const startBounds = win.bounds;
949
1024
  const session = {
950
1025
  id,
@@ -966,10 +1041,9 @@ function attachDesktop(wm, element, options = {}) {
966
1041
  finish: () => {
967
1042
  }
968
1043
  };
969
- const attached = registry.get(id);
970
- const el = attached?.element;
1044
+ const el = ctx.windowElement(id);
971
1045
  function flush() {
972
- if (!session.hasPending || drag !== session) return;
1046
+ if (!session.hasPending || ctx.currentDrag() !== session) return;
973
1047
  session.hasPending = false;
974
1048
  session.raf = 0;
975
1049
  const current = wm.get(id);
@@ -992,33 +1066,56 @@ function attachDesktop(wm, element, options = {}) {
992
1066
  }
993
1067
  return;
994
1068
  }
995
- wm.move(id, session.pendingX - session.grabDX, session.pendingY - session.grabDY);
996
- if (snapEnabled && current.snappable) {
1069
+ let nextX = session.pendingX - session.grabDX;
1070
+ let nextY = session.pendingY - session.grabDY;
1071
+ if (ctx.magnetThreshold > 0) {
1072
+ const state = wm.getState();
1073
+ const targets = [
1074
+ { x: 0, y: 0, width: state.viewport.width, height: state.viewport.height }
1075
+ ];
1076
+ for (const otherId of state.order) {
1077
+ if (otherId === id) continue;
1078
+ const other = state.windows[otherId];
1079
+ if (other && other.stage !== "minimized") targets.push(other.bounds);
1080
+ }
1081
+ const magnet = magnetize(
1082
+ { x: nextX, y: nextY, width: current.bounds.width, height: current.bounds.height },
1083
+ targets,
1084
+ ctx.magnetThreshold
1085
+ );
1086
+ nextX = magnet.x;
1087
+ nextY = magnet.y;
1088
+ }
1089
+ wm.move(id, nextX, nextY);
1090
+ if (ctx.snapEnabled && current.snappable) {
997
1091
  const viewport = wm.getState().viewport;
998
- const rawZone = detectSnapZone(session.pendingX, session.pendingY, viewport, snapOptions);
1092
+ const rawZone = detectSnapZone(session.pendingX, session.pendingY, viewport, ctx.snapDetect);
999
1093
  let zone = rawZone;
1000
1094
  if (rawZone === "top") {
1001
- if (topEdge === "maximize") zone = current.maximizable ? "maximize" : null;
1002
- else if (topEdge === "none") zone = null;
1095
+ if (ctx.topEdge === "maximize") zone = current.maximizable ? "maximize" : null;
1096
+ else if (ctx.topEdge === "none") zone = null;
1003
1097
  }
1004
1098
  session.zone = zone;
1005
1099
  if (zone) {
1006
- showPreview(
1100
+ ctx.showPreview(
1007
1101
  zone === "maximize" ? { x: 0, y: 0, width: viewport.width, height: viewport.height } : zoneBounds(zone, viewport)
1008
1102
  );
1009
1103
  } else {
1010
- hidePreview();
1104
+ ctx.hidePreview();
1011
1105
  }
1012
1106
  }
1013
1107
  }
1014
1108
  function onMove(moveEvent) {
1015
1109
  if (moveEvent.pointerId !== session.pointerId) return;
1016
- const movePoint = desktopPoint(moveEvent);
1110
+ const movePoint = ctx.toLocal(moveEvent);
1017
1111
  if (!session.moved) {
1018
1112
  const travelled = Math.abs(movePoint.x - (session.startBounds.x + session.grabDX)) + Math.abs(movePoint.y - (session.startBounds.y + session.grabDY));
1019
1113
  if (travelled < 3 && session.restored) return;
1020
1114
  session.moved = true;
1021
- if (el) el.dataset.wmDragging = "";
1115
+ if (el) {
1116
+ el.dataset.wmDragging = "";
1117
+ el.style.willChange = "transform";
1118
+ }
1022
1119
  }
1023
1120
  session.pendingX = movePoint.x;
1024
1121
  session.pendingY = movePoint.y;
@@ -1040,10 +1137,11 @@ function attachDesktop(wm, element, options = {}) {
1040
1137
  }
1041
1138
  }
1042
1139
  function finish(cancelled) {
1043
- if (drag !== session) return;
1044
- if (session.raf !== 0) view?.cancelAnimationFrame(session.raf);
1140
+ if (ctx.currentDrag() !== session) return;
1141
+ if (session.raf !== 0) view.cancelAnimationFrame(session.raf);
1045
1142
  if (session.hasPending && !cancelled) flush();
1046
- drag = null;
1143
+ ctx.releaseDrag(session);
1144
+ releaseRect();
1047
1145
  handle.removeEventListener("pointermove", onMove);
1048
1146
  handle.removeEventListener("pointerup", onUp);
1049
1147
  handle.removeEventListener("pointercancel", onCancel);
@@ -1051,8 +1149,11 @@ function attachDesktop(wm, element, options = {}) {
1051
1149
  if (handle.hasPointerCapture(session.pointerId)) {
1052
1150
  handle.releasePointerCapture(session.pointerId);
1053
1151
  }
1054
- if (el) delete el.dataset.wmDragging;
1055
- hidePreview();
1152
+ if (el) {
1153
+ delete el.dataset.wmDragging;
1154
+ el.style.willChange = "";
1155
+ }
1156
+ ctx.hidePreview();
1056
1157
  if (cancelled) {
1057
1158
  if (session.moved && session.restored) {
1058
1159
  if (session.startStage === "maximized") {
@@ -1065,28 +1166,49 @@ function attachDesktop(wm, element, options = {}) {
1065
1166
  wm.move(id, session.startBounds.x, session.startBounds.y);
1066
1167
  }
1067
1168
  }
1169
+ wm.endInteraction();
1068
1170
  return;
1069
1171
  }
1070
1172
  if (session.moved && session.zone) {
1071
1173
  if (session.zone === "maximize") wm.maximize(id);
1072
1174
  else wm.snap(id, session.zone);
1073
1175
  }
1176
+ wm.endInteraction();
1074
1177
  }
1075
1178
  session.finish = finish;
1076
- drag = session;
1179
+ wm.beginInteraction();
1180
+ ctx.claimDrag(session);
1077
1181
  handle.setPointerCapture(event.pointerId);
1078
1182
  handle.addEventListener("pointermove", onMove);
1079
1183
  handle.addEventListener("pointerup", onUp);
1080
1184
  handle.addEventListener("pointercancel", onCancel);
1081
1185
  doc.addEventListener("keydown", onKeydown, true);
1082
- }
1083
- function startResize(id, direction, event) {
1186
+ };
1187
+ }
1188
+
1189
+ // src/dom/resize.ts
1190
+ var RESIZE_DIRECTIONS = ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
1191
+ var RESIZE_CURSORS = {
1192
+ n: "ns-resize",
1193
+ s: "ns-resize",
1194
+ e: "ew-resize",
1195
+ w: "ew-resize",
1196
+ ne: "nesw-resize",
1197
+ sw: "nesw-resize",
1198
+ nw: "nwse-resize",
1199
+ se: "nwse-resize"
1200
+ };
1201
+ function createResizeStarter(ctx) {
1202
+ const { wm, doc, view } = ctx;
1203
+ return function startResize(id, direction, event) {
1084
1204
  const win = wm.get(id);
1085
- if (!win?.resizable || event.button !== 0 || drag) return;
1205
+ if (!win?.resizable || event.button !== 0 || ctx.currentDrag()) return;
1086
1206
  event.preventDefault();
1087
1207
  event.stopPropagation();
1088
1208
  const handleEl = event.currentTarget;
1089
- const startPoint = desktopPoint(event);
1209
+ wm.beginInteraction();
1210
+ const releaseRect = ctx.trackRect();
1211
+ const startPoint = ctx.toLocal(event);
1090
1212
  const start = win.bounds;
1091
1213
  const minSize = win.minSize;
1092
1214
  const maxSize = win.maxSize;
@@ -1094,8 +1216,8 @@ function attachDesktop(wm, element, options = {}) {
1094
1216
  let pendingX = 0;
1095
1217
  let pendingY = 0;
1096
1218
  let hasPending = false;
1097
- const attached = registry.get(id);
1098
- if (attached) attached.element.dataset.wmResizing = direction;
1219
+ const el = ctx.windowElement(id);
1220
+ if (el) el.dataset.wmResizing = direction;
1099
1221
  function clampWidth(width) {
1100
1222
  return clamp(width, minSize.width, maxSize ? maxSize.width : Number.POSITIVE_INFINITY);
1101
1223
  }
@@ -1123,15 +1245,16 @@ function attachDesktop(wm, element, options = {}) {
1123
1245
  }
1124
1246
  function onMove(moveEvent) {
1125
1247
  if (moveEvent.pointerId !== event.pointerId) return;
1126
- const movePoint = desktopPoint(moveEvent);
1248
+ const movePoint = ctx.toLocal(moveEvent);
1127
1249
  pendingX = movePoint.x;
1128
1250
  pendingY = movePoint.y;
1129
1251
  hasPending = true;
1130
- if (raf === 0) raf = view?.requestAnimationFrame(flush) ?? 0;
1252
+ if (raf === 0) raf = view.requestAnimationFrame(flush);
1131
1253
  }
1132
1254
  function finish(cancelled) {
1133
- if (raf !== 0) view?.cancelAnimationFrame(raf);
1255
+ if (raf !== 0) view.cancelAnimationFrame(raf);
1134
1256
  if (hasPending && !cancelled) flush();
1257
+ releaseRect();
1135
1258
  handleEl.removeEventListener("pointermove", onMove);
1136
1259
  handleEl.removeEventListener("pointerup", onUp);
1137
1260
  handleEl.removeEventListener("pointercancel", onCancelPointer);
@@ -1139,8 +1262,9 @@ function attachDesktop(wm, element, options = {}) {
1139
1262
  if (handleEl.hasPointerCapture(event.pointerId)) {
1140
1263
  handleEl.releasePointerCapture(event.pointerId);
1141
1264
  }
1142
- if (attached) delete attached.element.dataset.wmResizing;
1265
+ if (el) delete el.dataset.wmResizing;
1143
1266
  if (cancelled) wm.resize(id, start);
1267
+ wm.endInteraction();
1144
1268
  }
1145
1269
  function onUp(upEvent) {
1146
1270
  if (upEvent.pointerId !== event.pointerId) return;
@@ -1161,6 +1285,227 @@ function attachDesktop(wm, element, options = {}) {
1161
1285
  handleEl.addEventListener("pointerup", onUp);
1162
1286
  handleEl.addEventListener("pointercancel", onCancelPointer);
1163
1287
  doc.addEventListener("keydown", onKeydown, true);
1288
+ };
1289
+ }
1290
+ function createResizeHandles(doc, edge, corner) {
1291
+ const base = "position:absolute;touch-action:none;user-select:none;-webkit-user-select:none;";
1292
+ const styles = {
1293
+ n: `top:${-edge / 2}px;left:${corner}px;right:${corner}px;height:${edge}px`,
1294
+ s: `bottom:${-edge / 2}px;left:${corner}px;right:${corner}px;height:${edge}px`,
1295
+ e: `right:${-edge / 2}px;top:${corner}px;bottom:${corner}px;width:${edge}px`,
1296
+ w: `left:${-edge / 2}px;top:${corner}px;bottom:${corner}px;width:${edge}px`,
1297
+ ne: `top:${-edge / 2}px;right:${-edge / 2}px;width:${corner}px;height:${corner}px`,
1298
+ nw: `top:${-edge / 2}px;left:${-edge / 2}px;width:${corner}px;height:${corner}px`,
1299
+ se: `bottom:${-edge / 2}px;right:${-edge / 2}px;width:${corner}px;height:${corner}px`,
1300
+ sw: `bottom:${-edge / 2}px;left:${-edge / 2}px;width:${corner}px;height:${corner}px`
1301
+ };
1302
+ return RESIZE_DIRECTIONS.map((direction) => {
1303
+ const element = doc.createElement("div");
1304
+ element.dataset.wmResize = direction;
1305
+ element.setAttribute("aria-hidden", "true");
1306
+ element.style.cssText = `${base}${styles[direction]};cursor:${RESIZE_CURSORS[direction]}`;
1307
+ return { element, direction };
1308
+ });
1309
+ }
1310
+
1311
+ // src/dom/controller.ts
1312
+ function attachDesktop(wm, element, options = {}) {
1313
+ const doc = element.ownerDocument;
1314
+ const view = windowOf(element);
1315
+ const coarsePointer = typeof view.matchMedia === "function" && view.matchMedia("(pointer: coarse)").matches;
1316
+ const snapEnabled = options.snap !== false;
1317
+ const snapOptions = typeof options.snap === "object" ? options.snap : {};
1318
+ const snapPreviewEnabled = snapOptions.preview !== false;
1319
+ const topEdge = snapOptions.topEdge ?? "maximize";
1320
+ const keyboardEnabled = options.keyboard !== false;
1321
+ const keyboardOptions = typeof options.keyboard === "object" ? options.keyboard : {};
1322
+ const moveStep = keyboardOptions.moveStep ?? 16;
1323
+ const cycleEnabled = keyboardOptions.cycle !== false;
1324
+ const hitEdge = options.hitAreas?.edge ?? (coarsePointer ? 16 : 8);
1325
+ const hitCorner = options.hitAreas?.corner ?? (coarsePointer ? 24 : 12);
1326
+ const magnetThreshold = options.magnetism === false ? 0 : (typeof options.magnetism === "object" ? options.magnetism.threshold : void 0) ?? (coarsePointer ? 12 : 8);
1327
+ element.dataset.wmDesktop = "";
1328
+ if (view.getComputedStyle(element).position === "static") {
1329
+ element.style.position = "relative";
1330
+ }
1331
+ const registry = /* @__PURE__ */ new Map();
1332
+ const cleanup = [];
1333
+ let lastOrder = null;
1334
+ let lastFocused = null;
1335
+ let drag = null;
1336
+ let cachedRect = null;
1337
+ let rectUsers = 0;
1338
+ let announcer = null;
1339
+ if (options.announce !== false) {
1340
+ announcer = createAnnouncer(
1341
+ wm,
1342
+ element,
1343
+ typeof options.announce === "object" ? options.announce : {}
1344
+ );
1345
+ cleanup.push(() => announcer?.destroy());
1346
+ }
1347
+ let preview = null;
1348
+ function showPreview(bounds) {
1349
+ if (!snapPreviewEnabled) return;
1350
+ if (!preview) {
1351
+ preview = doc.createElement("div");
1352
+ preview.dataset.wmSnapPreview = "";
1353
+ preview.style.cssText = "position:absolute;left:0;top:0;pointer-events:none;display:none;z-index:2147483646";
1354
+ element.append(preview);
1355
+ }
1356
+ preview.style.display = "block";
1357
+ preview.style.transform = `translate3d(${bounds.x}px, ${bounds.y}px, 0)`;
1358
+ preview.style.width = `${bounds.width}px`;
1359
+ preview.style.height = `${bounds.height}px`;
1360
+ }
1361
+ function hidePreview() {
1362
+ if (preview) preview.style.display = "none";
1363
+ }
1364
+ const ctx = {
1365
+ wm,
1366
+ doc,
1367
+ view,
1368
+ toLocal(event) {
1369
+ const rect = cachedRect ?? element.getBoundingClientRect();
1370
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
1371
+ },
1372
+ trackRect() {
1373
+ if (rectUsers === 0) {
1374
+ const rect = element.getBoundingClientRect();
1375
+ cachedRect = { left: rect.left, top: rect.top };
1376
+ }
1377
+ rectUsers += 1;
1378
+ let released = false;
1379
+ return () => {
1380
+ if (released) return;
1381
+ released = true;
1382
+ rectUsers -= 1;
1383
+ if (rectUsers === 0) cachedRect = null;
1384
+ };
1385
+ },
1386
+ windowElement: (id) => registry.get(id)?.element,
1387
+ showPreview,
1388
+ hidePreview,
1389
+ snapEnabled,
1390
+ snapDetect: {
1391
+ threshold: snapOptions.threshold ?? (coarsePointer ? 20 : 12),
1392
+ cornerSize: snapOptions.cornerSize ?? (coarsePointer ? 96 : 64)
1393
+ },
1394
+ topEdge,
1395
+ hitEdge,
1396
+ hitCorner,
1397
+ magnetThreshold,
1398
+ currentDrag: () => drag,
1399
+ claimDrag(session) {
1400
+ drag = session;
1401
+ },
1402
+ releaseDrag(session) {
1403
+ if (drag === session) drag = null;
1404
+ }
1405
+ };
1406
+ const startDrag = createDragStarter(ctx);
1407
+ const startResize = createResizeStarter(ctx);
1408
+ if (options.autoViewport !== false) {
1409
+ const applyViewport = () => wm.setViewport({ width: element.clientWidth, height: element.clientHeight });
1410
+ applyViewport();
1411
+ const observer = new ResizeObserver(applyViewport);
1412
+ observer.observe(element);
1413
+ cleanup.push(() => observer.disconnect());
1414
+ }
1415
+ function syncWindow(attached, win, zIndex) {
1416
+ const el = attached.element;
1417
+ const firstSync = attached.lastState === null;
1418
+ if (firstSync) el.style.transition = "none";
1419
+ if (attached.lastState !== win) {
1420
+ el.style.transform = `translate3d(${win.bounds.x}px, ${win.bounds.y}px, 0)`;
1421
+ el.style.width = `${win.bounds.width}px`;
1422
+ el.style.height = `${win.bounds.height}px`;
1423
+ el.dataset.wmStage = win.stage;
1424
+ el.dataset.wmLayer = win.layer;
1425
+ el.hidden = win.stage === "minimized";
1426
+ el.setAttribute("aria-label", win.title);
1427
+ if (win.layer === "modal") el.setAttribute("aria-modal", "true");
1428
+ else el.removeAttribute("aria-modal");
1429
+ for (const handle of attached.handles) {
1430
+ handle.style.display = win.resizable && win.stage === "normal" ? "" : "none";
1431
+ }
1432
+ attached.lastState = win;
1433
+ }
1434
+ if (attached.lastZ !== zIndex) {
1435
+ el.style.zIndex = String(zIndex + 1);
1436
+ attached.lastZ = zIndex;
1437
+ }
1438
+ if (firstSync) {
1439
+ void el.offsetWidth;
1440
+ el.style.transition = "";
1441
+ }
1442
+ }
1443
+ function syncAll() {
1444
+ const state = wm.getState();
1445
+ const orderChanged = state.order !== lastOrder;
1446
+ state.order.forEach((id, index) => {
1447
+ const attached = registry.get(id);
1448
+ const win = state.windows[id];
1449
+ if (!attached || !win) return;
1450
+ if (orderChanged || attached.lastState !== win) syncWindow(attached, win, index);
1451
+ });
1452
+ if (state.focusedId !== lastFocused) {
1453
+ if (lastFocused) {
1454
+ const prev = registry.get(lastFocused);
1455
+ if (prev) delete prev.element.dataset.wmFocused;
1456
+ }
1457
+ if (state.focusedId) {
1458
+ const next = registry.get(state.focusedId);
1459
+ if (next) next.element.dataset.wmFocused = "";
1460
+ }
1461
+ lastFocused = state.focusedId;
1462
+ }
1463
+ lastOrder = state.order;
1464
+ }
1465
+ cleanup.push(wm.subscribe(syncAll));
1466
+ cleanup.push(
1467
+ wm.on("focus", ({ window: win }) => {
1468
+ const attached = registry.get(win.id);
1469
+ if (!attached) return;
1470
+ if (!attached.element.contains(doc.activeElement)) {
1471
+ attached.element.focus({ preventScroll: true });
1472
+ }
1473
+ })
1474
+ );
1475
+ cleanup.push(
1476
+ wm.on("modalblocked", ({ window: win }) => {
1477
+ const attached = registry.get(win.id);
1478
+ if (!attached) return;
1479
+ delete attached.element.dataset.wmFlash;
1480
+ void attached.element.offsetWidth;
1481
+ attached.element.dataset.wmFlash = "";
1482
+ })
1483
+ );
1484
+ cleanup.push(
1485
+ wm.on("stage", ({ window: win, previous }) => {
1486
+ const attached = registry.get(win.id);
1487
+ if (!attached) return;
1488
+ if (win.stage === "minimized" && previous !== "minimized") {
1489
+ const target = options.minimizeTarget?.(win);
1490
+ if (target) flipToTarget(attached.element, target);
1491
+ } else if (previous === "minimized" && win.stage !== "minimized") {
1492
+ const target = options.minimizeTarget?.(win);
1493
+ if (target) flipFromTarget(attached.element, target);
1494
+ }
1495
+ })
1496
+ );
1497
+ if (keyboardEnabled && cycleEnabled) {
1498
+ const onDesktopKeydown = (event) => {
1499
+ if (event.key === "F6") {
1500
+ event.preventDefault();
1501
+ wm.cycleFocus(event.shiftKey ? -1 : 1);
1502
+ }
1503
+ };
1504
+ element.addEventListener("keydown", onDesktopKeydown);
1505
+ cleanup.push(() => element.removeEventListener("keydown", onDesktopKeydown));
1506
+ }
1507
+ function endDrag(cancelled) {
1508
+ if (drag) drag.finish(cancelled);
1164
1509
  }
1165
1510
  function attachWindow(id, windowElement, windowOptions = {}) {
1166
1511
  const win = wm.get(id);
@@ -1171,6 +1516,7 @@ function attachDesktop(wm, element, options = {}) {
1171
1516
  handle: null,
1172
1517
  handles: [],
1173
1518
  lastState: null,
1519
+ lastZ: -1,
1174
1520
  cleanup: []
1175
1521
  };
1176
1522
  windowElement.dataset.wmWindow = id;
@@ -1221,26 +1567,23 @@ function attachDesktop(wm, element, options = {}) {
1221
1567
  };
1222
1568
  handle.addEventListener("dblclick", onDoubleClick);
1223
1569
  attached.cleanup.push(() => handle.removeEventListener("dblclick", onDoubleClick));
1570
+ if (options.onTitlebarContextMenu) {
1571
+ const onContextMenu = (event) => {
1572
+ const current = wm.get(id);
1573
+ if (!current) return;
1574
+ event.preventDefault();
1575
+ options.onTitlebarContextMenu?.(current, event);
1576
+ };
1577
+ handle.addEventListener("contextmenu", onContextMenu);
1578
+ attached.cleanup.push(() => handle.removeEventListener("contextmenu", onContextMenu));
1579
+ }
1224
1580
  }
1225
1581
  if (windowOptions.resizeHandles !== false) {
1226
- for (const direction of RESIZE_DIRECTIONS) {
1227
- const resizeHandle = doc.createElement("div");
1228
- resizeHandle.dataset.wmResize = direction;
1229
- resizeHandle.setAttribute("aria-hidden", "true");
1230
- const base = "position:absolute;touch-action:none;user-select:none;-webkit-user-select:none;";
1231
- const size = 8;
1232
- const corner = 12;
1233
- const styles = {
1234
- n: `top:${-size / 2}px;left:${corner}px;right:${corner}px;height:${size}px`,
1235
- s: `bottom:${-size / 2}px;left:${corner}px;right:${corner}px;height:${size}px`,
1236
- e: `right:${-size / 2}px;top:${corner}px;bottom:${corner}px;width:${size}px`,
1237
- w: `left:${-size / 2}px;top:${corner}px;bottom:${corner}px;width:${size}px`,
1238
- ne: `top:${-size / 2}px;right:${-size / 2}px;width:${corner}px;height:${corner}px`,
1239
- nw: `top:${-size / 2}px;left:${-size / 2}px;width:${corner}px;height:${corner}px`,
1240
- se: `bottom:${-size / 2}px;right:${-size / 2}px;width:${corner}px;height:${corner}px`,
1241
- sw: `bottom:${-size / 2}px;left:${-size / 2}px;width:${corner}px;height:${corner}px`
1242
- };
1243
- resizeHandle.style.cssText = `${base}${styles[direction]};cursor:${RESIZE_CURSORS[direction]}`;
1582
+ for (const { element: resizeHandle, direction } of createResizeHandles(
1583
+ doc,
1584
+ hitEdge,
1585
+ hitCorner
1586
+ )) {
1244
1587
  const onResizeDown = (event) => startResize(id, direction, event);
1245
1588
  resizeHandle.addEventListener("pointerdown", onResizeDown);
1246
1589
  attached.cleanup.push(() => resizeHandle.removeEventListener("pointerdown", onResizeDown));
@@ -1298,16 +1641,25 @@ function attachDesktop(wm, element, options = {}) {
1298
1641
  };
1299
1642
  windowElement.addEventListener("keydown", onModalTrap);
1300
1643
  attached.cleanup.push(() => windowElement.removeEventListener("keydown", onModalTrap));
1301
- registry.set(id, attached);
1302
- lastOrder = null;
1303
- lastFocused = null;
1304
- syncAll();
1305
- return () => {
1644
+ const detach = () => {
1306
1645
  if (drag?.id === id) endDrag(true);
1307
1646
  for (const dispose of attached.cleanup) dispose();
1308
1647
  for (const resizeHandle of attached.handles) resizeHandle.remove();
1309
1648
  registry.delete(id);
1310
1649
  };
1650
+ if (windowOptions.removeOnClose) {
1651
+ const stopOnClose = wm.on("close", ({ window: closed }) => {
1652
+ if (closed.id !== id) return;
1653
+ detach();
1654
+ windowElement.remove();
1655
+ });
1656
+ attached.cleanup.push(stopOnClose);
1657
+ }
1658
+ registry.set(id, attached);
1659
+ lastOrder = null;
1660
+ lastFocused = null;
1661
+ syncAll();
1662
+ return detach;
1311
1663
  }
1312
1664
  return {
1313
1665
  element,
@@ -1326,6 +1678,8 @@ function attachDesktop(wm, element, options = {}) {
1326
1678
  }
1327
1679
  };
1328
1680
  }
1681
+
1682
+ // src/dom/binder.ts
1329
1683
  function createDesktopBinder(wm, options = {}) {
1330
1684
  let controller = null;
1331
1685
  const entries = /* @__PURE__ */ new Set();
@@ -1377,7 +1731,8 @@ exports.createWindowManager = createWindowManager;
1377
1731
  exports.defaultMessages = defaultMessages;
1378
1732
  exports.detectSnapZone = detectSnapZone;
1379
1733
  exports.flipToTarget = flipToTarget;
1734
+ exports.magnetize = magnetize;
1380
1735
  exports.prefersReducedMotion = prefersReducedMotion;
1381
1736
  exports.zoneBounds = zoneBounds;
1382
- //# sourceMappingURL=chunk-TXINVEK3.cjs.map
1383
- //# sourceMappingURL=chunk-TXINVEK3.cjs.map
1737
+ //# sourceMappingURL=chunk-B35DANTX.cjs.map
1738
+ //# sourceMappingURL=chunk-B35DANTX.cjs.map