@surdeddd/wmkit 0.4.1 → 0.4.2

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.
package/README.md CHANGED
@@ -376,7 +376,7 @@ This README is the tour. The reference lives in [`docs/`](./docs/README.md):
376
376
 
377
377
  ## Quality
378
378
 
379
- - 216 unit tests, **100%** line/branch/function/statement coverage on the core state machine and persistence
379
+ - 250 unit tests: **100%** line/branch/function/statement coverage on the core state machine and persistence, with enforced floors on the DOM layer and the adapters
380
380
  - 190+ Playwright scenarios on Chromium, WebKit and mobile emulation: drag, 8-way resize, snap, magnetism, workspaces, undo after drag, keyboard, touch, persistence across reloads, 50-window stress, modal traps, axe accessibility scans, visual regression screenshots
381
381
  - performance benchmarks run in CI on every push (`vitest bench`): 1 000 windows open in ~150 ms, a move among 50 windows costs ~1.2 µs, a full 100-step undo/redo sweep ~52 µs
382
382
  - `publint` + `@arethetypeswrong/cli` validate the published package, `size-limit` guards bundle budgets
package/README.ru.md CHANGED
@@ -273,7 +273,7 @@ if (isPopoutSupported()) await popout(wm, 'docs', contentElement)
273
273
 
274
274
  ## Качество
275
275
 
276
- - 216 юнит-тестов, **100%** покрытие стейт-машины и persist по строкам/веткам/функциям
276
+ - 250 юнит-тестов: **100%** покрытие стейт-машины и persist по строкам/веткам/функциям, плюс обязательные пороги на DOM-слой и адаптеры
277
277
  - 190+ Playwright-сценариев на Chromium, WebKit и мобильной эмуляции: drag, ресайз во все стороны, снэп, магнетизм, рабочие столы, undo после drag, клавиатура, touch, персист через перезагрузку, стресс на 50 окон, модальные ловушки, axe-аудиты доступности, визуальная регрессия по скриншотам
278
278
  - перф-бенчмарки в CI на каждый push (`vitest bench`): 1 000 окон открываются за ~150 мс, move среди 50 окон ~1.2 мкс, полный undo/redo-проход на 100 шагов ~52 мкс
279
279
  - `publint` + `@arethetypeswrong/cli` проверяют валидность пакета, `size-limit` следит за бюджетами
package/dist/angular.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkTJ6QOOJ2_cjs = require('./chunk-TJ6QOOJ2.cjs');
3
+ var chunkGLBVT4AQ_cjs = require('./chunk-GLBVT4AQ.cjs');
4
4
  var core = require('@angular/core');
5
5
 
6
6
  function onDestroy(dispose) {
@@ -13,7 +13,7 @@ function onDestroy(dispose) {
13
13
  destroyRef?.onDestroy(dispose);
14
14
  }
15
15
  function useWindowManager(options) {
16
- const wm = chunkTJ6QOOJ2_cjs.createWindowManager(options);
16
+ const wm = chunkGLBVT4AQ_cjs.createWindowManager(options);
17
17
  onDestroy(() => wm.destroy());
18
18
  return wm;
19
19
  }
@@ -28,7 +28,7 @@ function useWmWindow(wm, id) {
28
28
  return core.computed(() => state().windows[id]);
29
29
  }
30
30
  function createDesktop(wm, options) {
31
- const binder = chunkTJ6QOOJ2_cjs.createDesktopBinder(wm, options);
31
+ const binder = chunkGLBVT4AQ_cjs.createDesktopBinder(wm, options);
32
32
  onDestroy(() => binder.destroy());
33
33
  return {
34
34
  binder,
package/dist/angular.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createWindowManager, createDesktopBinder } from './chunk-OOEDFA7K.js';
1
+ import { createWindowManager, createDesktopBinder } from './chunk-GGDF2EWV.js';
2
2
  import { signal, computed, inject, DestroyRef } from '@angular/core';
3
3
 
4
4
  function onDestroy(dispose) {
@@ -209,6 +209,7 @@ function createWindowManager(options = {}) {
209
209
  let future = [];
210
210
  let interactionDepth = 0;
211
211
  let interactionRecorded = false;
212
+ let interactionMark = null;
212
213
  let skipNextHistory = false;
213
214
  let pendingHistory = null;
214
215
  const layouts = /* @__PURE__ */ new Map();
@@ -319,7 +320,13 @@ function createWindowManager(options = {}) {
319
320
  width: init.width ?? defaultSize.width,
320
321
  height: init.height ?? defaultSize.height
321
322
  };
322
- const size = aspectRatio === null ? clampSize(requested, minSize, maxSize) : applyAspect(requested, aspectRatio, minSize, maxSize, "width");
323
+ const size = aspectRatio === null ? clampSize(requested, minSize, maxSize) : applyAspect(
324
+ requested,
325
+ aspectRatio,
326
+ minSize,
327
+ maxSize,
328
+ init.width === void 0 && init.height !== void 0 ? "height" : "width"
329
+ );
323
330
  const position = positionForOpen(init);
324
331
  let bounds = { ...position, ...size };
325
332
  if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
@@ -435,7 +442,13 @@ function createWindowManager(options = {}) {
435
442
  }
436
443
  function snapBounds(win, zone) {
437
444
  const rect = zoneBounds(zone, viewport);
438
- return { x: rect.x, y: rect.y, ...normalizeSize(rect, win) };
445
+ const byWidth = normalizeSize(rect, win);
446
+ const size = byWidth.height > rect.height ? normalizeSize(rect, win, "height") : byWidth;
447
+ return {
448
+ x: Math.max(0, Math.min(rect.x, viewport.width - size.width)),
449
+ y: Math.max(0, Math.min(rect.y, viewport.height - size.height)),
450
+ ...size
451
+ };
439
452
  }
440
453
  function focusUnlessBlocked(id) {
441
454
  const modal = topModalId();
@@ -600,7 +613,7 @@ function createWindowManager(options = {}) {
600
613
  snappable: patch.snappable ?? win.snappable,
601
614
  meta: patch.meta ? { ...win.meta, ...patch.meta } : win.meta
602
615
  };
603
- const size = normalizeSize(next.bounds, next);
616
+ const size = next.stage === "maximized" ? next.bounds : normalizeSize(next.bounds, next);
604
617
  const resized = size.width !== next.bounds.width || size.height !== next.bounds.height;
605
618
  const finalWin = resized ? { ...next, bounds: { ...next.bounds, ...size } } : next;
606
619
  setWindow(finalWin);
@@ -648,6 +661,10 @@ function createWindowManager(options = {}) {
648
661
  if (!onActiveWorkspace(updated) && focusedId === id) {
649
662
  focusTop();
650
663
  if (focusedId) emitFocus(windows[focusedId], id);
664
+ } else if (onActiveWorkspace(updated) && topModalId() === id && focusedId !== id) {
665
+ const previous = focusedId;
666
+ focusedId = id;
667
+ emitFocus(updated, previous);
651
668
  }
652
669
  queueEvent(() => emitter.emit("update", { window: updated }));
653
670
  commit();
@@ -775,14 +792,20 @@ function createWindowManager(options = {}) {
775
792
  }
776
793
  function beginInteraction() {
777
794
  interactionDepth += 1;
778
- if (interactionDepth === 1) interactionRecorded = false;
795
+ if (interactionDepth === 1) {
796
+ interactionRecorded = false;
797
+ interactionMark = { past: [...past], future };
798
+ }
779
799
  }
780
800
  function endInteraction() {
781
801
  if (interactionDepth > 0) interactionDepth -= 1;
802
+ if (interactionDepth === 0) interactionMark = null;
782
803
  }
783
804
  function abortInteraction() {
784
- if (interactionDepth > 0 && interactionRecorded) {
785
- past.pop();
805
+ if (interactionDepth > 0 && interactionMark) {
806
+ past.length = 0;
807
+ past.push(...interactionMark.past);
808
+ future = interactionMark.future;
786
809
  interactionRecorded = false;
787
810
  }
788
811
  endInteraction();
@@ -1130,10 +1153,8 @@ function createAnnouncer(wm, container, messages = {}) {
1130
1153
  element.style.cssText = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0";
1131
1154
  container.append(element);
1132
1155
  let clearTimer;
1133
- let lastMessage = "";
1134
1156
  let detailed = false;
1135
1157
  function announce(message) {
1136
- lastMessage = message;
1137
1158
  element.textContent = message;
1138
1159
  if (clearTimer !== void 0) clearTimeout(clearTimer);
1139
1160
  clearTimer = setTimeout(() => {
@@ -1141,8 +1162,14 @@ function createAnnouncer(wm, container, messages = {}) {
1141
1162
  }, 2e3);
1142
1163
  }
1143
1164
  const unsubscribers = [
1144
- wm.on("open", ({ window: win }) => announce(dict.opened(win.title))),
1145
- wm.on("close", ({ window: win }) => announce(dict.closed(win.title))),
1165
+ wm.on("open", ({ window: win }) => {
1166
+ announce(dict.opened(win.title));
1167
+ detailed = true;
1168
+ }),
1169
+ wm.on("close", ({ window: win }) => {
1170
+ announce(dict.closed(win.title));
1171
+ detailed = true;
1172
+ }),
1146
1173
  wm.on("stage", ({ window: win, previous }) => {
1147
1174
  if (win.stage === "minimized") announce(dict.minimized(win.title));
1148
1175
  else if (win.stage === "maximized") announce(dict.maximized(win.title));
@@ -1153,7 +1180,7 @@ function createAnnouncer(wm, container, messages = {}) {
1153
1180
  detailed = true;
1154
1181
  }),
1155
1182
  wm.on("focus", ({ window: win, previous }) => {
1156
- if (detailed || previous === null || lastMessage === dict.opened(win.title)) return;
1183
+ if (detailed || previous === null) return;
1157
1184
  announce(dict.focused(win.title));
1158
1185
  }),
1159
1186
  wm.on("workspace", ({ workspace }) => {
@@ -1400,6 +1427,7 @@ function createResizeStarter(ctx) {
1400
1427
  let pendingX = 0;
1401
1428
  let pendingY = 0;
1402
1429
  let hasPending = false;
1430
+ let applied = false;
1403
1431
  const el = ctx.windowElement(id);
1404
1432
  if (el) el.dataset.wmResizing = direction;
1405
1433
  function flush() {
@@ -1408,18 +1436,24 @@ function createResizeStarter(ctx) {
1408
1436
  raf = 0;
1409
1437
  const dx = pendingX - startPoint.x;
1410
1438
  const dy = pendingY - startPoint.y;
1411
- const top = direction.includes("n") ? Math.max(0, start.y + dy) : start.y;
1439
+ const floor = Math.min(0, start.y);
1440
+ const top = direction.includes("n") ? Math.max(floor, start.y + dy) : start.y;
1412
1441
  const raw = {
1413
1442
  width: direction.includes("e") ? start.width + dx : direction.includes("w") ? start.width - dx : start.width,
1414
1443
  height: direction.includes("s") ? start.height + dy : direction.includes("n") ? start.y + start.height - top : start.height
1415
1444
  };
1416
- const size = aspect === null ? clampSize(raw, minSize, maxSize) : applyAspect(raw, aspect, minSize, maxSize, drive);
1445
+ let size = aspect === null ? clampSize(raw, minSize, maxSize) : applyAspect(raw, aspect, minSize, maxSize, drive);
1446
+ const available = start.y + start.height - floor;
1447
+ if (direction.includes("n") && size.height > available) {
1448
+ size = applyAspect({ ...size, height: available }, aspect ?? 0, minSize, maxSize, "height");
1449
+ }
1417
1450
  const next = {
1418
1451
  x: direction.includes("w") ? start.x + start.width - size.width : start.x,
1419
1452
  y: direction.includes("n") ? start.y + start.height - size.height : start.y,
1420
1453
  ...size
1421
1454
  };
1422
1455
  wm.resize(id, next);
1456
+ applied = true;
1423
1457
  }
1424
1458
  function onMove(moveEvent) {
1425
1459
  if (moveEvent.pointerId !== event.pointerId) return;
@@ -1434,7 +1468,7 @@ function createResizeStarter(ctx) {
1434
1468
  try {
1435
1469
  if (raf !== 0) view.cancelAnimationFrame(raf);
1436
1470
  if (hasPending && !cancelled) flush();
1437
- if (cancelled) {
1471
+ if (cancelled && applied) {
1438
1472
  if (startStage === "snapped" && startZone) {
1439
1473
  wm.restoreTo(id, startRestore ?? start);
1440
1474
  wm.snap(id, startZone);
@@ -1636,6 +1670,11 @@ function attachDesktop(wm, element, options = {}) {
1636
1670
  if (hide && !el.hidden && el.contains(doc.activeElement)) {
1637
1671
  element.focus({ preventScroll: true });
1638
1672
  }
1673
+ if (!hide && el.hidden && wm.getState().focusedId === win.id) {
1674
+ queueMicrotask(() => {
1675
+ if (!el.hidden && !el.contains(doc.activeElement)) el.focus({ preventScroll: true });
1676
+ });
1677
+ }
1639
1678
  el.hidden = hide;
1640
1679
  el.setAttribute("aria-label", win.title);
1641
1680
  if (win.layer === "modal") el.setAttribute("aria-modal", "true");
@@ -1684,6 +1723,7 @@ function attachDesktop(wm, element, options = {}) {
1684
1723
  wm.on("focus", ({ window: win }) => {
1685
1724
  const attached = registry.get(win.id);
1686
1725
  if (!attached) return;
1726
+ if (attached.element.hidden) syncAll();
1687
1727
  if (!attached.element.contains(doc.activeElement)) {
1688
1728
  attached.element.focus({ preventScroll: true });
1689
1729
  }
@@ -1719,6 +1759,7 @@ function attachDesktop(wm, element, options = {}) {
1719
1759
  return;
1720
1760
  }
1721
1761
  if (!event.ctrlKey && !event.metaKey) return;
1762
+ if (drag) return;
1722
1763
  const target = event.target;
1723
1764
  if (target?.closest(INTERACTIVE_SELECTOR)) return;
1724
1765
  if (historyShortcuts && (event.key === "z" || event.key === "Z")) {
@@ -1987,5 +2028,5 @@ function createDesktopBinder(wm, options = {}) {
1987
2028
  }
1988
2029
 
1989
2030
  export { applyAspect, attachDesktop, boundsEqual, clamp, clampSize, clampToViewport, createAnnouncer, createDesktopBinder, createEmitter, createWindowManager, defaultMessages, detectSnapZone, flipFromTarget, flipToTarget, magnetize, prefersReducedMotion, zoneBounds };
1990
- //# sourceMappingURL=chunk-OOEDFA7K.js.map
1991
- //# sourceMappingURL=chunk-OOEDFA7K.js.map
2031
+ //# sourceMappingURL=chunk-GGDF2EWV.js.map
2032
+ //# sourceMappingURL=chunk-GGDF2EWV.js.map