@surdeddd/wmkit 0.2.0 → 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 (45) hide show
  1. package/README.md +55 -5
  2. package/README.ru.md +18 -5
  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/{binder-BY-saDUB.d.cts → binder-0Oku22Nc.d.ts} +14 -2
  10. package/dist/{binder-D9UEE1SM.d.ts → binder-Dq_AZRbo.d.cts} +14 -2
  11. package/dist/{chunk-KQCVZ2W7.cjs → chunk-B35DANTX.cjs} +303 -35
  12. package/dist/chunk-B35DANTX.cjs.map +1 -0
  13. package/dist/{chunk-2TVOEDRG.js → chunk-YCHJXSTC.js} +303 -36
  14. package/dist/chunk-YCHJXSTC.js.map +1 -0
  15. package/dist/index.cjs +19 -15
  16. package/dist/index.d.cts +4 -4
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.js +1 -1
  19. package/dist/persist.d.cts +1 -1
  20. package/dist/persist.d.ts +1 -1
  21. package/dist/popout.d.cts +1 -1
  22. package/dist/popout.d.ts +1 -1
  23. package/dist/react.cjs +3 -3
  24. package/dist/react.d.cts +2 -2
  25. package/dist/react.d.ts +2 -2
  26. package/dist/react.js +1 -1
  27. package/dist/solid.cjs +3 -3
  28. package/dist/solid.d.cts +2 -2
  29. package/dist/solid.d.ts +2 -2
  30. package/dist/solid.js +1 -1
  31. package/dist/svelte.cjs +3 -3
  32. package/dist/svelte.d.cts +2 -2
  33. package/dist/svelte.d.ts +2 -2
  34. package/dist/svelte.js +1 -1
  35. package/dist/themes/light.css +202 -0
  36. package/dist/themes/retro.css +152 -0
  37. package/dist/{types-Bkrq2djr.d.cts → types-DdCtDHgt.d.cts} +23 -1
  38. package/dist/{types-Bkrq2djr.d.ts → types-DdCtDHgt.d.ts} +23 -1
  39. package/dist/vue.cjs +3 -3
  40. package/dist/vue.d.cts +2 -2
  41. package/dist/vue.d.ts +2 -2
  42. package/dist/vue.js +1 -1
  43. package/package.json +34 -7
  44. package/dist/chunk-2TVOEDRG.js.map +0 -1
  45. package/dist/chunk-KQCVZ2W7.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;
558
604
  }
559
605
  }
560
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
+ }
625
+ }
626
+ }
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;
@@ -841,7 +1066,27 @@ function createDragStarter(ctx) {
841
1066
  }
842
1067
  return;
843
1068
  }
844
- wm.move(id, session.pendingX - session.grabDX, session.pendingY - session.grabDY);
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);
845
1090
  if (ctx.snapEnabled && current.snappable) {
846
1091
  const viewport = wm.getState().viewport;
847
1092
  const rawZone = detectSnapZone(session.pendingX, session.pendingY, viewport, ctx.snapDetect);
@@ -921,14 +1166,17 @@ function createDragStarter(ctx) {
921
1166
  wm.move(id, session.startBounds.x, session.startBounds.y);
922
1167
  }
923
1168
  }
1169
+ wm.endInteraction();
924
1170
  return;
925
1171
  }
926
1172
  if (session.moved && session.zone) {
927
1173
  if (session.zone === "maximize") wm.maximize(id);
928
1174
  else wm.snap(id, session.zone);
929
1175
  }
1176
+ wm.endInteraction();
930
1177
  }
931
1178
  session.finish = finish;
1179
+ wm.beginInteraction();
932
1180
  ctx.claimDrag(session);
933
1181
  handle.setPointerCapture(event.pointerId);
934
1182
  handle.addEventListener("pointermove", onMove);
@@ -958,6 +1206,7 @@ function createResizeStarter(ctx) {
958
1206
  event.preventDefault();
959
1207
  event.stopPropagation();
960
1208
  const handleEl = event.currentTarget;
1209
+ wm.beginInteraction();
961
1210
  const releaseRect = ctx.trackRect();
962
1211
  const startPoint = ctx.toLocal(event);
963
1212
  const start = win.bounds;
@@ -1015,6 +1264,7 @@ function createResizeStarter(ctx) {
1015
1264
  }
1016
1265
  if (el) delete el.dataset.wmResizing;
1017
1266
  if (cancelled) wm.resize(id, start);
1267
+ wm.endInteraction();
1018
1268
  }
1019
1269
  function onUp(upEvent) {
1020
1270
  if (upEvent.pointerId !== event.pointerId) return;
@@ -1073,6 +1323,7 @@ function attachDesktop(wm, element, options = {}) {
1073
1323
  const cycleEnabled = keyboardOptions.cycle !== false;
1074
1324
  const hitEdge = options.hitAreas?.edge ?? (coarsePointer ? 16 : 8);
1075
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);
1076
1327
  element.dataset.wmDesktop = "";
1077
1328
  if (view.getComputedStyle(element).position === "static") {
1078
1329
  element.style.position = "relative";
@@ -1143,6 +1394,7 @@ function attachDesktop(wm, element, options = {}) {
1143
1394
  topEdge,
1144
1395
  hitEdge,
1145
1396
  hitCorner,
1397
+ magnetThreshold,
1146
1398
  currentDrag: () => drag,
1147
1399
  claimDrag(session) {
1148
1400
  drag = session;
@@ -1231,10 +1483,15 @@ function attachDesktop(wm, element, options = {}) {
1231
1483
  );
1232
1484
  cleanup.push(
1233
1485
  wm.on("stage", ({ window: win, previous }) => {
1234
- if (win.stage !== "minimized" || previous === "minimized") return;
1235
1486
  const attached = registry.get(win.id);
1236
- const target = options.minimizeTarget?.(win);
1237
- if (attached && target) flipToTarget(attached.element, target);
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
+ }
1238
1495
  })
1239
1496
  );
1240
1497
  if (keyboardEnabled && cycleEnabled) {
@@ -1310,6 +1567,16 @@ function attachDesktop(wm, element, options = {}) {
1310
1567
  };
1311
1568
  handle.addEventListener("dblclick", onDoubleClick);
1312
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
+ }
1313
1580
  }
1314
1581
  if (windowOptions.resizeHandles !== false) {
1315
1582
  for (const { element: resizeHandle, direction } of createResizeHandles(
@@ -1464,7 +1731,8 @@ exports.createWindowManager = createWindowManager;
1464
1731
  exports.defaultMessages = defaultMessages;
1465
1732
  exports.detectSnapZone = detectSnapZone;
1466
1733
  exports.flipToTarget = flipToTarget;
1734
+ exports.magnetize = magnetize;
1467
1735
  exports.prefersReducedMotion = prefersReducedMotion;
1468
1736
  exports.zoneBounds = zoneBounds;
1469
- //# sourceMappingURL=chunk-KQCVZ2W7.cjs.map
1470
- //# sourceMappingURL=chunk-KQCVZ2W7.cjs.map
1737
+ //# sourceMappingURL=chunk-B35DANTX.cjs.map
1738
+ //# sourceMappingURL=chunk-B35DANTX.cjs.map