@surdeddd/wmkit 0.1.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/README.ru.md +120 -0
  4. package/dist/chunk-7HHDQEBI.js +1334 -0
  5. package/dist/chunk-7HHDQEBI.js.map +1 -0
  6. package/dist/chunk-KWLI7JIY.cjs +1349 -0
  7. package/dist/chunk-KWLI7JIY.cjs.map +1 -0
  8. package/dist/controller-B0bogK9N.d.cts +65 -0
  9. package/dist/controller-D5zIriLg.d.ts +65 -0
  10. package/dist/index.cjs +64 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +22 -0
  13. package/dist/index.d.ts +22 -0
  14. package/dist/index.js +3 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/persist.cjs +74 -0
  17. package/dist/persist.cjs.map +1 -0
  18. package/dist/persist.d.cts +22 -0
  19. package/dist/persist.d.ts +22 -0
  20. package/dist/persist.js +72 -0
  21. package/dist/persist.js.map +1 -0
  22. package/dist/popout.cjs +66 -0
  23. package/dist/popout.cjs.map +1 -0
  24. package/dist/popout.d.cts +28 -0
  25. package/dist/popout.d.ts +28 -0
  26. package/dist/popout.js +63 -0
  27. package/dist/popout.js.map +1 -0
  28. package/dist/react.cjs +57 -0
  29. package/dist/react.cjs.map +1 -0
  30. package/dist/react.d.cts +16 -0
  31. package/dist/react.d.ts +16 -0
  32. package/dist/react.js +51 -0
  33. package/dist/react.js.map +1 -0
  34. package/dist/solid.cjs +45 -0
  35. package/dist/solid.cjs.map +1 -0
  36. package/dist/solid.d.cts +15 -0
  37. package/dist/solid.d.ts +15 -0
  38. package/dist/solid.js +40 -0
  39. package/dist/solid.js.map +1 -0
  40. package/dist/svelte.cjs +60 -0
  41. package/dist/svelte.cjs.map +1 -0
  42. package/dist/svelte.d.cts +24 -0
  43. package/dist/svelte.d.ts +24 -0
  44. package/dist/svelte.js +55 -0
  45. package/dist/svelte.js.map +1 -0
  46. package/dist/themes/glass.css +203 -0
  47. package/dist/types-CtFzL_oA.d.cts +151 -0
  48. package/dist/types-CtFzL_oA.d.ts +151 -0
  49. package/dist/vue.cjs +54 -0
  50. package/dist/vue.cjs.map +1 -0
  51. package/dist/vue.d.cts +11 -0
  52. package/dist/vue.d.ts +11 -0
  53. package/dist/vue.js +48 -0
  54. package/dist/vue.js.map +1 -0
  55. package/package.json +257 -0
@@ -0,0 +1,1349 @@
1
+ 'use strict';
2
+
3
+ // src/core/emitter.ts
4
+ function createEmitter() {
5
+ const listeners = /* @__PURE__ */ new Map();
6
+ return {
7
+ on(event, listener) {
8
+ let set = listeners.get(event);
9
+ if (!set) {
10
+ set = /* @__PURE__ */ new Set();
11
+ listeners.set(event, set);
12
+ }
13
+ set.add(listener);
14
+ return () => {
15
+ set.delete(listener);
16
+ };
17
+ },
18
+ emit(event, payload) {
19
+ const set = listeners.get(event);
20
+ if (!set) return;
21
+ for (const listener of [...set]) {
22
+ listener(payload);
23
+ }
24
+ },
25
+ clear() {
26
+ listeners.clear();
27
+ }
28
+ };
29
+ }
30
+
31
+ // src/core/geometry.ts
32
+ function clamp(value, min, max) {
33
+ return Math.min(Math.max(value, min), max);
34
+ }
35
+ function clampSize(size, min, max) {
36
+ return {
37
+ width: clamp(size.width, min.width, max ? max.width : Number.POSITIVE_INFINITY),
38
+ height: clamp(size.height, min.height, max ? max.height : Number.POSITIVE_INFINITY)
39
+ };
40
+ }
41
+ function clampToViewport(bounds, viewport, minVisible) {
42
+ if (viewport.width <= 0 || viewport.height <= 0) return bounds;
43
+ const x = clamp(bounds.x, minVisible - bounds.width, viewport.width - minVisible);
44
+ const y = clamp(bounds.y, 0, Math.max(0, viewport.height - minVisible));
45
+ return x === bounds.x && y === bounds.y ? bounds : { ...bounds, x, y };
46
+ }
47
+ function boundsEqual(a, b) {
48
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
49
+ }
50
+ function zoneBounds(zone, viewport) {
51
+ const w = viewport.width;
52
+ const h = viewport.height;
53
+ const halfW = Math.round(w / 2);
54
+ const halfH = Math.round(h / 2);
55
+ switch (zone) {
56
+ case "left":
57
+ return { x: 0, y: 0, width: halfW, height: h };
58
+ case "right":
59
+ return { x: halfW, y: 0, width: w - halfW, height: h };
60
+ case "top":
61
+ return { x: 0, y: 0, width: w, height: halfH };
62
+ case "bottom":
63
+ return { x: 0, y: halfH, width: w, height: h - halfH };
64
+ case "top-left":
65
+ return { x: 0, y: 0, width: halfW, height: halfH };
66
+ case "top-right":
67
+ return { x: halfW, y: 0, width: w - halfW, height: halfH };
68
+ case "bottom-left":
69
+ return { x: 0, y: halfH, width: halfW, height: h - halfH };
70
+ case "bottom-right":
71
+ return { x: halfW, y: halfH, width: w - halfW, height: h - halfH };
72
+ }
73
+ }
74
+ function detectSnapZone(x, y, viewport, options = {}) {
75
+ const threshold = options.threshold ?? 12;
76
+ const cornerSize = options.cornerSize ?? 64;
77
+ const w = viewport.width;
78
+ const h = viewport.height;
79
+ if (w <= 0 || h <= 0) return null;
80
+ const nearLeft = x <= threshold;
81
+ const nearRight = x >= w - threshold;
82
+ const nearTop = y <= threshold;
83
+ const nearBottom = y >= h - threshold;
84
+ if (nearLeft || nearRight) {
85
+ const side = nearLeft ? "left" : "right";
86
+ if (y <= cornerSize) return `top-${side}`;
87
+ if (y >= h - cornerSize) return `bottom-${side}`;
88
+ return side;
89
+ }
90
+ if (nearTop) {
91
+ if (x <= cornerSize) return "top-left";
92
+ if (x >= w - cornerSize) return "top-right";
93
+ return "top";
94
+ }
95
+ if (nearBottom) {
96
+ if (x <= cornerSize) return "bottom-left";
97
+ if (x >= w - cornerSize) return "bottom-right";
98
+ return "bottom";
99
+ }
100
+ return null;
101
+ }
102
+
103
+ // src/core/manager.ts
104
+ var LAYER_RANK = { normal: 0, floating: 1, modal: 2 };
105
+ var STAGES = ["normal", "minimized", "maximized", "snapped"];
106
+ var LAYERS = ["normal", "floating", "modal"];
107
+ var ZONES = [
108
+ "left",
109
+ "right",
110
+ "top",
111
+ "bottom",
112
+ "top-left",
113
+ "top-right",
114
+ "bottom-left",
115
+ "bottom-right"
116
+ ];
117
+ function createWindowManager(options = {}) {
118
+ const emitter = createEmitter();
119
+ const keepInViewport = options.keepInViewport ?? true;
120
+ const minVisible = options.minVisible ?? 48;
121
+ const defaultSize = options.defaultSize ?? { width: 480, height: 320 };
122
+ const cascadeOffset = options.cascadeOffset ?? 32;
123
+ const cascadeOrigin = options.cascadeOrigin ?? { x: 32, y: 32 };
124
+ const idPrefix = options.idPrefix ?? "wm";
125
+ let windows = {};
126
+ let order = [];
127
+ let focusedId = null;
128
+ let viewport = options.viewport ?? { width: 0, height: 0 };
129
+ let seq = 0;
130
+ let idCounter = 0;
131
+ let cascadeIndex = 0;
132
+ let snapshot = null;
133
+ let batchDepth = 0;
134
+ let batchDirty = false;
135
+ const pendingEvents = [];
136
+ function getState() {
137
+ if (!snapshot) {
138
+ snapshot = { windows, order, focusedId, viewport };
139
+ }
140
+ return snapshot;
141
+ }
142
+ function commit() {
143
+ snapshot = null;
144
+ if (batchDepth > 0) {
145
+ batchDirty = true;
146
+ return;
147
+ }
148
+ flushEvents();
149
+ emitter.emit("change", { state: getState() });
150
+ }
151
+ function flushEvents() {
152
+ while (pendingEvents.length > 0) {
153
+ for (const emit of pendingEvents.splice(0)) emit();
154
+ }
155
+ }
156
+ function queueEvent(emit) {
157
+ pendingEvents.push(emit);
158
+ if (batchDepth === 0) flushEvents();
159
+ }
160
+ function setWindow(next) {
161
+ windows = { ...windows, [next.id]: next };
162
+ }
163
+ function removeWindow(id) {
164
+ const { [id]: _removed, ...rest } = windows;
165
+ windows = rest;
166
+ order = order.filter((entry) => entry !== id);
167
+ }
168
+ function layerRankOf(id) {
169
+ return LAYER_RANK[windows[id].layer];
170
+ }
171
+ function sortByLayer(ids) {
172
+ return ids.map((id, index) => ({ id, index, rank: layerRankOf(id) })).sort((a, b) => a.rank !== b.rank ? a.rank - b.rank : a.index - b.index).map((entry) => entry.id);
173
+ }
174
+ function raise(id) {
175
+ const win = windows[id];
176
+ const without = order.filter((entry) => entry !== id);
177
+ const rank = LAYER_RANK[win.layer];
178
+ let insertAt = without.length;
179
+ for (let i = without.length - 1; i >= 0; i -= 1) {
180
+ if (layerRankOf(without[i]) > rank) insertAt = i;
181
+ else break;
182
+ }
183
+ const next = [...without.slice(0, insertAt), id, ...without.slice(insertAt)];
184
+ const changed = next.length !== order.length || next.some((entry, i) => entry !== order[i]);
185
+ order = next;
186
+ return changed;
187
+ }
188
+ function topModalId() {
189
+ for (let i = order.length - 1; i >= 0; i -= 1) {
190
+ const win = windows[order[i]];
191
+ if (win.layer === "modal" && win.stage !== "minimized") return win.id;
192
+ }
193
+ return null;
194
+ }
195
+ function focusTargets() {
196
+ const modal = topModalId();
197
+ return order.filter((id) => {
198
+ const win = windows[id];
199
+ if (win.stage === "minimized") return false;
200
+ if (modal && win.layer !== "modal") return false;
201
+ return true;
202
+ });
203
+ }
204
+ function focusTop() {
205
+ const targets = focusTargets();
206
+ focusedId = targets.length > 0 ? targets[targets.length - 1] : null;
207
+ }
208
+ function emitFocus(win, previous) {
209
+ queueEvent(() => emitter.emit("focus", { window: win, previous }));
210
+ }
211
+ function normalizeSize(size, win) {
212
+ return clampSize(size, win.minSize, win.maxSize);
213
+ }
214
+ function positionForOpen(init) {
215
+ if (init.x !== void 0 && init.y !== void 0) return { x: init.x, y: init.y };
216
+ const offset = cascadeIndex % 10 * cascadeOffset;
217
+ cascadeIndex += 1;
218
+ const base = { x: cascadeOrigin.x + offset, y: cascadeOrigin.y + offset };
219
+ return { x: init.x ?? base.x, y: init.y ?? base.y };
220
+ }
221
+ function open(init = {}) {
222
+ const id = init.id ?? `${idPrefix}-${++idCounter}`;
223
+ if (windows[id]) throw new Error(`wmkit: window id "${id}" already exists`);
224
+ const minSize = { width: init.minWidth ?? 160, height: init.minHeight ?? 100 };
225
+ const maxSize = init.maxWidth !== void 0 || init.maxHeight !== void 0 ? {
226
+ width: init.maxWidth ?? Number.POSITIVE_INFINITY,
227
+ height: init.maxHeight ?? Number.POSITIVE_INFINITY
228
+ } : null;
229
+ const size = clampSize(
230
+ { width: init.width ?? defaultSize.width, height: init.height ?? defaultSize.height },
231
+ minSize,
232
+ maxSize
233
+ );
234
+ const position = positionForOpen(init);
235
+ let bounds = { ...position, ...size };
236
+ if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
237
+ const requestedStage = init.stage ?? "normal";
238
+ const stage = requestedStage === "snapped" ? "normal" : requestedStage;
239
+ let win = {
240
+ id,
241
+ title: init.title ?? "Window",
242
+ bounds,
243
+ restoreBounds: null,
244
+ restoreStage: null,
245
+ stage: "normal",
246
+ snapZone: null,
247
+ layer: init.layer ?? "normal",
248
+ minSize,
249
+ maxSize,
250
+ openedSeq: ++seq,
251
+ draggable: init.draggable ?? true,
252
+ resizable: init.resizable ?? true,
253
+ closable: init.closable ?? true,
254
+ minimizable: init.minimizable ?? true,
255
+ maximizable: init.maximizable ?? true,
256
+ snappable: init.snappable ?? true,
257
+ meta: init.meta ?? {}
258
+ };
259
+ if (stage === "maximized") {
260
+ win = { ...win, stage, restoreBounds: bounds, bounds: fullBounds() };
261
+ } else if (stage === "minimized") {
262
+ win = { ...win, stage, restoreStage: "normal" };
263
+ }
264
+ setWindow(win);
265
+ raise(id);
266
+ let focusPayload = null;
267
+ if (stage !== "minimized") {
268
+ const previous = focusedId;
269
+ const modal = topModalId();
270
+ if (!modal || win.layer === "modal") {
271
+ focusedId = id;
272
+ focusPayload = { window: win, previous };
273
+ } else {
274
+ focusTop();
275
+ }
276
+ }
277
+ queueEvent(() => emitter.emit("open", { window: win }));
278
+ if (focusPayload) {
279
+ const payload = focusPayload;
280
+ queueEvent(() => emitter.emit("focus", payload));
281
+ }
282
+ queueEvent(() => emitter.emit("order", { order }));
283
+ commit();
284
+ return win;
285
+ }
286
+ function close(id) {
287
+ const win = windows[id];
288
+ if (!win) return false;
289
+ removeWindow(id);
290
+ queueEvent(() => emitter.emit("close", { window: win }));
291
+ if (focusedId === id) {
292
+ focusTop();
293
+ if (focusedId) emitFocus(windows[focusedId], id);
294
+ }
295
+ queueEvent(() => emitter.emit("order", { order }));
296
+ commit();
297
+ return true;
298
+ }
299
+ function closeAll() {
300
+ batch(() => {
301
+ for (const id of [...order]) close(id);
302
+ });
303
+ }
304
+ function focus(id) {
305
+ const win = windows[id];
306
+ if (!win) return false;
307
+ const modal = topModalId();
308
+ if (modal && modal !== id && win.layer !== "modal") {
309
+ const modalWin = windows[modal];
310
+ if (modalWin) queueEvent(() => emitter.emit("modalblocked", { window: modalWin }));
311
+ commit();
312
+ return false;
313
+ }
314
+ if (win.stage === "minimized") {
315
+ restore(id);
316
+ return focusedId === id;
317
+ }
318
+ const previous = focusedId;
319
+ const raised = raise(id);
320
+ const changed = focusedId !== id || raised;
321
+ focusedId = id;
322
+ if (changed) {
323
+ emitFocus(win, previous);
324
+ if (raised) queueEvent(() => emitter.emit("order", { order }));
325
+ commit();
326
+ }
327
+ return true;
328
+ }
329
+ function blur() {
330
+ if (focusedId === null) return;
331
+ focusedId = null;
332
+ commit();
333
+ }
334
+ function cycleFocus(direction = 1) {
335
+ const targets = focusTargets();
336
+ if (targets.length === 0) return null;
337
+ const current = focusedId ? targets.indexOf(focusedId) : -1;
338
+ const nextIndex = current === -1 ? direction === 1 ? 0 : targets.length - 1 : (current + direction + targets.length) % targets.length;
339
+ const id = targets[nextIndex];
340
+ focus(id);
341
+ return id;
342
+ }
343
+ function fullBounds() {
344
+ return { x: 0, y: 0, width: viewport.width, height: viewport.height };
345
+ }
346
+ function applyStage(id, build) {
347
+ const win = windows[id];
348
+ if (!win) return false;
349
+ const next = build(win);
350
+ if (!next) return false;
351
+ setWindow(next);
352
+ queueEvent(() => emitter.emit("stage", { window: next, previous: win.stage }));
353
+ commit();
354
+ return true;
355
+ }
356
+ function minimize(id) {
357
+ const result = applyStage(id, (win) => {
358
+ if (win.stage === "minimized") return null;
359
+ return { ...win, stage: "minimized", restoreStage: win.stage };
360
+ });
361
+ if (result && focusedId === id) {
362
+ const previous = focusedId;
363
+ focusTop();
364
+ if (focusedId) emitFocus(windows[focusedId], previous);
365
+ commit();
366
+ }
367
+ return result;
368
+ }
369
+ function maximize(id) {
370
+ const result = applyStage(id, (win) => {
371
+ if (win.stage === "maximized") return null;
372
+ const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds;
373
+ return {
374
+ ...win,
375
+ stage: "maximized",
376
+ snapZone: null,
377
+ restoreBounds,
378
+ restoreStage: null,
379
+ bounds: fullBounds()
380
+ };
381
+ });
382
+ if (result) focus(id);
383
+ return result;
384
+ }
385
+ function toggleMaximize(id) {
386
+ const win = windows[id];
387
+ if (!win) return false;
388
+ return win.stage === "maximized" ? restore(id) : maximize(id);
389
+ }
390
+ function restore(id) {
391
+ const result = applyStage(id, (win) => {
392
+ if (win.stage === "minimized") {
393
+ const target = win.restoreStage ?? "normal";
394
+ if (target === "maximized") {
395
+ return { ...win, stage: "maximized", restoreStage: null, bounds: fullBounds() };
396
+ }
397
+ if (target === "snapped" && win.snapZone) {
398
+ return {
399
+ ...win,
400
+ stage: "snapped",
401
+ restoreStage: null,
402
+ bounds: zoneBounds(win.snapZone, viewport)
403
+ };
404
+ }
405
+ return { ...win, stage: "normal", restoreStage: null };
406
+ }
407
+ if (win.stage === "normal") return null;
408
+ const bounds = win.restoreBounds ?? win.bounds;
409
+ return {
410
+ ...win,
411
+ stage: "normal",
412
+ snapZone: null,
413
+ restoreBounds: null,
414
+ restoreStage: null,
415
+ bounds: keepInViewport ? clampToViewport(bounds, viewport, minVisible) : bounds
416
+ };
417
+ });
418
+ if (result) focus(id);
419
+ return result;
420
+ }
421
+ function restoreTo(id, bounds) {
422
+ const result = applyStage(id, (win) => {
423
+ const size = normalizeSize(bounds, win);
424
+ const next = { x: bounds.x, y: bounds.y, ...size };
425
+ return {
426
+ ...win,
427
+ stage: "normal",
428
+ snapZone: null,
429
+ restoreBounds: null,
430
+ restoreStage: null,
431
+ bounds: keepInViewport ? clampToViewport(next, viewport, minVisible) : next
432
+ };
433
+ });
434
+ if (result) focus(id);
435
+ return result;
436
+ }
437
+ function snap(id, zone) {
438
+ const result = applyStage(id, (win) => {
439
+ const restoreBounds = win.stage === "normal" ? win.bounds : win.restoreBounds;
440
+ return {
441
+ ...win,
442
+ stage: "snapped",
443
+ snapZone: zone,
444
+ restoreBounds,
445
+ restoreStage: null,
446
+ bounds: zoneBounds(zone, viewport)
447
+ };
448
+ });
449
+ if (result) focus(id);
450
+ return result;
451
+ }
452
+ function move(id, x, y) {
453
+ const win = windows[id];
454
+ if (win?.stage !== "normal") return false;
455
+ let bounds = { ...win.bounds, x, y };
456
+ if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
457
+ if (boundsEqual(bounds, win.bounds)) return true;
458
+ const next = { ...win, bounds };
459
+ setWindow(next);
460
+ queueEvent(() => emitter.emit("move", { window: next }));
461
+ commit();
462
+ return true;
463
+ }
464
+ function moveBy(id, dx, dy) {
465
+ const win = windows[id];
466
+ if (!win) return false;
467
+ return move(id, win.bounds.x + dx, win.bounds.y + dy);
468
+ }
469
+ function resize(id, patch) {
470
+ const win = windows[id];
471
+ if (!win || win.stage === "maximized" || win.stage === "minimized") return false;
472
+ const merged = { ...win.bounds, ...patch };
473
+ const size = normalizeSize(merged, win);
474
+ let bounds = { x: merged.x, y: merged.y, ...size };
475
+ if (keepInViewport) bounds = clampToViewport(bounds, viewport, minVisible);
476
+ const becameNormal = win.stage === "snapped";
477
+ if (boundsEqual(bounds, win.bounds) && !becameNormal) return true;
478
+ const next = becameNormal ? { ...win, stage: "normal", snapZone: null, restoreBounds: null, bounds } : { ...win, bounds };
479
+ setWindow(next);
480
+ if (becameNormal) queueEvent(() => emitter.emit("stage", { window: next, previous: "snapped" }));
481
+ queueEvent(() => emitter.emit("resize", { window: next }));
482
+ commit();
483
+ return true;
484
+ }
485
+ function update(id, patch) {
486
+ const win = windows[id];
487
+ if (!win) return false;
488
+ const next = {
489
+ ...win,
490
+ title: patch.title ?? win.title,
491
+ layer: patch.layer ?? win.layer,
492
+ minSize: patch.minSize ?? win.minSize,
493
+ maxSize: patch.maxSize === void 0 ? win.maxSize : patch.maxSize,
494
+ draggable: patch.draggable ?? win.draggable,
495
+ resizable: patch.resizable ?? win.resizable,
496
+ closable: patch.closable ?? win.closable,
497
+ minimizable: patch.minimizable ?? win.minimizable,
498
+ maximizable: patch.maximizable ?? win.maximizable,
499
+ snappable: patch.snappable ?? win.snappable,
500
+ meta: patch.meta ? { ...win.meta, ...patch.meta } : win.meta
501
+ };
502
+ const size = normalizeSize(next.bounds, next);
503
+ const resized = size.width !== next.bounds.width || size.height !== next.bounds.height;
504
+ const finalWin = resized ? { ...next, bounds: { ...next.bounds, ...size } } : next;
505
+ setWindow(finalWin);
506
+ if (patch.layer && patch.layer !== win.layer) {
507
+ order = sortByLayer(order);
508
+ queueEvent(() => emitter.emit("order", { order }));
509
+ }
510
+ queueEvent(() => emitter.emit("update", { window: finalWin }));
511
+ if (resized) queueEvent(() => emitter.emit("resize", { window: finalWin }));
512
+ commit();
513
+ return true;
514
+ }
515
+ function setViewport(next) {
516
+ if (next.width === viewport.width && next.height === viewport.height) return;
517
+ viewport = { ...next };
518
+ batch(() => {
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
+ });
541
+ }
542
+ function minimized() {
543
+ return Object.values(windows).filter((win) => win.stage === "minimized").sort((a, b) => a.openedSeq - b.openedSeq);
544
+ }
545
+ function batch(run) {
546
+ batchDepth += 1;
547
+ try {
548
+ run();
549
+ } finally {
550
+ batchDepth -= 1;
551
+ if (batchDepth === 0 && batchDirty) {
552
+ batchDirty = false;
553
+ snapshot = null;
554
+ flushEvents();
555
+ emitter.emit("change", { state: getState() });
556
+ }
557
+ }
558
+ }
559
+ function serialize() {
560
+ return {
561
+ version: 1,
562
+ windows: order.map((id) => windows[id]).map((win) => ({
563
+ ...win,
564
+ bounds: { ...win.bounds },
565
+ restoreBounds: win.restoreBounds ? { ...win.restoreBounds } : null,
566
+ minSize: { ...win.minSize },
567
+ maxSize: win.maxSize ? { ...win.maxSize } : null,
568
+ meta: { ...win.meta }
569
+ })),
570
+ order: [...order],
571
+ focusedId
572
+ };
573
+ }
574
+ function isBounds(value) {
575
+ if (typeof value !== "object" || value === null) return false;
576
+ const bounds = value;
577
+ return typeof bounds.x === "number" && typeof bounds.y === "number" && typeof bounds.width === "number" && typeof bounds.height === "number";
578
+ }
579
+ function isSize(value) {
580
+ if (typeof value !== "object" || value === null) return false;
581
+ const size = value;
582
+ return typeof size.width === "number" && typeof size.height === "number";
583
+ }
584
+ function isSerializedWindow(value) {
585
+ if (typeof value !== "object" || value === null) return false;
586
+ const win = value;
587
+ return typeof win.id === "string" && win.id.length > 0 && isBounds(win.bounds) && STAGES.includes(win.stage) && LAYERS.includes(win.layer);
588
+ }
589
+ function hydrate(data) {
590
+ if (typeof data !== "object" || data === null) return false;
591
+ if (data.version !== 1 || !Array.isArray(data.windows)) return false;
592
+ if (!data.windows.every(isSerializedWindow)) return false;
593
+ const nextWindows = {};
594
+ let maxSeq = 0;
595
+ for (const raw of data.windows) {
596
+ if (nextWindows[raw.id]) return false;
597
+ const win = {
598
+ id: raw.id,
599
+ title: typeof raw.title === "string" ? raw.title : "Window",
600
+ bounds: { ...raw.bounds },
601
+ restoreBounds: raw.restoreBounds && isBounds(raw.restoreBounds) ? { ...raw.restoreBounds } : null,
602
+ restoreStage: STAGES.includes(raw.restoreStage) ? raw.restoreStage : null,
603
+ stage: raw.stage,
604
+ snapZone: ZONES.includes(raw.snapZone) ? raw.snapZone : null,
605
+ layer: raw.layer,
606
+ minSize: isSize(raw.minSize) ? { ...raw.minSize } : { width: 160, height: 100 },
607
+ maxSize: isSize(raw.maxSize) ? { ...raw.maxSize } : null,
608
+ openedSeq: typeof raw.openedSeq === "number" ? raw.openedSeq : ++maxSeq,
609
+ draggable: raw.draggable !== false,
610
+ resizable: raw.resizable !== false,
611
+ closable: raw.closable !== false,
612
+ minimizable: raw.minimizable !== false,
613
+ maximizable: raw.maximizable !== false,
614
+ snappable: raw.snappable !== false,
615
+ meta: raw.meta && typeof raw.meta === "object" ? raw.meta : {}
616
+ };
617
+ maxSeq = Math.max(maxSeq, win.openedSeq);
618
+ nextWindows[win.id] = win;
619
+ }
620
+ const requestedOrder = Array.isArray(data.order) ? data.order : [];
621
+ const seen = /* @__PURE__ */ new Set();
622
+ const nextOrder = [];
623
+ for (const id of requestedOrder) {
624
+ if (typeof id === "string" && nextWindows[id] && !seen.has(id)) {
625
+ seen.add(id);
626
+ nextOrder.push(id);
627
+ }
628
+ }
629
+ for (const id of Object.keys(nextWindows)) {
630
+ if (!seen.has(id)) nextOrder.push(id);
631
+ }
632
+ windows = nextWindows;
633
+ order = nextOrder;
634
+ order = sortByLayer(order);
635
+ seq = maxSeq;
636
+ focusedId = data.focusedId && windows[data.focusedId] ? data.focusedId : null;
637
+ if (focusedId && windows[focusedId]?.stage === "minimized") focusedId = null;
638
+ if (!focusedId) focusTop();
639
+ commit();
640
+ return true;
641
+ }
642
+ function destroy() {
643
+ emitter.clear();
644
+ }
645
+ return {
646
+ open,
647
+ close,
648
+ closeAll,
649
+ focus,
650
+ blur,
651
+ cycleFocus,
652
+ minimize,
653
+ maximize,
654
+ toggleMaximize,
655
+ restore,
656
+ restoreTo,
657
+ snap,
658
+ move,
659
+ moveBy,
660
+ resize,
661
+ update,
662
+ get: (id) => windows[id],
663
+ getState,
664
+ minimized,
665
+ setViewport,
666
+ batch,
667
+ serialize,
668
+ hydrate,
669
+ subscribe: (listener) => emitter.on("change", ({ state }) => listener(state)),
670
+ on: (event, listener) => emitter.on(event, listener),
671
+ destroy
672
+ };
673
+ }
674
+
675
+ // src/dom/animate.ts
676
+ function prefersReducedMotion(win) {
677
+ return win.matchMedia("(prefers-reduced-motion: reduce)").matches;
678
+ }
679
+ function flipToTarget(source, target, options = {}) {
680
+ const view = source.ownerDocument.defaultView;
681
+ if (!view || prefersReducedMotion(view)) return;
682
+ if (typeof source.animate !== "function") return;
683
+ const from = source.getBoundingClientRect();
684
+ const to = target.getBoundingClientRect();
685
+ if (from.width === 0 || to.width === 0) return;
686
+ const ghost = source.ownerDocument.createElement("div");
687
+ const style = view.getComputedStyle(source);
688
+ ghost.style.cssText = `position:fixed;left:${from.left}px;top:${from.top}px;width:${from.width}px;height:${from.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`;
689
+ source.ownerDocument.body.append(ghost);
690
+ const dx = to.left + to.width / 2 - (from.left + from.width / 2);
691
+ const dy = to.top + to.height / 2 - (from.top + from.height / 2);
692
+ const scaleX = Math.max(to.width / from.width, 0.05);
693
+ const scaleY = Math.max(to.height / from.height, 0.05);
694
+ const animation = ghost.animate(
695
+ [
696
+ { transform: "translate(0, 0) scale(1, 1)", opacity: 0.9 },
697
+ { transform: `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`, opacity: 0.2 }
698
+ ],
699
+ {
700
+ duration: options.duration ?? 260,
701
+ easing: options.easing ?? "cubic-bezier(0.32, 0.72, 0, 1)"
702
+ }
703
+ );
704
+ animation.onfinish = () => ghost.remove();
705
+ animation.oncancel = () => ghost.remove();
706
+ }
707
+
708
+ // src/dom/announcer.ts
709
+ var defaultMessages = {
710
+ opened: (title) => `${title} window opened`,
711
+ closed: (title) => `${title} window closed`,
712
+ minimized: (title) => `${title} minimized`,
713
+ restored: (title) => `${title} restored`,
714
+ maximized: (title) => `${title} maximized`,
715
+ snapped: (title, zone) => `${title} snapped to ${zone.replace("-", " ")}`,
716
+ focused: (title) => `${title} focused`
717
+ };
718
+ function createAnnouncer(wm, container, messages = {}) {
719
+ const dict = { ...defaultMessages, ...messages };
720
+ const element = container.ownerDocument.createElement("div");
721
+ element.setAttribute("role", "status");
722
+ element.setAttribute("aria-live", "polite");
723
+ element.dataset.wmAnnouncer = "";
724
+ 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";
725
+ container.append(element);
726
+ let clearTimer;
727
+ function announce(message) {
728
+ element.textContent = message;
729
+ if (clearTimer !== void 0) clearTimeout(clearTimer);
730
+ clearTimer = setTimeout(() => {
731
+ element.textContent = "";
732
+ }, 2e3);
733
+ }
734
+ const unsubscribers = [
735
+ wm.on("open", ({ window: win }) => announce(dict.opened(win.title))),
736
+ wm.on("close", ({ window: win }) => announce(dict.closed(win.title))),
737
+ wm.on("stage", ({ window: win, previous }) => {
738
+ if (win.stage === "minimized") announce(dict.minimized(win.title));
739
+ else if (win.stage === "maximized") announce(dict.maximized(win.title));
740
+ else if (win.stage === "snapped" && win.snapZone)
741
+ announce(dict.snapped(win.title, win.snapZone));
742
+ else if (win.stage === "normal" && previous !== "normal") announce(dict.restored(win.title));
743
+ })
744
+ ];
745
+ return {
746
+ element,
747
+ announce,
748
+ destroy() {
749
+ for (const unsubscribe of unsubscribers) unsubscribe();
750
+ if (clearTimer !== void 0) clearTimeout(clearTimer);
751
+ element.remove();
752
+ }
753
+ };
754
+ }
755
+
756
+ // src/dom/controller.ts
757
+ var RESIZE_DIRECTIONS = ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
758
+ var RESIZE_CURSORS = {
759
+ n: "ns-resize",
760
+ s: "ns-resize",
761
+ e: "ew-resize",
762
+ w: "ew-resize",
763
+ ne: "nesw-resize",
764
+ sw: "nesw-resize",
765
+ nw: "nwse-resize",
766
+ se: "nwse-resize"
767
+ };
768
+ var INTERACTIVE_SELECTOR = "button, input, select, textarea, a[href], [contenteditable], [data-wm-close], [data-wm-minimize], [data-wm-maximize]";
769
+ function windowOf(element) {
770
+ const view = element.ownerDocument.defaultView;
771
+ if (!view) throw new Error("wmkit: desktop element is not attached to a document");
772
+ return view;
773
+ }
774
+ function attachDesktop(wm, element, options = {}) {
775
+ const doc = element.ownerDocument;
776
+ const view = windowOf(element);
777
+ const snapEnabled = options.snap !== false;
778
+ const snapOptions = typeof options.snap === "object" ? options.snap : {};
779
+ const snapPreviewEnabled = snapOptions.preview !== false;
780
+ const topEdge = snapOptions.topEdge ?? "maximize";
781
+ const keyboardEnabled = options.keyboard !== false;
782
+ const keyboardOptions = typeof options.keyboard === "object" ? options.keyboard : {};
783
+ const moveStep = keyboardOptions.moveStep ?? 16;
784
+ const cycleEnabled = keyboardOptions.cycle !== false;
785
+ element.dataset.wmDesktop = "";
786
+ if (view.getComputedStyle(element).position === "static") {
787
+ element.style.position = "relative";
788
+ }
789
+ const registry = /* @__PURE__ */ new Map();
790
+ const cleanup = [];
791
+ let lastOrder = null;
792
+ let lastFocused = null;
793
+ let drag = null;
794
+ let announcer = null;
795
+ if (options.announce !== false) {
796
+ announcer = createAnnouncer(
797
+ wm,
798
+ element,
799
+ typeof options.announce === "object" ? options.announce : {}
800
+ );
801
+ cleanup.push(() => announcer?.destroy());
802
+ }
803
+ let preview = null;
804
+ function showPreview(bounds) {
805
+ if (!snapPreviewEnabled) return;
806
+ if (!preview) {
807
+ preview = doc.createElement("div");
808
+ preview.dataset.wmSnapPreview = "";
809
+ preview.style.cssText = "position:absolute;left:0;top:0;pointer-events:none;display:none;z-index:2147483646";
810
+ element.append(preview);
811
+ }
812
+ preview.style.display = "block";
813
+ preview.style.transform = `translate3d(${bounds.x}px, ${bounds.y}px, 0)`;
814
+ preview.style.width = `${bounds.width}px`;
815
+ preview.style.height = `${bounds.height}px`;
816
+ }
817
+ function hidePreview() {
818
+ if (preview) preview.style.display = "none";
819
+ }
820
+ if (options.autoViewport !== false) {
821
+ const applyViewport = () => wm.setViewport({ width: element.clientWidth, height: element.clientHeight });
822
+ applyViewport();
823
+ const observer = new ResizeObserver(applyViewport);
824
+ observer.observe(element);
825
+ cleanup.push(() => observer.disconnect());
826
+ }
827
+ function syncWindow(attached, win, zIndex) {
828
+ const el = attached.element;
829
+ const firstSync = attached.lastState === null;
830
+ if (firstSync) el.style.transition = "none";
831
+ if (attached.lastState !== win) {
832
+ el.style.transform = `translate3d(${win.bounds.x}px, ${win.bounds.y}px, 0)`;
833
+ el.style.width = `${win.bounds.width}px`;
834
+ el.style.height = `${win.bounds.height}px`;
835
+ el.dataset.wmStage = win.stage;
836
+ el.dataset.wmLayer = win.layer;
837
+ el.hidden = win.stage === "minimized";
838
+ el.setAttribute("aria-label", win.title);
839
+ if (win.layer === "modal") el.setAttribute("aria-modal", "true");
840
+ else el.removeAttribute("aria-modal");
841
+ for (const handle of attached.handles) {
842
+ handle.style.display = win.resizable && win.stage === "normal" ? "" : "none";
843
+ }
844
+ attached.lastState = win;
845
+ }
846
+ el.style.zIndex = String(zIndex + 1);
847
+ if (firstSync) {
848
+ void el.offsetWidth;
849
+ el.style.transition = "";
850
+ }
851
+ }
852
+ function syncAll() {
853
+ const state = wm.getState();
854
+ const orderChanged = state.order !== lastOrder;
855
+ state.order.forEach((id, index) => {
856
+ const attached = registry.get(id);
857
+ const win = state.windows[id];
858
+ if (!attached || !win) return;
859
+ if (orderChanged || attached.lastState !== win) syncWindow(attached, win, index);
860
+ });
861
+ if (state.focusedId !== lastFocused) {
862
+ if (lastFocused) {
863
+ const prev = registry.get(lastFocused);
864
+ if (prev) delete prev.element.dataset.wmFocused;
865
+ }
866
+ if (state.focusedId) {
867
+ const next = registry.get(state.focusedId);
868
+ if (next) next.element.dataset.wmFocused = "";
869
+ }
870
+ lastFocused = state.focusedId;
871
+ }
872
+ lastOrder = state.order;
873
+ }
874
+ cleanup.push(wm.subscribe(syncAll));
875
+ cleanup.push(
876
+ wm.on("focus", ({ window: win }) => {
877
+ const attached = registry.get(win.id);
878
+ if (!attached) return;
879
+ if (!attached.element.contains(doc.activeElement)) {
880
+ attached.element.focus({ preventScroll: true });
881
+ }
882
+ })
883
+ );
884
+ cleanup.push(
885
+ wm.on("modalblocked", ({ window: win }) => {
886
+ const attached = registry.get(win.id);
887
+ if (!attached) return;
888
+ delete attached.element.dataset.wmFlash;
889
+ void attached.element.offsetWidth;
890
+ attached.element.dataset.wmFlash = "";
891
+ })
892
+ );
893
+ cleanup.push(
894
+ wm.on("stage", ({ window: win, previous }) => {
895
+ if (win.stage !== "minimized" || previous === "minimized") return;
896
+ const attached = registry.get(win.id);
897
+ const target = options.minimizeTarget?.(win);
898
+ if (attached && target) flipToTarget(attached.element, target);
899
+ })
900
+ );
901
+ if (keyboardEnabled && cycleEnabled) {
902
+ const onDesktopKeydown = (event) => {
903
+ if (event.key === "F6") {
904
+ event.preventDefault();
905
+ wm.cycleFocus(event.shiftKey ? -1 : 1);
906
+ }
907
+ };
908
+ element.addEventListener("keydown", onDesktopKeydown);
909
+ cleanup.push(() => element.removeEventListener("keydown", onDesktopKeydown));
910
+ }
911
+ function desktopPoint(event) {
912
+ const rect = element.getBoundingClientRect();
913
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
914
+ }
915
+ function endDrag(cancelled) {
916
+ if (drag) drag.finish(cancelled);
917
+ }
918
+ function startDrag(id, handle, event) {
919
+ const win = wm.get(id);
920
+ if (!win?.draggable || event.button !== 0 || drag) return;
921
+ const target = event.target;
922
+ if (target?.closest(INTERACTIVE_SELECTOR)) return;
923
+ event.preventDefault();
924
+ const point = desktopPoint(event);
925
+ const startBounds = win.bounds;
926
+ const session = {
927
+ id,
928
+ pointerId: event.pointerId,
929
+ grabDX: point.x - startBounds.x,
930
+ grabDY: point.y - startBounds.y,
931
+ grabRatio: clamp((point.x - startBounds.x) / startBounds.width, 0.05, 0.95),
932
+ startBounds,
933
+ restored: win.stage === "normal",
934
+ moved: false,
935
+ zone: null,
936
+ raf: 0,
937
+ pendingX: 0,
938
+ pendingY: 0,
939
+ hasPending: false,
940
+ finish: () => {
941
+ }
942
+ };
943
+ const attached = registry.get(id);
944
+ const el = attached?.element;
945
+ function flush() {
946
+ if (!session.hasPending || drag !== session) return;
947
+ session.hasPending = false;
948
+ session.raf = 0;
949
+ const current = wm.get(id);
950
+ if (!current) return;
951
+ if (!session.restored) {
952
+ const source = current.restoreBounds ?? session.startBounds;
953
+ const width = source.width;
954
+ const height = source.height;
955
+ wm.restoreTo(id, {
956
+ x: session.pendingX - width * session.grabRatio,
957
+ y: session.pendingY - Math.min(session.grabDY, 24),
958
+ width,
959
+ height
960
+ });
961
+ session.restored = true;
962
+ const restoredWin = wm.get(id);
963
+ if (restoredWin) {
964
+ session.grabDX = session.pendingX - restoredWin.bounds.x;
965
+ session.grabDY = session.pendingY - restoredWin.bounds.y;
966
+ }
967
+ return;
968
+ }
969
+ wm.move(id, session.pendingX - session.grabDX, session.pendingY - session.grabDY);
970
+ if (snapEnabled && current.snappable) {
971
+ const viewport = wm.getState().viewport;
972
+ const rawZone = detectSnapZone(session.pendingX, session.pendingY, viewport, snapOptions);
973
+ let zone = rawZone;
974
+ if (rawZone === "top") {
975
+ if (topEdge === "maximize") zone = current.maximizable ? "maximize" : null;
976
+ else if (topEdge === "none") zone = null;
977
+ }
978
+ session.zone = zone;
979
+ if (zone) {
980
+ showPreview(
981
+ zone === "maximize" ? { x: 0, y: 0, width: viewport.width, height: viewport.height } : zoneBounds(zone, viewport)
982
+ );
983
+ } else {
984
+ hidePreview();
985
+ }
986
+ }
987
+ }
988
+ function onMove(moveEvent) {
989
+ if (moveEvent.pointerId !== session.pointerId) return;
990
+ const movePoint = desktopPoint(moveEvent);
991
+ if (!session.moved) {
992
+ const travelled = Math.abs(movePoint.x - (session.startBounds.x + session.grabDX)) + Math.abs(movePoint.y - (session.startBounds.y + session.grabDY));
993
+ if (travelled < 3 && session.restored) return;
994
+ session.moved = true;
995
+ if (el) el.dataset.wmDragging = "";
996
+ }
997
+ session.pendingX = movePoint.x;
998
+ session.pendingY = movePoint.y;
999
+ session.hasPending = true;
1000
+ if (session.raf === 0) session.raf = view.requestAnimationFrame(flush);
1001
+ }
1002
+ function onUp(upEvent) {
1003
+ if (upEvent.pointerId !== session.pointerId) return;
1004
+ finish(false);
1005
+ }
1006
+ function onCancel(cancelEvent) {
1007
+ if (cancelEvent.pointerId !== session.pointerId) return;
1008
+ finish(true);
1009
+ }
1010
+ function onKeydown(keyEvent) {
1011
+ if (keyEvent.key === "Escape") {
1012
+ keyEvent.preventDefault();
1013
+ finish(true);
1014
+ }
1015
+ }
1016
+ function finish(cancelled) {
1017
+ if (drag !== session) return;
1018
+ if (session.raf !== 0) view?.cancelAnimationFrame(session.raf);
1019
+ if (session.hasPending && !cancelled) flush();
1020
+ drag = null;
1021
+ handle.removeEventListener("pointermove", onMove);
1022
+ handle.removeEventListener("pointerup", onUp);
1023
+ handle.removeEventListener("pointercancel", onCancel);
1024
+ doc.removeEventListener("keydown", onKeydown, true);
1025
+ if (handle.hasPointerCapture(session.pointerId)) {
1026
+ handle.releasePointerCapture(session.pointerId);
1027
+ }
1028
+ if (el) delete el.dataset.wmDragging;
1029
+ hidePreview();
1030
+ if (cancelled) {
1031
+ if (session.moved && session.restored) {
1032
+ wm.move(id, session.startBounds.x, session.startBounds.y);
1033
+ }
1034
+ return;
1035
+ }
1036
+ if (session.moved && session.zone) {
1037
+ if (session.zone === "maximize") wm.maximize(id);
1038
+ else wm.snap(id, session.zone);
1039
+ }
1040
+ }
1041
+ session.finish = finish;
1042
+ drag = session;
1043
+ handle.setPointerCapture(event.pointerId);
1044
+ handle.addEventListener("pointermove", onMove);
1045
+ handle.addEventListener("pointerup", onUp);
1046
+ handle.addEventListener("pointercancel", onCancel);
1047
+ doc.addEventListener("keydown", onKeydown, true);
1048
+ }
1049
+ function startResize(id, direction, event) {
1050
+ const win = wm.get(id);
1051
+ if (!win?.resizable || event.button !== 0 || drag) return;
1052
+ event.preventDefault();
1053
+ event.stopPropagation();
1054
+ const handleEl = event.currentTarget;
1055
+ const startPoint = desktopPoint(event);
1056
+ const start = win.bounds;
1057
+ const minSize = win.minSize;
1058
+ const maxSize = win.maxSize;
1059
+ let raf = 0;
1060
+ let pendingX = 0;
1061
+ let pendingY = 0;
1062
+ let hasPending = false;
1063
+ const attached = registry.get(id);
1064
+ if (attached) attached.element.dataset.wmResizing = direction;
1065
+ function clampWidth(width) {
1066
+ return clamp(width, minSize.width, maxSize ? maxSize.width : Number.POSITIVE_INFINITY);
1067
+ }
1068
+ function clampHeight(height) {
1069
+ return clamp(height, minSize.height, maxSize ? maxSize.height : Number.POSITIVE_INFINITY);
1070
+ }
1071
+ function flush() {
1072
+ if (!hasPending) return;
1073
+ hasPending = false;
1074
+ raf = 0;
1075
+ const dx = pendingX - startPoint.x;
1076
+ const dy = pendingY - startPoint.y;
1077
+ const next = { ...start };
1078
+ if (direction.includes("e")) next.width = clampWidth(start.width + dx);
1079
+ if (direction.includes("s")) next.height = clampHeight(start.height + dy);
1080
+ if (direction.includes("w")) {
1081
+ next.width = clampWidth(start.width - dx);
1082
+ next.x = start.x + (start.width - next.width);
1083
+ }
1084
+ if (direction.includes("n")) {
1085
+ next.height = clampHeight(start.height - dy);
1086
+ next.y = start.y + (start.height - next.height);
1087
+ }
1088
+ wm.resize(id, next);
1089
+ }
1090
+ function onMove(moveEvent) {
1091
+ if (moveEvent.pointerId !== event.pointerId) return;
1092
+ const movePoint = desktopPoint(moveEvent);
1093
+ pendingX = movePoint.x;
1094
+ pendingY = movePoint.y;
1095
+ hasPending = true;
1096
+ if (raf === 0) raf = view?.requestAnimationFrame(flush) ?? 0;
1097
+ }
1098
+ function finish(cancelled) {
1099
+ if (raf !== 0) view?.cancelAnimationFrame(raf);
1100
+ if (hasPending && !cancelled) flush();
1101
+ handleEl.removeEventListener("pointermove", onMove);
1102
+ handleEl.removeEventListener("pointerup", onUp);
1103
+ handleEl.removeEventListener("pointercancel", onCancelPointer);
1104
+ doc.removeEventListener("keydown", onKeydown, true);
1105
+ if (handleEl.hasPointerCapture(event.pointerId)) {
1106
+ handleEl.releasePointerCapture(event.pointerId);
1107
+ }
1108
+ if (attached) delete attached.element.dataset.wmResizing;
1109
+ if (cancelled) wm.resize(id, start);
1110
+ }
1111
+ function onUp(upEvent) {
1112
+ if (upEvent.pointerId !== event.pointerId) return;
1113
+ finish(false);
1114
+ }
1115
+ function onCancelPointer(cancelEvent) {
1116
+ if (cancelEvent.pointerId !== event.pointerId) return;
1117
+ finish(true);
1118
+ }
1119
+ function onKeydown(keyEvent) {
1120
+ if (keyEvent.key === "Escape") {
1121
+ keyEvent.preventDefault();
1122
+ finish(true);
1123
+ }
1124
+ }
1125
+ handleEl.setPointerCapture(event.pointerId);
1126
+ handleEl.addEventListener("pointermove", onMove);
1127
+ handleEl.addEventListener("pointerup", onUp);
1128
+ handleEl.addEventListener("pointercancel", onCancelPointer);
1129
+ doc.addEventListener("keydown", onKeydown, true);
1130
+ }
1131
+ function attachWindow(id, windowElement, windowOptions = {}) {
1132
+ const win = wm.get(id);
1133
+ if (!win) throw new Error(`wmkit: cannot attach unknown window "${id}"`);
1134
+ if (registry.has(id)) throw new Error(`wmkit: window "${id}" is already attached`);
1135
+ const attached = {
1136
+ element: windowElement,
1137
+ handle: null,
1138
+ handles: [],
1139
+ lastState: null,
1140
+ cleanup: []
1141
+ };
1142
+ windowElement.dataset.wmWindow = id;
1143
+ windowElement.setAttribute("role", "dialog");
1144
+ windowElement.tabIndex = -1;
1145
+ windowElement.style.position = "absolute";
1146
+ windowElement.style.left = "0";
1147
+ windowElement.style.top = "0";
1148
+ const titleEl = windowElement.querySelector("[data-wm-title]");
1149
+ if (titleEl) {
1150
+ if (!titleEl.id) titleEl.id = `wmkit-title-${id}`;
1151
+ windowElement.setAttribute("aria-labelledby", titleEl.id);
1152
+ }
1153
+ const handle = typeof windowOptions.handle === "string" ? windowElement.querySelector(windowOptions.handle) : windowOptions.handle ?? windowElement.querySelector("[data-wm-drag]");
1154
+ attached.handle = handle;
1155
+ const onPointerDownFocus = () => {
1156
+ wm.focus(id);
1157
+ };
1158
+ windowElement.addEventListener("pointerdown", onPointerDownFocus, true);
1159
+ attached.cleanup.push(
1160
+ () => windowElement.removeEventListener("pointerdown", onPointerDownFocus, true)
1161
+ );
1162
+ const onClick = (event) => {
1163
+ const target = event.target;
1164
+ if (!target) return;
1165
+ const current = wm.get(id);
1166
+ if (!current) return;
1167
+ if (target.closest("[data-wm-close]")) {
1168
+ if (current.closable) wm.close(id);
1169
+ } else if (target.closest("[data-wm-minimize]")) {
1170
+ if (current.minimizable) wm.minimize(id);
1171
+ } else if (target.closest("[data-wm-maximize]")) {
1172
+ if (current.maximizable) wm.toggleMaximize(id);
1173
+ }
1174
+ };
1175
+ windowElement.addEventListener("click", onClick);
1176
+ attached.cleanup.push(() => windowElement.removeEventListener("click", onClick));
1177
+ if (handle) {
1178
+ handle.style.touchAction = "none";
1179
+ const onHandleDown = (event) => startDrag(id, handle, event);
1180
+ handle.addEventListener("pointerdown", onHandleDown);
1181
+ attached.cleanup.push(() => handle.removeEventListener("pointerdown", onHandleDown));
1182
+ const onDoubleClick = (event) => {
1183
+ const target = event.target;
1184
+ if (target?.closest(INTERACTIVE_SELECTOR)) return;
1185
+ const current = wm.get(id);
1186
+ if (current?.maximizable) wm.toggleMaximize(id);
1187
+ };
1188
+ handle.addEventListener("dblclick", onDoubleClick);
1189
+ attached.cleanup.push(() => handle.removeEventListener("dblclick", onDoubleClick));
1190
+ }
1191
+ if (windowOptions.resizeHandles !== false) {
1192
+ for (const direction of RESIZE_DIRECTIONS) {
1193
+ const resizeHandle = doc.createElement("div");
1194
+ resizeHandle.dataset.wmResize = direction;
1195
+ resizeHandle.setAttribute("aria-hidden", "true");
1196
+ const base = "position:absolute;touch-action:none;user-select:none;-webkit-user-select:none;";
1197
+ const size = 8;
1198
+ const corner = 12;
1199
+ const styles = {
1200
+ n: `top:${-size / 2}px;left:${corner}px;right:${corner}px;height:${size}px`,
1201
+ s: `bottom:${-size / 2}px;left:${corner}px;right:${corner}px;height:${size}px`,
1202
+ e: `right:${-size / 2}px;top:${corner}px;bottom:${corner}px;width:${size}px`,
1203
+ w: `left:${-size / 2}px;top:${corner}px;bottom:${corner}px;width:${size}px`,
1204
+ ne: `top:${-size / 2}px;right:${-size / 2}px;width:${corner}px;height:${corner}px`,
1205
+ nw: `top:${-size / 2}px;left:${-size / 2}px;width:${corner}px;height:${corner}px`,
1206
+ se: `bottom:${-size / 2}px;right:${-size / 2}px;width:${corner}px;height:${corner}px`,
1207
+ sw: `bottom:${-size / 2}px;left:${-size / 2}px;width:${corner}px;height:${corner}px`
1208
+ };
1209
+ resizeHandle.style.cssText = `${base}${styles[direction]};cursor:${RESIZE_CURSORS[direction]}`;
1210
+ const onResizeDown = (event) => startResize(id, direction, event);
1211
+ resizeHandle.addEventListener("pointerdown", onResizeDown);
1212
+ attached.cleanup.push(() => resizeHandle.removeEventListener("pointerdown", onResizeDown));
1213
+ windowElement.append(resizeHandle);
1214
+ attached.handles.push(resizeHandle);
1215
+ }
1216
+ }
1217
+ const onWindowKeydown = (event) => {
1218
+ const target = event.target;
1219
+ if (target?.closest(INTERACTIVE_SELECTOR)) return;
1220
+ const current = wm.get(id);
1221
+ if (!current) return;
1222
+ const arrows = {
1223
+ ArrowLeft: [-1, 0],
1224
+ ArrowRight: [1, 0],
1225
+ ArrowUp: [0, -1],
1226
+ ArrowDown: [0, 1]
1227
+ };
1228
+ const vector = arrows[event.key];
1229
+ if (!vector || !keyboardEnabled) return;
1230
+ event.preventDefault();
1231
+ const step = event.altKey ? 1 : moveStep;
1232
+ const [dx, dy] = vector;
1233
+ if (event.shiftKey) {
1234
+ if (current.resizable) {
1235
+ wm.resize(id, {
1236
+ width: current.bounds.width + dx * step,
1237
+ height: current.bounds.height + dy * step
1238
+ });
1239
+ }
1240
+ } else if (current.draggable && current.stage === "normal") {
1241
+ wm.moveBy(id, dx * step, dy * step);
1242
+ }
1243
+ };
1244
+ windowElement.addEventListener("keydown", onWindowKeydown);
1245
+ attached.cleanup.push(() => windowElement.removeEventListener("keydown", onWindowKeydown));
1246
+ const onModalTrap = (event) => {
1247
+ if (event.key !== "Tab") return;
1248
+ const current = wm.get(id);
1249
+ if (current?.layer !== "modal") return;
1250
+ const focusables = windowElement.querySelectorAll(
1251
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
1252
+ );
1253
+ if (focusables.length === 0) return;
1254
+ const first = focusables[0];
1255
+ const last = focusables[focusables.length - 1];
1256
+ if (!first || !last) return;
1257
+ if (event.shiftKey && doc.activeElement === first) {
1258
+ event.preventDefault();
1259
+ last.focus();
1260
+ } else if (!event.shiftKey && doc.activeElement === last) {
1261
+ event.preventDefault();
1262
+ first.focus();
1263
+ }
1264
+ };
1265
+ windowElement.addEventListener("keydown", onModalTrap);
1266
+ attached.cleanup.push(() => windowElement.removeEventListener("keydown", onModalTrap));
1267
+ registry.set(id, attached);
1268
+ lastOrder = null;
1269
+ lastFocused = null;
1270
+ syncAll();
1271
+ return () => {
1272
+ if (drag?.id === id) endDrag(true);
1273
+ for (const dispose of attached.cleanup) dispose();
1274
+ for (const resizeHandle of attached.handles) resizeHandle.remove();
1275
+ registry.delete(id);
1276
+ };
1277
+ }
1278
+ return {
1279
+ element,
1280
+ wm,
1281
+ attachWindow,
1282
+ destroy() {
1283
+ endDrag(true);
1284
+ for (const [, attached] of registry) {
1285
+ for (const dispose of attached.cleanup) dispose();
1286
+ for (const resizeHandle of attached.handles) resizeHandle.remove();
1287
+ }
1288
+ registry.clear();
1289
+ for (const dispose of cleanup) dispose();
1290
+ preview?.remove();
1291
+ delete element.dataset.wmDesktop;
1292
+ }
1293
+ };
1294
+ }
1295
+ function createDesktopBinder(wm, options = {}) {
1296
+ let controller = null;
1297
+ const entries = /* @__PURE__ */ new Set();
1298
+ function attachEntry(entry) {
1299
+ if (controller && !entry.detach && wm.get(entry.id)) {
1300
+ entry.detach = controller.attachWindow(entry.id, entry.element, entry.options);
1301
+ }
1302
+ }
1303
+ wm.on("open", ({ window: win }) => {
1304
+ for (const entry of entries) {
1305
+ if (entry.id === win.id) attachEntry(entry);
1306
+ }
1307
+ });
1308
+ return {
1309
+ wm,
1310
+ controller: () => controller,
1311
+ bindDesktop(element) {
1312
+ if (controller) throw new Error("wmkit: desktop is already bound");
1313
+ controller = attachDesktop(wm, element, options);
1314
+ for (const entry of entries) attachEntry(entry);
1315
+ return () => {
1316
+ for (const entry of entries) entry.detach = null;
1317
+ controller?.destroy();
1318
+ controller = null;
1319
+ };
1320
+ },
1321
+ bindWindow(id, element, windowOptions) {
1322
+ const entry = { id, element, options: windowOptions, detach: null };
1323
+ entries.add(entry);
1324
+ attachEntry(entry);
1325
+ return () => {
1326
+ entry.detach?.();
1327
+ entry.detach = null;
1328
+ entries.delete(entry);
1329
+ };
1330
+ }
1331
+ };
1332
+ }
1333
+
1334
+ exports.attachDesktop = attachDesktop;
1335
+ exports.boundsEqual = boundsEqual;
1336
+ exports.clamp = clamp;
1337
+ exports.clampSize = clampSize;
1338
+ exports.clampToViewport = clampToViewport;
1339
+ exports.createAnnouncer = createAnnouncer;
1340
+ exports.createDesktopBinder = createDesktopBinder;
1341
+ exports.createEmitter = createEmitter;
1342
+ exports.createWindowManager = createWindowManager;
1343
+ exports.defaultMessages = defaultMessages;
1344
+ exports.detectSnapZone = detectSnapZone;
1345
+ exports.flipToTarget = flipToTarget;
1346
+ exports.prefersReducedMotion = prefersReducedMotion;
1347
+ exports.zoneBounds = zoneBounds;
1348
+ //# sourceMappingURL=chunk-KWLI7JIY.cjs.map
1349
+ //# sourceMappingURL=chunk-KWLI7JIY.cjs.map