@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
@@ -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;
556
602
  }
557
603
  }
558
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
+ }
623
+ }
624
+ }
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;
@@ -839,7 +1064,27 @@ function createDragStarter(ctx) {
839
1064
  }
840
1065
  return;
841
1066
  }
842
- wm.move(id, session.pendingX - session.grabDX, session.pendingY - session.grabDY);
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);
843
1088
  if (ctx.snapEnabled && current.snappable) {
844
1089
  const viewport = wm.getState().viewport;
845
1090
  const rawZone = detectSnapZone(session.pendingX, session.pendingY, viewport, ctx.snapDetect);
@@ -919,14 +1164,17 @@ function createDragStarter(ctx) {
919
1164
  wm.move(id, session.startBounds.x, session.startBounds.y);
920
1165
  }
921
1166
  }
1167
+ wm.endInteraction();
922
1168
  return;
923
1169
  }
924
1170
  if (session.moved && session.zone) {
925
1171
  if (session.zone === "maximize") wm.maximize(id);
926
1172
  else wm.snap(id, session.zone);
927
1173
  }
1174
+ wm.endInteraction();
928
1175
  }
929
1176
  session.finish = finish;
1177
+ wm.beginInteraction();
930
1178
  ctx.claimDrag(session);
931
1179
  handle.setPointerCapture(event.pointerId);
932
1180
  handle.addEventListener("pointermove", onMove);
@@ -956,6 +1204,7 @@ function createResizeStarter(ctx) {
956
1204
  event.preventDefault();
957
1205
  event.stopPropagation();
958
1206
  const handleEl = event.currentTarget;
1207
+ wm.beginInteraction();
959
1208
  const releaseRect = ctx.trackRect();
960
1209
  const startPoint = ctx.toLocal(event);
961
1210
  const start = win.bounds;
@@ -1013,6 +1262,7 @@ function createResizeStarter(ctx) {
1013
1262
  }
1014
1263
  if (el) delete el.dataset.wmResizing;
1015
1264
  if (cancelled) wm.resize(id, start);
1265
+ wm.endInteraction();
1016
1266
  }
1017
1267
  function onUp(upEvent) {
1018
1268
  if (upEvent.pointerId !== event.pointerId) return;
@@ -1071,6 +1321,7 @@ function attachDesktop(wm, element, options = {}) {
1071
1321
  const cycleEnabled = keyboardOptions.cycle !== false;
1072
1322
  const hitEdge = options.hitAreas?.edge ?? (coarsePointer ? 16 : 8);
1073
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);
1074
1325
  element.dataset.wmDesktop = "";
1075
1326
  if (view.getComputedStyle(element).position === "static") {
1076
1327
  element.style.position = "relative";
@@ -1141,6 +1392,7 @@ function attachDesktop(wm, element, options = {}) {
1141
1392
  topEdge,
1142
1393
  hitEdge,
1143
1394
  hitCorner,
1395
+ magnetThreshold,
1144
1396
  currentDrag: () => drag,
1145
1397
  claimDrag(session) {
1146
1398
  drag = session;
@@ -1229,10 +1481,15 @@ function attachDesktop(wm, element, options = {}) {
1229
1481
  );
1230
1482
  cleanup.push(
1231
1483
  wm.on("stage", ({ window: win, previous }) => {
1232
- if (win.stage !== "minimized" || previous === "minimized") return;
1233
1484
  const attached = registry.get(win.id);
1234
- const target = options.minimizeTarget?.(win);
1235
- if (attached && target) flipToTarget(attached.element, target);
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
+ }
1236
1493
  })
1237
1494
  );
1238
1495
  if (keyboardEnabled && cycleEnabled) {
@@ -1308,6 +1565,16 @@ function attachDesktop(wm, element, options = {}) {
1308
1565
  };
1309
1566
  handle.addEventListener("dblclick", onDoubleClick);
1310
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
+ }
1311
1578
  }
1312
1579
  if (windowOptions.resizeHandles !== false) {
1313
1580
  for (const { element: resizeHandle, direction } of createResizeHandles(
@@ -1450,6 +1717,6 @@ function createDesktopBinder(wm, options = {}) {
1450
1717
  };
1451
1718
  }
1452
1719
 
1453
- export { attachDesktop, boundsEqual, clamp, clampSize, clampToViewport, createAnnouncer, createDesktopBinder, createEmitter, createWindowManager, defaultMessages, detectSnapZone, flipToTarget, prefersReducedMotion, zoneBounds };
1454
- //# sourceMappingURL=chunk-2TVOEDRG.js.map
1455
- //# sourceMappingURL=chunk-2TVOEDRG.js.map
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