@surdeddd/wmkit 0.4.2 → 0.5.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 (41) hide show
  1. package/README.md +1 -0
  2. package/README.ru.md +1 -0
  3. package/dist/angular.cjs +3 -3
  4. package/dist/angular.d.cts +2 -2
  5. package/dist/angular.d.ts +2 -2
  6. package/dist/angular.js +1 -1
  7. package/dist/{binder-B2A2IZGh.d.ts → binder-Ca3GOfZX.d.ts} +5 -1
  8. package/dist/{binder-Dl07SXIM.d.cts → binder-EBroZTRw.d.cts} +5 -1
  9. package/dist/{chunk-GGDF2EWV.js → chunk-L43XBN44.js} +235 -14
  10. package/dist/chunk-L43XBN44.js.map +1 -0
  11. package/dist/{chunk-GLBVT4AQ.cjs → chunk-RXAMUBUJ.cjs} +235 -14
  12. package/dist/chunk-RXAMUBUJ.cjs.map +1 -0
  13. package/dist/index.cjs +18 -18
  14. package/dist/index.d.cts +4 -4
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +1 -1
  17. package/dist/persist.d.cts +1 -1
  18. package/dist/persist.d.ts +1 -1
  19. package/dist/popout.d.cts +1 -1
  20. package/dist/popout.d.ts +1 -1
  21. package/dist/react.cjs +3 -3
  22. package/dist/react.d.cts +2 -2
  23. package/dist/react.d.ts +2 -2
  24. package/dist/react.js +1 -1
  25. package/dist/solid.cjs +3 -3
  26. package/dist/solid.d.cts +2 -2
  27. package/dist/solid.d.ts +2 -2
  28. package/dist/solid.js +1 -1
  29. package/dist/svelte.cjs +3 -3
  30. package/dist/svelte.d.cts +2 -2
  31. package/dist/svelte.d.ts +2 -2
  32. package/dist/svelte.js +1 -1
  33. package/dist/{types-t6pyVYKN.d.cts → types-LP7Inhxx.d.cts} +18 -0
  34. package/dist/{types-t6pyVYKN.d.ts → types-LP7Inhxx.d.ts} +18 -0
  35. package/dist/vue.cjs +3 -3
  36. package/dist/vue.d.cts +2 -2
  37. package/dist/vue.d.ts +2 -2
  38. package/dist/vue.js +1 -1
  39. package/package.json +7 -7
  40. package/dist/chunk-GGDF2EWV.js.map +0 -1
  41. package/dist/chunk-GLBVT4AQ.cjs.map +0 -1
package/README.md CHANGED
@@ -15,6 +15,7 @@
15
15
  - 🪟 **Full window lifecycle** — open, close, focus, minimize, maximize, restore, drag, 8-direction resize
16
16
  - 🧠 **Headless core** — a serializable state machine plus a DOM controller; bring your own markup or use the glass theme
17
17
  - ⚛️ **Official adapters** — `@surdeddd/wmkit/react`, `/vue`, `/svelte`, `/solid`, `/angular`, all thin sugar over one core
18
+ - 🗃️ **Tab groups** — drop one titlebar onto another and the windows share a frame, dockview style
18
19
  - ⊞ **Snap zones** — halves, quarters, thirds and drag-to-top maximize with a live preview
19
20
  - 🧲 **Magnetism** — window edges align to neighbours and the viewport while dragging
20
21
  - ↩️ **Undo/redo** — every mutation is one step; a whole drag collapses into a single history entry
package/README.ru.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - 🪟 **Полный жизненный цикл окна** — открытие, закрытие, фокус, сворачивание, разворачивание, восстановление, drag, ресайз в 8 направлениях
12
12
  - 🧠 **Headless-ядро** — сериализуемая стейт-машина плюс DOM-контроллер; своя разметка или готовая стеклянная тема
13
13
  - ⚛️ **Родные адаптеры** — `@surdeddd/wmkit/react`, `/vue`, `/svelte`, `/solid`, `/angular`, тонкий сахар над одним ядром
14
+ - 🗃️ **Вкладочные группы** — бросьте заголовок на заголовок, и окна начнут делить одну рамку
14
15
  - ⊞ **Snap-зоны** — половины, четверти, трети и максимизация от верхнего края с живым превью
15
16
  - 🧲 **Магнетизм** — края окна прилипают к соседям и вьюпорту при перетаскивании
16
17
  - ↩️ **Undo/redo** — каждая мутация = один шаг; целый drag схлопывается в одну запись истории
package/dist/angular.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkGLBVT4AQ_cjs = require('./chunk-GLBVT4AQ.cjs');
3
+ var chunkRXAMUBUJ_cjs = require('./chunk-RXAMUBUJ.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 = chunkGLBVT4AQ_cjs.createWindowManager(options);
16
+ const wm = chunkRXAMUBUJ_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 = chunkGLBVT4AQ_cjs.createDesktopBinder(wm, options);
31
+ const binder = chunkRXAMUBUJ_cjs.createDesktopBinder(wm, options);
32
32
  onDestroy(() => binder.destroy());
33
33
  return {
34
34
  binder,
@@ -1,6 +1,6 @@
1
1
  import { Signal } from '@angular/core';
2
- import { W as WindowManager, M as ManagerOptions, a as ManagerState, b as WindowState } from './types-t6pyVYKN.cjs';
3
- import { D as DesktopBinder, W as WindowAttachOptions, a as DesktopOptions } from './binder-Dl07SXIM.cjs';
2
+ import { W as WindowManager, M as ManagerOptions, a as ManagerState, b as WindowState } from './types-LP7Inhxx.cjs';
3
+ import { D as DesktopBinder, W as WindowAttachOptions, a as DesktopOptions } from './binder-EBroZTRw.cjs';
4
4
 
5
5
  declare function useWindowManager(options?: ManagerOptions): WindowManager;
6
6
  declare function useWmState(wm: WindowManager): Signal<ManagerState>;
package/dist/angular.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Signal } from '@angular/core';
2
- import { W as WindowManager, M as ManagerOptions, a as ManagerState, b as WindowState } from './types-t6pyVYKN.js';
3
- import { D as DesktopBinder, W as WindowAttachOptions, a as DesktopOptions } from './binder-B2A2IZGh.js';
2
+ import { W as WindowManager, M as ManagerOptions, a as ManagerState, b as WindowState } from './types-LP7Inhxx.js';
3
+ import { D as DesktopBinder, W as WindowAttachOptions, a as DesktopOptions } from './binder-Ca3GOfZX.js';
4
4
 
5
5
  declare function useWindowManager(options?: ManagerOptions): WindowManager;
6
6
  declare function useWmState(wm: WindowManager): Signal<ManagerState>;
package/dist/angular.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createWindowManager, createDesktopBinder } from './chunk-GGDF2EWV.js';
1
+ import { createWindowManager, createDesktopBinder } from './chunk-L43XBN44.js';
2
2
  import { signal, computed, inject, DestroyRef } from '@angular/core';
3
3
 
4
4
  function onDestroy(dispose) {
@@ -1,4 +1,4 @@
1
- import { f as Size, B as Bounds, g as SnapZone, W as WindowManager, b as WindowState } from './types-t6pyVYKN.js';
1
+ import { f as Size, B as Bounds, g as SnapZone, W as WindowManager, b as WindowState } from './types-LP7Inhxx.js';
2
2
 
3
3
  declare function clamp(value: number, min: number, max: number): number;
4
4
  declare function clampSize(size: Size, min: Size, max: Size | null): Size;
@@ -54,7 +54,11 @@ interface HitAreaOptions {
54
54
  interface MagnetismOptions {
55
55
  threshold?: number;
56
56
  }
57
+ interface GroupingOptions {
58
+ dwell?: number;
59
+ }
57
60
  interface DesktopOptions {
61
+ grouping?: boolean | GroupingOptions;
58
62
  snap?: boolean | DesktopSnapOptions;
59
63
  keyboard?: boolean | DesktopKeyboardOptions;
60
64
  announce?: boolean | Partial<AnnouncerMessages>;
@@ -1,4 +1,4 @@
1
- import { f as Size, B as Bounds, g as SnapZone, W as WindowManager, b as WindowState } from './types-t6pyVYKN.cjs';
1
+ import { f as Size, B as Bounds, g as SnapZone, W as WindowManager, b as WindowState } from './types-LP7Inhxx.cjs';
2
2
 
3
3
  declare function clamp(value: number, min: number, max: number): number;
4
4
  declare function clampSize(size: Size, min: Size, max: Size | null): Size;
@@ -54,7 +54,11 @@ interface HitAreaOptions {
54
54
  interface MagnetismOptions {
55
55
  threshold?: number;
56
56
  }
57
+ interface GroupingOptions {
58
+ dwell?: number;
59
+ }
57
60
  interface DesktopOptions {
61
+ grouping?: boolean | GroupingOptions;
58
62
  snap?: boolean | DesktopSnapOptions;
59
63
  keyboard?: boolean | DesktopKeyboardOptions;
60
64
  announce?: boolean | Partial<AnnouncerMessages>;
@@ -197,6 +197,8 @@ function createWindowManager(options = {}) {
197
197
  let focusedId = null;
198
198
  let viewport = options.viewport ?? { width: 0, height: 0 };
199
199
  let workspace = normalizeWorkspace(options.workspace, 0);
200
+ let activeTabs = {};
201
+ let groupCounter = 0;
200
202
  let seq = 0;
201
203
  let idCounter = 0;
202
204
  let cascadeIndex = 0;
@@ -215,7 +217,7 @@ function createWindowManager(options = {}) {
215
217
  const layouts = /* @__PURE__ */ new Map();
216
218
  function getState() {
217
219
  if (!snapshot) {
218
- snapshot = { windows, order, focusedId, viewport, workspace };
220
+ snapshot = { windows, order, focusedId, viewport, workspace, groups: buildGroups() };
219
221
  }
220
222
  return snapshot;
221
223
  }
@@ -267,11 +269,54 @@ function createWindowManager(options = {}) {
267
269
  function onActiveWorkspace(win) {
268
270
  return win.workspace === workspace;
269
271
  }
272
+ function membersOf(groupId) {
273
+ return order.filter((entry) => windows[entry].groupId === groupId);
274
+ }
275
+ function isVisibleTab(win) {
276
+ return win.groupId === null || activeTabs[win.groupId] === win.id;
277
+ }
278
+ function buildGroups() {
279
+ const result = {};
280
+ for (const [groupId, activeId] of Object.entries(activeTabs)) {
281
+ result[groupId] = { id: groupId, activeId, members: membersOf(groupId) };
282
+ }
283
+ return result;
284
+ }
285
+ function syncGroup(source) {
286
+ if (source.groupId === null) return;
287
+ for (const id of membersOf(source.groupId)) {
288
+ if (id === source.id) continue;
289
+ const member = windows[id];
290
+ if (boundsEqual(member.bounds, source.bounds) && member.stage === source.stage && member.snapZone === source.snapZone && member.layer === source.layer && member.workspace === source.workspace) {
291
+ continue;
292
+ }
293
+ setWindow({
294
+ ...member,
295
+ bounds: source.bounds,
296
+ stage: source.stage,
297
+ snapZone: source.snapZone,
298
+ restoreBounds: source.restoreBounds,
299
+ restoreStage: source.restoreStage,
300
+ layer: source.layer,
301
+ workspace: source.workspace
302
+ });
303
+ }
304
+ }
305
+ function dissolveIfOrphaned(groupId) {
306
+ const members = membersOf(groupId);
307
+ if (members.length >= 2) return;
308
+ for (const id of members) {
309
+ setWindow({ ...windows[id], groupId: null });
310
+ }
311
+ const { [groupId]: _dropped, ...rest } = activeTabs;
312
+ activeTabs = rest;
313
+ }
270
314
  function topModalId() {
271
315
  for (let i = order.length - 1; i >= 0; i -= 1) {
272
316
  const win = windows[order[i]];
273
- if (win.layer === "modal" && win.stage !== "minimized" && onActiveWorkspace(win))
317
+ if (win.layer === "modal" && win.stage !== "minimized" && onActiveWorkspace(win) && isVisibleTab(win)) {
274
318
  return win.id;
319
+ }
275
320
  }
276
321
  return null;
277
322
  }
@@ -279,7 +324,7 @@ function createWindowManager(options = {}) {
279
324
  const modal = topModalId();
280
325
  return order.filter((id) => {
281
326
  const win = windows[id];
282
- if (win.stage === "minimized" || !onActiveWorkspace(win)) return false;
327
+ if (win.stage === "minimized" || !onActiveWorkspace(win) || !isVisibleTab(win)) return false;
283
328
  if (modal && win.layer !== "modal") return false;
284
329
  return true;
285
330
  });
@@ -342,6 +387,7 @@ function createWindowManager(options = {}) {
342
387
  snapZone: null,
343
388
  layer: init.layer ?? "normal",
344
389
  workspace: normalizeWorkspace(init.workspace, workspace),
390
+ groupId: null,
345
391
  minSize,
346
392
  maxSize,
347
393
  aspectRatio,
@@ -385,6 +431,13 @@ function createWindowManager(options = {}) {
385
431
  const win = windows[id];
386
432
  if (!win) return false;
387
433
  removeWindow(id);
434
+ if (win.groupId !== null) {
435
+ const remaining = membersOf(win.groupId);
436
+ if (activeTabs[win.groupId] === id && remaining[0]) {
437
+ activeTabs = { ...activeTabs, [win.groupId]: remaining[0] };
438
+ }
439
+ dissolveIfOrphaned(win.groupId);
440
+ }
388
441
  queueEvent(() => emitter.emit("close", { window: win }));
389
442
  if (focusedId === id) {
390
443
  focusTop();
@@ -401,6 +454,7 @@ function createWindowManager(options = {}) {
401
454
  const win = windows[id];
402
455
  if (!win) return false;
403
456
  if (!onActiveWorkspace(win)) setWorkspace(win.workspace);
457
+ if (!isVisibleTab(win)) activateTab(id);
404
458
  const modal = topModalId();
405
459
  if (modal && modal !== id && win.layer !== "modal") {
406
460
  const modalWin = windows[modal];
@@ -460,6 +514,7 @@ function createWindowManager(options = {}) {
460
514
  const next = build(win);
461
515
  if (!next) return false;
462
516
  setWindow(next);
517
+ syncGroup(next);
463
518
  queueEvent(() => emitter.emit("stage", { window: next, previous: win.stage }));
464
519
  commit();
465
520
  return true;
@@ -569,6 +624,7 @@ function createWindowManager(options = {}) {
569
624
  if (boundsEqual(bounds, win.bounds)) return true;
570
625
  const next = { ...win, bounds };
571
626
  setWindow(next);
627
+ syncGroup(next);
572
628
  queueEvent(() => emitter.emit("move", { window: next }));
573
629
  commit();
574
630
  return true;
@@ -590,6 +646,7 @@ function createWindowManager(options = {}) {
590
646
  if (boundsEqual(bounds, win.bounds) && !becameNormal) return true;
591
647
  const next = becameNormal ? { ...win, stage: "normal", snapZone: null, restoreBounds: null, bounds } : { ...win, bounds };
592
648
  setWindow(next);
649
+ syncGroup(next);
593
650
  if (becameNormal) queueEvent(() => emitter.emit("stage", { window: next, previous: "snapped" }));
594
651
  queueEvent(() => emitter.emit("resize", { window: next }));
595
652
  commit();
@@ -617,6 +674,7 @@ function createWindowManager(options = {}) {
617
674
  const resized = size.width !== next.bounds.width || size.height !== next.bounds.height;
618
675
  const finalWin = resized ? { ...next, bounds: { ...next.bounds, ...size } } : next;
619
676
  setWindow(finalWin);
677
+ syncGroup(finalWin);
620
678
  if (patch.layer && patch.layer !== win.layer) {
621
679
  order = sortByLayer(order);
622
680
  queueEvent(() => emitter.emit("order", { order }));
@@ -658,6 +716,7 @@ function createWindowManager(options = {}) {
658
716
  if (target === win.workspace) return false;
659
717
  const updated = { ...win, workspace: target };
660
718
  setWindow(updated);
719
+ syncGroup(updated);
661
720
  if (!onActiveWorkspace(updated) && focusedId === id) {
662
721
  focusTop();
663
722
  if (focusedId) emitFocus(windows[focusedId], id);
@@ -670,6 +729,77 @@ function createWindowManager(options = {}) {
670
729
  commit();
671
730
  return true;
672
731
  }
732
+ function emitGroup(groupId, previous) {
733
+ const members = membersOf(groupId);
734
+ const activeId = activeTabs[groupId];
735
+ queueEvent(() => emitter.emit("group", { groupId, members, activeId, previous }));
736
+ }
737
+ function group(ids) {
738
+ const seeds = ids.filter((id, index) => windows[id] && ids.indexOf(id) === index);
739
+ const host = seeds[0] ? windows[seeds[0]] : null;
740
+ if (!host) return null;
741
+ const expanded = [];
742
+ for (const id of seeds) {
743
+ const win = windows[id];
744
+ const siblings = win.groupId === null ? [id] : membersOf(win.groupId);
745
+ for (const entry of siblings) if (!expanded.includes(entry)) expanded.push(entry);
746
+ }
747
+ if (expanded.length < 2) return null;
748
+ const groupId = `${idPrefix}-group-${++groupCounter}`;
749
+ const retired = /* @__PURE__ */ new Set();
750
+ for (const id of expanded) {
751
+ const win = windows[id];
752
+ if (win.groupId !== null) retired.add(win.groupId);
753
+ setWindow({
754
+ ...win,
755
+ groupId,
756
+ bounds: host.bounds,
757
+ stage: host.stage,
758
+ snapZone: host.snapZone,
759
+ restoreBounds: host.restoreBounds,
760
+ restoreStage: host.restoreStage,
761
+ layer: host.layer,
762
+ workspace: host.workspace
763
+ });
764
+ }
765
+ const next = { ...activeTabs, [groupId]: host.id };
766
+ for (const old of retired) delete next[old];
767
+ activeTabs = next;
768
+ raise(host.id);
769
+ emitGroup(groupId, null);
770
+ queueEvent(() => emitter.emit("order", { order }));
771
+ commit();
772
+ if (onActiveWorkspace(host) && host.stage !== "minimized") focus(host.id);
773
+ return groupId;
774
+ }
775
+ function ungroup(id) {
776
+ const win = windows[id];
777
+ if (!win?.groupId) return false;
778
+ const groupId = win.groupId;
779
+ const wasActive = activeTabs[groupId] === id;
780
+ setWindow({ ...win, groupId: null });
781
+ const remaining = membersOf(groupId);
782
+ if (wasActive) activeTabs = { ...activeTabs, [groupId]: remaining[0] };
783
+ dissolveIfOrphaned(groupId);
784
+ if (membersOf(groupId).length >= 2) emitGroup(groupId, wasActive ? id : null);
785
+ commit();
786
+ focus(id);
787
+ return true;
788
+ }
789
+ function activateTab(id) {
790
+ const win = windows[id];
791
+ if (!win?.groupId) return false;
792
+ const groupId = win.groupId;
793
+ const previous = activeTabs[groupId];
794
+ if (previous === id) return false;
795
+ activeTabs = { ...activeTabs, [groupId]: id };
796
+ emitGroup(groupId, previous);
797
+ commit();
798
+ return true;
799
+ }
800
+ function groupMembers(groupId) {
801
+ return membersOf(groupId).map((id) => windows[id]);
802
+ }
673
803
  function sendToBack(id) {
674
804
  const win = windows[id];
675
805
  if (!win) return false;
@@ -701,10 +831,10 @@ function createWindowManager(options = {}) {
701
831
  );
702
832
  }
703
833
  function minimized() {
704
- return Object.values(windows).filter((win) => win.stage === "minimized" && onActiveWorkspace(win)).sort((a, b) => a.openedSeq - b.openedSeq);
834
+ return Object.values(windows).filter((win) => win.stage === "minimized" && onActiveWorkspace(win) && isVisibleTab(win)).sort((a, b) => a.openedSeq - b.openedSeq);
705
835
  }
706
836
  function captureEntry() {
707
- return { windows, order, focusedId, workspace };
837
+ return { windows, order, focusedId, workspace, activeTabs };
708
838
  }
709
839
  function recordHistory() {
710
840
  if (!historyDirty) return;
@@ -766,6 +896,7 @@ function createWindowManager(options = {}) {
766
896
  windows = entry.windows;
767
897
  order = [...entry.order];
768
898
  focusedId = entry.focusedId;
899
+ activeTabs = entry.activeTabs;
769
900
  const previousWorkspace = workspace;
770
901
  workspace = entry.workspace;
771
902
  if (workspace !== previousWorkspace) {
@@ -896,7 +1027,8 @@ function createWindowManager(options = {}) {
896
1027
  })),
897
1028
  order: [...order],
898
1029
  focusedId,
899
- workspace
1030
+ workspace,
1031
+ activeTabs: { ...activeTabs }
900
1032
  };
901
1033
  }
902
1034
  function isBounds(value) {
@@ -951,6 +1083,7 @@ function createWindowManager(options = {}) {
951
1083
  snapZone: ZONES.includes(raw.snapZone) ? raw.snapZone : null,
952
1084
  layer: raw.layer,
953
1085
  workspace: normalizeWorkspace(raw.workspace, 0),
1086
+ groupId: typeof raw.groupId === "string" && raw.groupId.length > 0 ? raw.groupId : null,
954
1087
  minSize: isSize(raw.minSize) ? { ...raw.minSize } : { width: 160, height: 100 },
955
1088
  maxSize: readMaxSize(raw.maxSize),
956
1089
  aspectRatio: typeof raw.aspectRatio === "number" && Number.isFinite(raw.aspectRatio) && raw.aspectRatio > 0 ? raw.aspectRatio : null,
@@ -992,7 +1125,18 @@ function createWindowManager(options = {}) {
992
1125
  if (focusedCandidate && (focusedCandidate.stage === "minimized" || !onActiveWorkspace(focusedCandidate))) {
993
1126
  focusedId = null;
994
1127
  }
995
- if (!focusedId) focusTop();
1128
+ const restoredTabs = {};
1129
+ const rawTabs = data.activeTabs;
1130
+ for (const id of order) {
1131
+ const groupId = windows[id].groupId;
1132
+ if (groupId === null || restoredTabs[groupId]) continue;
1133
+ const wanted = typeof rawTabs === "object" && rawTabs !== null ? rawTabs[groupId] : void 0;
1134
+ const members = membersOf(groupId);
1135
+ restoredTabs[groupId] = typeof wanted === "string" && members.includes(wanted) ? wanted : members[0];
1136
+ }
1137
+ activeTabs = restoredTabs;
1138
+ for (const groupId of Object.keys(restoredTabs)) dissolveIfOrphaned(groupId);
1139
+ if (!focusedId || !isVisibleTab(windows[focusedId])) focusTop();
996
1140
  const modal = topModalId();
997
1141
  if (modal) {
998
1142
  const focusedWin = focusedId ? windows[focusedId] : null;
@@ -1046,6 +1190,10 @@ function createWindowManager(options = {}) {
1046
1190
  workspace: () => workspace,
1047
1191
  setWorkspace: (next) => transact(() => setWorkspace(next)),
1048
1192
  moveToWorkspace: (id, next) => transact(() => moveToWorkspace(id, next)),
1193
+ group: (ids) => transact(() => group(ids)),
1194
+ ungroup: (id) => transact(() => ungroup(id)),
1195
+ activateTab: (id) => transact(() => activateTab(id)),
1196
+ groupMembers,
1049
1197
  batch: (run) => transact(run),
1050
1198
  serialize,
1051
1199
  hydrate: (data) => transact(() => hydrate(data)),
@@ -1235,6 +1383,11 @@ function createDragStarter(ctx) {
1235
1383
  restored: win.stage === "normal",
1236
1384
  moved: false,
1237
1385
  zone: null,
1386
+ groupTarget: null,
1387
+ hoverId: null,
1388
+ hoverTimer: 0,
1389
+ clientX: event.clientX,
1390
+ clientY: event.clientY,
1238
1391
  raf: 0,
1239
1392
  pendingX: 0,
1240
1393
  pendingY: 0,
@@ -1243,6 +1396,18 @@ function createDragStarter(ctx) {
1243
1396
  }
1244
1397
  };
1245
1398
  const el = ctx.windowElement(id);
1399
+ function armGroup(target2) {
1400
+ session.groupTarget = target2;
1401
+ ctx.markGroupTarget(target2);
1402
+ }
1403
+ function clearHover() {
1404
+ if (session.hoverTimer !== 0) {
1405
+ view.clearTimeout(session.hoverTimer);
1406
+ session.hoverTimer = 0;
1407
+ }
1408
+ session.groupTarget = null;
1409
+ ctx.markGroupTarget(null);
1410
+ }
1246
1411
  function flush() {
1247
1412
  if (!session.hasPending || ctx.currentDrag() !== session) return;
1248
1413
  session.hasPending = false;
@@ -1290,6 +1455,20 @@ function createDragStarter(ctx) {
1290
1455
  nextY = magnet.y;
1291
1456
  }
1292
1457
  wm.move(id, nextX, nextY);
1458
+ const candidate = ctx.groupTarget(session.clientX, session.clientY, id);
1459
+ if (candidate !== session.hoverId) {
1460
+ session.hoverId = candidate;
1461
+ clearHover();
1462
+ if (candidate) {
1463
+ if (ctx.groupDwell <= 0) armGroup(candidate);
1464
+ else session.hoverTimer = view.setTimeout(() => armGroup(candidate), ctx.groupDwell);
1465
+ }
1466
+ }
1467
+ if (session.groupTarget) {
1468
+ session.zone = null;
1469
+ ctx.hidePreview();
1470
+ return;
1471
+ }
1293
1472
  if (ctx.snapEnabled && current.snappable) {
1294
1473
  const viewport = wm.getState().viewport;
1295
1474
  const rawZone = detectSnapZone(session.pendingX, session.pendingY, viewport, ctx.snapDetect);
@@ -1322,6 +1501,8 @@ function createDragStarter(ctx) {
1322
1501
  }
1323
1502
  session.pendingX = movePoint.x;
1324
1503
  session.pendingY = movePoint.y;
1504
+ session.clientX = moveEvent.clientX;
1505
+ session.clientY = moveEvent.clientY;
1325
1506
  session.hasPending = true;
1326
1507
  if (session.raf === 0) session.raf = view.requestAnimationFrame(flush);
1327
1508
  }
@@ -1356,6 +1537,8 @@ function createDragStarter(ctx) {
1356
1537
  wm.move(id, session.startBounds.x, session.startBounds.y);
1357
1538
  }
1358
1539
  }
1540
+ } else if (session.moved && session.groupTarget) {
1541
+ if (wm.group([session.groupTarget, id])) wm.activateTab(id);
1359
1542
  } else if (session.moved && session.zone) {
1360
1543
  if (session.zone === "maximize") wm.maximize(id);
1361
1544
  else wm.snap(id, session.zone);
@@ -1375,6 +1558,8 @@ function createDragStarter(ctx) {
1375
1558
  el.style.willChange = "";
1376
1559
  }
1377
1560
  ctx.hidePreview();
1561
+ if (session.hoverTimer !== 0) view.clearTimeout(session.hoverTimer);
1562
+ ctx.markGroupTarget(null);
1378
1563
  if (cancelled) wm.abortInteraction();
1379
1564
  else wm.endInteraction();
1380
1565
  }
@@ -1544,6 +1729,8 @@ function attachDesktop(wm, element, options = {}) {
1544
1729
  const doc = element.ownerDocument;
1545
1730
  const view = windowOf(element);
1546
1731
  const coarsePointer = typeof view.matchMedia === "function" && view.matchMedia("(pointer: coarse)").matches;
1732
+ const groupingEnabled = options.grouping !== false;
1733
+ const groupDwell = (typeof options.grouping === "object" ? options.grouping.dwell : void 0) ?? 420;
1547
1734
  const snapEnabled = options.snap !== false;
1548
1735
  const snapOptions = typeof options.snap === "object" ? options.snap : {};
1549
1736
  const snapPreviewEnabled = snapOptions.preview !== false;
@@ -1567,6 +1754,7 @@ function attachDesktop(wm, element, options = {}) {
1567
1754
  let lastOrder = null;
1568
1755
  let lastFocused = null;
1569
1756
  let drag = null;
1757
+ let groupTargetId = null;
1570
1758
  let cachedRect = null;
1571
1759
  let rectUsers = 0;
1572
1760
  let announcer = null;
@@ -1638,6 +1826,29 @@ function attachDesktop(wm, element, options = {}) {
1638
1826
  hitEdge,
1639
1827
  hitCorner,
1640
1828
  magnetThreshold,
1829
+ groupDwell,
1830
+ groupTarget(clientX, clientY, selfId) {
1831
+ if (!groupingEnabled || typeof doc.elementsFromPoint !== "function") return null;
1832
+ for (const node of doc.elementsFromPoint(clientX, clientY)) {
1833
+ const handle = node.closest?.("[data-wm-drag]");
1834
+ const host = handle?.closest("[data-wm-window]");
1835
+ const id = host?.dataset.wmWindow;
1836
+ if (id && id !== selfId && registry.has(id)) return id;
1837
+ }
1838
+ return null;
1839
+ },
1840
+ markGroupTarget(id) {
1841
+ if (groupTargetId === id) return;
1842
+ if (groupTargetId) {
1843
+ const previous = registry.get(groupTargetId);
1844
+ if (previous) delete previous.element.dataset.wmTabTarget;
1845
+ }
1846
+ groupTargetId = id;
1847
+ if (id) {
1848
+ const next = registry.get(id);
1849
+ if (next) next.element.dataset.wmTabTarget = "";
1850
+ }
1851
+ },
1641
1852
  currentDrag: () => drag,
1642
1853
  claimDrag(session) {
1643
1854
  drag = session;
@@ -1655,18 +1866,25 @@ function attachDesktop(wm, element, options = {}) {
1655
1866
  observer.observe(element);
1656
1867
  cleanup.push(() => observer.disconnect());
1657
1868
  }
1658
- function syncWindow(attached, win, zIndex, activeWorkspace) {
1869
+ function syncWindow(attached, win, zIndex, activeWorkspace, activeTab) {
1659
1870
  const el = attached.element;
1660
1871
  const firstSync = attached.lastState === null;
1661
1872
  if (firstSync) el.style.transition = "none";
1662
- if (attached.lastState !== win || attached.lastWorkspace !== activeWorkspace) {
1873
+ if (attached.lastState !== win || attached.lastWorkspace !== activeWorkspace || attached.lastTab !== activeTab) {
1663
1874
  el.style.transform = `translate3d(${win.bounds.x}px, ${win.bounds.y}px, 0)`;
1664
1875
  el.style.width = `${win.bounds.width}px`;
1665
1876
  el.style.height = `${win.bounds.height}px`;
1666
1877
  el.dataset.wmStage = win.stage;
1667
1878
  el.dataset.wmLayer = win.layer;
1668
1879
  el.dataset.wmWorkspace = String(win.workspace);
1669
- const hide = win.stage === "minimized" || win.workspace !== activeWorkspace;
1880
+ if (win.groupId === null) {
1881
+ delete el.dataset.wmGroup;
1882
+ delete el.dataset.wmTab;
1883
+ } else {
1884
+ el.dataset.wmGroup = win.groupId;
1885
+ el.dataset.wmTab = activeTab ? "active" : "inactive";
1886
+ }
1887
+ const hide = win.stage === "minimized" || win.workspace !== activeWorkspace || !activeTab;
1670
1888
  if (hide && !el.hidden && el.contains(doc.activeElement)) {
1671
1889
  element.focus({ preventScroll: true });
1672
1890
  }
@@ -1684,6 +1902,7 @@ function attachDesktop(wm, element, options = {}) {
1684
1902
  }
1685
1903
  attached.lastState = win;
1686
1904
  attached.lastWorkspace = activeWorkspace;
1905
+ attached.lastTab = activeTab;
1687
1906
  }
1688
1907
  if (attached.lastZ !== zIndex) {
1689
1908
  el.style.zIndex = String(zIndex + 1);
@@ -1701,8 +1920,9 @@ function attachDesktop(wm, element, options = {}) {
1701
1920
  const attached = registry.get(id);
1702
1921
  const win = state.windows[id];
1703
1922
  if (!attached || !win) return;
1704
- if (orderChanged || attached.lastState !== win || attached.lastWorkspace !== state.workspace) {
1705
- syncWindow(attached, win, index, state.workspace);
1923
+ const activeTab = win.groupId === null || state.groups[win.groupId]?.activeId === win.id;
1924
+ if (orderChanged || attached.lastState !== win || attached.lastWorkspace !== state.workspace || attached.lastTab !== activeTab) {
1925
+ syncWindow(attached, win, index, state.workspace, activeTab);
1706
1926
  }
1707
1927
  });
1708
1928
  if (state.focusedId !== lastFocused) {
@@ -1805,6 +2025,7 @@ function attachDesktop(wm, element, options = {}) {
1805
2025
  lastState: null,
1806
2026
  lastZ: -1,
1807
2027
  lastWorkspace: -1,
2028
+ lastTab: true,
1808
2029
  cleanup: []
1809
2030
  };
1810
2031
  windowElement.dataset.wmWindow = id;
@@ -2028,5 +2249,5 @@ function createDesktopBinder(wm, options = {}) {
2028
2249
  }
2029
2250
 
2030
2251
  export { applyAspect, attachDesktop, boundsEqual, clamp, clampSize, clampToViewport, createAnnouncer, createDesktopBinder, createEmitter, createWindowManager, defaultMessages, detectSnapZone, flipFromTarget, flipToTarget, magnetize, prefersReducedMotion, zoneBounds };
2031
- //# sourceMappingURL=chunk-GGDF2EWV.js.map
2032
- //# sourceMappingURL=chunk-GGDF2EWV.js.map
2252
+ //# sourceMappingURL=chunk-L43XBN44.js.map
2253
+ //# sourceMappingURL=chunk-L43XBN44.js.map