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