sibujs 4.5.0 → 4.7.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 (116) hide show
  1. package/README.md +1 -1
  2. package/dist/browser.cjs +326 -133
  3. package/dist/browser.d.cts +46 -14
  4. package/dist/browser.d.ts +46 -14
  5. package/dist/browser.js +8 -6
  6. package/dist/build.cjs +2094 -1033
  7. package/dist/build.d.cts +133 -44
  8. package/dist/build.d.ts +133 -44
  9. package/dist/build.js +1372 -574
  10. package/dist/cdn.dev.global.js +10 -10
  11. package/dist/cdn.full.dev.global.js +11 -11
  12. package/dist/cdn.full.global.js +10 -10
  13. package/dist/cdn.global.js +10 -10
  14. package/dist/{chunk-2DCGACUU.js → chunk-2BPG2XDA.js} +251 -66
  15. package/dist/{chunk-IXKSNWV5.js → chunk-2INLLLMZ.js} +1 -1
  16. package/dist/{chunk-4PMLNECI.js → chunk-3QWBSL5R.js} +98 -29
  17. package/dist/{chunk-KKLW7YWL.js → chunk-4AWA2PVD.js} +275 -144
  18. package/dist/chunk-5HZXGZ6T.js +24 -0
  19. package/dist/{chunk-PCT43HW3.js → chunk-7LUJQAOJ.js} +1 -1
  20. package/dist/{chunk-5DXA2J44.js → chunk-7XHATCIH.js} +5 -2
  21. package/dist/chunk-BC2SECJD.js +44 -0
  22. package/dist/{chunk-TBYTO6BS.js → chunk-BSY63EM6.js} +136 -80
  23. package/dist/{chunk-FTIR4QW2.js → chunk-DKXRACVN.js} +6 -8
  24. package/dist/{chunk-ONOHFDLG.js → chunk-FNJXNGYZ.js} +3 -3
  25. package/dist/{chunk-XZZOBQAY.js → chunk-FQRUXCEE.js} +6 -5
  26. package/dist/{chunk-NIOYEGBQ.js → chunk-HURREPU2.js} +27 -13
  27. package/dist/chunk-J6FW5TV6.js +233 -0
  28. package/dist/{chunk-UGRX3S57.js → chunk-JWKYU5GV.js} +22 -4
  29. package/dist/chunk-NYNYSPK7.js +318 -0
  30. package/dist/{chunk-RBTPLM32.js → chunk-RUSSKG6G.js} +13 -12
  31. package/dist/{chunk-7LN645I6.js → chunk-SLM3IA34.js} +3 -3
  32. package/dist/{chunk-R25EFXXC.js → chunk-TUCPL2HB.js} +3 -3
  33. package/dist/{chunk-B3WHI2QA.js → chunk-UNWRJRKC.js} +55 -31
  34. package/dist/{chunk-KEISJXBU.js → chunk-UOL2ECCS.js} +44 -17
  35. package/dist/{chunk-RJE2BNI4.js → chunk-VZSG24LS.js} +108 -45
  36. package/dist/{chunk-RIXRAYIU.js → chunk-WEQ3DMVL.js} +10 -4
  37. package/dist/{chunk-3JZ4L5TJ.js → chunk-WOLJZUFQ.js} +354 -90
  38. package/dist/{chunk-S373NSMK.js → chunk-XYV3EDB7.js} +283 -123
  39. package/dist/{chunk-VZKNK2V7.js → chunk-XZR4PXRE.js} +296 -38
  40. package/dist/{chunk-GW3SCCZG.js → chunk-Z3OHK6QT.js} +153 -110
  41. package/dist/{chunk-OMJJM3KM.js → chunk-ZVL7TY4K.js} +287 -130
  42. package/dist/{contracts-DBdg9J_a.d.ts → contracts-CLqzJnOV.d.ts} +36 -17
  43. package/dist/{contracts-DBdg9J_a.d.cts → contracts-CTOJXu-x.d.cts} +36 -17
  44. package/dist/{customElement-OB9CIsc5.d.cts → customElement-MmInOW1U.d.cts} +21 -0
  45. package/dist/{customElement-OB9CIsc5.d.ts → customElement-MmInOW1U.d.ts} +21 -0
  46. package/dist/data.cjs +410 -155
  47. package/dist/data.d.cts +166 -12
  48. package/dist/data.d.ts +166 -12
  49. package/dist/data.js +12 -9
  50. package/dist/devtools.cjs +98 -61
  51. package/dist/devtools.js +7 -8
  52. package/dist/dispose-GEIG2KOF.js +28 -0
  53. package/dist/ecosystem.cjs +372 -126
  54. package/dist/ecosystem.d.cts +20 -3
  55. package/dist/ecosystem.d.ts +20 -3
  56. package/dist/ecosystem.js +12 -12
  57. package/dist/extras.cjs +2366 -909
  58. package/dist/extras.d.cts +11 -9
  59. package/dist/extras.d.ts +11 -9
  60. package/dist/extras.js +39 -28
  61. package/dist/index.cjs +421 -158
  62. package/dist/index.d.cts +213 -171
  63. package/dist/index.d.ts +213 -171
  64. package/dist/index.js +24 -27
  65. package/dist/motion.cjs +118 -44
  66. package/dist/motion.js +5 -5
  67. package/dist/patterns.cjs +344 -53
  68. package/dist/patterns.d.cts +28 -9
  69. package/dist/patterns.d.ts +28 -9
  70. package/dist/patterns.js +8 -8
  71. package/dist/performance.cjs +325 -220
  72. package/dist/performance.d.cts +2 -2
  73. package/dist/performance.d.ts +2 -2
  74. package/dist/performance.js +8 -9
  75. package/dist/plugin-DVgSnTfK.d.cts +112 -0
  76. package/dist/plugin-DVgSnTfK.d.ts +112 -0
  77. package/dist/plugins.cjs +664 -233
  78. package/dist/plugins.d.cts +127 -14
  79. package/dist/plugins.d.ts +127 -14
  80. package/dist/plugins.js +96 -42
  81. package/dist/signal-EotCj4hS.d.cts +110 -0
  82. package/dist/signal-EotCj4hS.d.ts +110 -0
  83. package/dist/{ssr-BiPRdZ6n.d.cts → ssr-Bli9XRW5.d.cts} +5 -0
  84. package/dist/{ssr-BiPRdZ6n.d.ts → ssr-Bli9XRW5.d.ts} +5 -0
  85. package/dist/{ssr-Y7XOEPEN.js → ssr-XOTUASDO.js} +4 -5
  86. package/dist/ssr.cjs +229 -84
  87. package/dist/ssr.d.cts +9 -3
  88. package/dist/ssr.d.ts +9 -3
  89. package/dist/ssr.js +11 -12
  90. package/dist/{startup-BMpaiMhP.d.ts → startup-BLfSeL15.d.cts} +73 -22
  91. package/dist/{startup-BMpaiMhP.d.cts → startup-BLfSeL15.d.ts} +73 -22
  92. package/dist/tagFactory-8qL9LCIx.d.cts +156 -0
  93. package/dist/tagFactory-BL2fymez.d.ts +156 -0
  94. package/dist/testing.cjs +2503 -2191
  95. package/dist/testing.d.cts +56 -5
  96. package/dist/testing.d.ts +56 -5
  97. package/dist/testing.js +580 -307
  98. package/dist/types-CJFViL6Q.d.cts +26 -0
  99. package/dist/types-CJFViL6Q.d.ts +26 -0
  100. package/dist/ui.cjs +732 -329
  101. package/dist/ui.d.cts +41 -7
  102. package/dist/ui.d.ts +41 -7
  103. package/dist/ui.js +151 -56
  104. package/dist/widgets.cjs +267 -291
  105. package/dist/widgets.js +9 -10
  106. package/package.json +4 -2
  107. package/dist/chunk-2WLZ6757.js +0 -149
  108. package/dist/chunk-CCSJMTRN.js +0 -15
  109. package/dist/chunk-QKRPLZ2V.js +0 -108
  110. package/dist/chunk-VUF4ALSW.js +0 -60
  111. package/dist/chunk-WWV3SJ3L.js +0 -131
  112. package/dist/dispose-46BOMMQJ.js +0 -19
  113. package/dist/plugin-D30wlGW5.d.cts +0 -71
  114. package/dist/plugin-D30wlGW5.d.ts +0 -71
  115. package/dist/tagFactory-DVoDpHye.d.cts +0 -215
  116. package/dist/tagFactory-DVoDpHye.d.ts +0 -215
@@ -4,16 +4,19 @@ import {
4
4
  import {
5
5
  acquireTitle
6
6
  } from "./chunk-52XFPGSN.js";
7
+ import {
8
+ globalSingleton
9
+ } from "./chunk-7ZHH77QA.js";
7
10
  import {
8
11
  effect
9
- } from "./chunk-RIXRAYIU.js";
12
+ } from "./chunk-WEQ3DMVL.js";
10
13
  import {
11
14
  batch,
12
15
  signal
13
- } from "./chunk-XZZOBQAY.js";
16
+ } from "./chunk-FQRUXCEE.js";
14
17
  import {
15
18
  reportError
16
- } from "./chunk-2WLZ6757.js";
19
+ } from "./chunk-NYNYSPK7.js";
17
20
 
18
21
  // src/browser/media.ts
19
22
  function media(query) {
@@ -306,13 +309,15 @@ function clipboard() {
306
309
  const [copied, setCopied] = signal(false);
307
310
  let copiedTimer = null;
308
311
  let disposed = false;
312
+ let generation = 0;
309
313
  async function copy(value) {
310
314
  if (disposed) return;
311
315
  if (typeof navigator === "undefined" || !navigator.clipboard) {
312
316
  return;
313
317
  }
318
+ const mine = ++generation;
314
319
  await navigator.clipboard.writeText(value);
315
- if (disposed) return;
320
+ if (disposed || mine !== generation) return;
316
321
  setText(value);
317
322
  setCopied(true);
318
323
  if (copiedTimer !== null) clearTimeout(copiedTimer);
@@ -324,6 +329,7 @@ function clipboard() {
324
329
  }
325
330
  function dispose() {
326
331
  disposed = true;
332
+ generation++;
327
333
  if (copiedTimer !== null) {
328
334
  clearTimeout(copiedTimer);
329
335
  copiedTimer = null;
@@ -345,15 +351,26 @@ function draggable(element, data) {
345
351
  let currentEl = null;
346
352
  let onDragStart = null;
347
353
  let onDragEnd = null;
354
+ let prevDraggableAttr = null;
355
+ function detach() {
356
+ if (!currentEl) return;
357
+ if (onDragStart) currentEl.removeEventListener("dragstart", onDragStart);
358
+ if (onDragEnd) currentEl.removeEventListener("dragend", onDragEnd);
359
+ if (prevDraggableAttr === null) currentEl.removeAttribute("draggable");
360
+ else currentEl.setAttribute("draggable", prevDraggableAttr);
361
+ currentEl = null;
362
+ onDragStart = null;
363
+ onDragEnd = null;
364
+ setIsDragging(false);
365
+ }
348
366
  const getter = resolveTarget2(element);
349
367
  const cleanup = effect(() => {
350
- if (currentEl && onDragStart && onDragEnd) {
351
- currentEl.removeEventListener("dragstart", onDragStart);
352
- currentEl.removeEventListener("dragend", onDragEnd);
353
- }
354
368
  const el = getter();
355
- currentEl = el;
369
+ if (el === currentEl) return;
370
+ detach();
356
371
  if (!el) return;
372
+ currentEl = el;
373
+ prevDraggableAttr = el.getAttribute("draggable");
357
374
  el.draggable = true;
358
375
  onDragStart = (e) => {
359
376
  setIsDragging(true);
@@ -367,16 +384,16 @@ function draggable(element, data) {
367
384
  el.addEventListener("dragstart", onDragStart);
368
385
  el.addEventListener("dragend", onDragEnd);
369
386
  });
387
+ let disposed = false;
370
388
  function dispose() {
389
+ if (disposed) return;
390
+ disposed = true;
371
391
  cleanup();
372
- if (currentEl && onDragStart && onDragEnd) {
373
- currentEl.removeEventListener("dragstart", onDragStart);
374
- currentEl.removeEventListener("dragend", onDragEnd);
375
- currentEl = null;
376
- }
392
+ detach();
377
393
  }
378
394
  return { isDragging, dispose };
379
395
  }
396
+ var DROP_ZONE_EXIT_GRACE_MS = 600;
380
397
  function dropZone(element, options) {
381
398
  const [isOver, setIsOver] = signal(false);
382
399
  if (typeof window === "undefined") {
@@ -388,30 +405,66 @@ function dropZone(element, options) {
388
405
  let onDragEnter = null;
389
406
  let onDragLeave = null;
390
407
  let onDrop = null;
391
- const getter = resolveTarget2(element);
392
- const cleanup = effect(() => {
408
+ let depth = 0;
409
+ let exitTimer = null;
410
+ function cancelPendingExit() {
411
+ if (exitTimer !== null) {
412
+ clearTimeout(exitTimer);
413
+ exitTimer = null;
414
+ }
415
+ }
416
+ function resetOver() {
417
+ cancelPendingExit();
418
+ depth = 0;
419
+ setIsOver(false);
420
+ }
421
+ function detach() {
393
422
  if (currentEl && onDragOver && onDragEnter && onDragLeave && onDrop) {
394
423
  currentEl.removeEventListener("dragover", onDragOver);
395
424
  currentEl.removeEventListener("dragenter", onDragEnter);
396
425
  currentEl.removeEventListener("dragleave", onDragLeave);
397
426
  currentEl.removeEventListener("drop", onDrop);
398
427
  }
428
+ currentEl = null;
429
+ resetOver();
430
+ }
431
+ const getter = resolveTarget2(element);
432
+ const cleanup = effect(() => {
399
433
  const el = getter();
400
- currentEl = el;
434
+ if (el === currentEl) return;
435
+ detach();
401
436
  if (!el) return;
437
+ currentEl = el;
402
438
  onDragOver = (e) => {
403
439
  e.preventDefault();
440
+ cancelPendingExit();
404
441
  };
405
442
  onDragEnter = (e) => {
406
443
  e.preventDefault();
444
+ cancelPendingExit();
445
+ depth++;
407
446
  setIsOver(true);
408
447
  };
409
- onDragLeave = () => {
410
- setIsOver(false);
448
+ onDragLeave = (e) => {
449
+ depth = Math.max(0, depth - 1);
450
+ if (depth === 0) {
451
+ resetOver();
452
+ return;
453
+ }
454
+ const to = e.relatedTarget;
455
+ if (to != null && typeof to.nodeType === "number") {
456
+ if (!el.contains(to)) resetOver();
457
+ return;
458
+ }
459
+ cancelPendingExit();
460
+ exitTimer = setTimeout(() => {
461
+ exitTimer = null;
462
+ resetOver();
463
+ }, DROP_ZONE_EXIT_GRACE_MS);
411
464
  };
412
465
  onDrop = (e) => {
413
466
  e.preventDefault();
414
- setIsOver(false);
467
+ resetOver();
415
468
  let transferData = null;
416
469
  if (e.dataTransfer) {
417
470
  const raw = e.dataTransfer.getData("application/json");
@@ -432,13 +485,7 @@ function dropZone(element, options) {
432
485
  });
433
486
  function dispose() {
434
487
  cleanup();
435
- if (currentEl && onDragOver && onDragEnter && onDragLeave && onDrop) {
436
- currentEl.removeEventListener("dragover", onDragOver);
437
- currentEl.removeEventListener("dragenter", onDragEnter);
438
- currentEl.removeEventListener("dragleave", onDragLeave);
439
- currentEl.removeEventListener("drop", onDrop);
440
- currentEl = null;
441
- }
488
+ detach();
442
489
  }
443
490
  return { isOver, dispose };
444
491
  }
@@ -493,9 +540,9 @@ function formatNumber(value, options) {
493
540
  function formatCurrency(value, currency, options) {
494
541
  const { locale, ...formatOptions } = options ?? {};
495
542
  return new Intl.NumberFormat(locale, {
543
+ ...formatOptions,
496
544
  style: "currency",
497
- currency,
498
- ...formatOptions
545
+ currency
499
546
  }).format(value);
500
547
  }
501
548
 
@@ -581,18 +628,36 @@ function swipe(target, options = {}) {
581
628
  }
582
629
  let startX = 0;
583
630
  let startY = 0;
584
- let tracking = false;
631
+ let trackedId = null;
632
+ const idOf = (t) => t.identifier ?? 0;
633
+ const findTracked = (list) => {
634
+ if (!list) return null;
635
+ for (let i = 0; i < list.length; i++) {
636
+ const t = list[i];
637
+ if (idOf(t) === trackedId) return t;
638
+ }
639
+ return null;
640
+ };
585
641
  const onStart = (e) => {
586
- if (e.touches.length === 0) return;
587
- startX = e.touches[0].clientX;
588
- startY = e.touches[0].clientY;
589
- tracking = true;
642
+ const onTarget = e.targetTouches ?? e.touches;
643
+ if (onTarget.length > 1) {
644
+ trackedId = null;
645
+ return;
646
+ }
647
+ const touch = e.changedTouches?.[0] ?? e.touches[0];
648
+ if (!touch) return;
649
+ trackedId = idOf(touch);
650
+ startX = touch.clientX;
651
+ startY = touch.clientY;
652
+ };
653
+ const onCancel = (e) => {
654
+ if (trackedId !== null && findTracked(e.changedTouches)) trackedId = null;
590
655
  };
591
656
  const onEnd = (e) => {
592
- if (!tracking) return;
593
- tracking = false;
594
- const touch = e.changedTouches[0];
657
+ if (trackedId === null) return;
658
+ const touch = findTracked(e.changedTouches);
595
659
  if (!touch) return;
660
+ trackedId = null;
596
661
  const dx = touch.clientX - startX;
597
662
  const dy = touch.clientY - startY;
598
663
  const absX = Math.abs(dx);
@@ -609,9 +674,12 @@ function swipe(target, options = {}) {
609
674
  };
610
675
  target.addEventListener("touchstart", onStart, { passive: true });
611
676
  target.addEventListener("touchend", onEnd, { passive: true });
677
+ target.addEventListener("touchcancel", onCancel, { passive: true });
612
678
  function dispose() {
613
679
  target.removeEventListener("touchstart", onStart);
614
680
  target.removeEventListener("touchend", onEnd);
681
+ target.removeEventListener("touchcancel", onCancel);
682
+ trackedId = null;
615
683
  }
616
684
  return { direction, dispose };
617
685
  }
@@ -638,6 +706,18 @@ function windowSize() {
638
706
  }
639
707
 
640
708
  // src/browser/urlState.ts
709
+ var _url = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.urlState.v1"), () => ({
710
+ instances: /* @__PURE__ */ new Set()
711
+ }));
712
+ function notifyUrlWrite() {
713
+ for (const sync of Array.from(_url.instances)) sync();
714
+ }
715
+ var SCROLL_ENTRY_KEY = "__sibuScrollKey";
716
+ function withoutEntryIdentity(state) {
717
+ if (state === null || typeof state !== "object" || !Object.hasOwn(state, SCROLL_ENTRY_KEY)) return state;
718
+ const { [SCROLL_ENTRY_KEY]: _identity, ...rest } = state;
719
+ return rest;
720
+ }
641
721
  function urlState() {
642
722
  if (typeof window === "undefined") {
643
723
  const [params2] = signal(new URLSearchParams());
@@ -671,26 +751,36 @@ function urlState() {
671
751
  }
672
752
  window.addEventListener("popstate", syncFromLocation);
673
753
  window.addEventListener("hashchange", syncFromLocation);
754
+ _url.instances.add(syncFromLocation);
755
+ function writeHistory(newUrl, opts) {
756
+ const state = "state" in opts ? opts.state : window.history.state;
757
+ if (opts.replace) window.history.replaceState(state, "", newUrl);
758
+ else window.history.pushState("state" in opts ? state : withoutEntryIdentity(state), "", newUrl);
759
+ }
674
760
  function setParams(next, opts = {}) {
675
761
  const p = next instanceof URLSearchParams ? next : new URLSearchParams(next);
676
762
  const query = p.toString();
677
763
  const newUrl = `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`;
678
- if (opts.replace) window.history.replaceState(null, "", newUrl);
679
- else window.history.pushState(null, "", newUrl);
764
+ writeHistory(newUrl, opts);
680
765
  lastSearch = window.location.search;
681
766
  setParamsSignal(new URLSearchParams(p));
767
+ notifyUrlWrite();
682
768
  }
683
769
  function setHash(next, opts = {}) {
684
770
  const normalized = next && next !== "#" ? next.startsWith("#") ? next : `#${next}` : "";
685
771
  const newUrl = `${window.location.pathname}${window.location.search}${normalized}`;
686
- if (opts.replace) window.history.replaceState(null, "", newUrl);
687
- else window.history.pushState(null, "", newUrl);
772
+ writeHistory(newUrl, opts);
688
773
  lastHash = normalized;
689
774
  setHashSignal(normalized);
775
+ notifyUrlWrite();
690
776
  }
777
+ let disposed = false;
691
778
  function dispose() {
779
+ if (disposed) return;
780
+ disposed = true;
692
781
  window.removeEventListener("popstate", syncFromLocation);
693
782
  window.removeEventListener("hashchange", syncFromLocation);
783
+ _url.instances.delete(syncFromLocation);
694
784
  }
695
785
  return { params, hash, setParams, setHash, dispose };
696
786
  }
@@ -707,10 +797,14 @@ function broadcast(channelName) {
707
797
  const channel = new BroadcastChannel(channelName);
708
798
  const handler = (ev) => setLast(ev.data);
709
799
  channel.addEventListener("message", handler);
800
+ let disposed = false;
710
801
  function post(message) {
802
+ if (disposed) return;
711
803
  channel.postMessage(message);
712
804
  }
713
805
  function dispose() {
806
+ if (disposed) return;
807
+ disposed = true;
714
808
  channel.removeEventListener("message", handler);
715
809
  channel.close();
716
810
  }
@@ -909,31 +1003,42 @@ function animationFrame(options = {}) {
909
1003
  let prev = -1;
910
1004
  let start = -1;
911
1005
  let disposed = false;
1006
+ let active = false;
1007
+ let generation = 0;
912
1008
  const minFrameMs = options.fpsLimit ? 1e3 / options.fpsLimit : 0;
913
1009
  const step = (now) => {
1010
+ id = null;
1011
+ if (!active) return;
1012
+ const frameGeneration = generation;
914
1013
  if (start < 0) start = now;
915
1014
  const firstTick = prev < 0;
916
1015
  const dt = firstTick ? 0 : now - prev;
917
1016
  if (firstTick || dt >= minFrameMs) {
918
- setDelta(dt);
919
- setElapsed(now - start);
920
1017
  prev = now;
1018
+ const elapsedValue = now - start;
1019
+ setDelta(dt);
1020
+ if (!active || generation !== frameGeneration) return;
1021
+ setElapsed(elapsedValue);
1022
+ if (!active || generation !== frameGeneration) return;
921
1023
  }
922
- id = requestAnimationFrame(step);
1024
+ if (id === null) id = requestAnimationFrame(step);
923
1025
  };
924
1026
  function resume() {
925
- if (disposed || id !== null) return;
926
- setRunning(true);
1027
+ if (disposed || active) return;
1028
+ active = true;
927
1029
  id = requestAnimationFrame(step);
1030
+ setRunning(true);
928
1031
  }
929
1032
  function pause() {
1033
+ generation++;
1034
+ active = false;
930
1035
  if (id !== null) {
931
1036
  cancelAnimationFrame(id);
932
1037
  id = null;
933
1038
  }
934
- setRunning(false);
935
1039
  prev = -1;
936
1040
  start = -1;
1041
+ setRunning(false);
937
1042
  }
938
1043
  function dispose() {
939
1044
  disposed = true;
@@ -1063,6 +1168,35 @@ function keyboard(options = {}) {
1063
1168
  }
1064
1169
 
1065
1170
  // src/browser/speech.ts
1171
+ var _speech = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.speech.v1"), () => ({
1172
+ queue: [],
1173
+ active: null,
1174
+ owners: /* @__PURE__ */ new Set()
1175
+ }));
1176
+ function syncOwners() {
1177
+ for (const owner of Array.from(_speech.owners)) owner.sync();
1178
+ }
1179
+ function pump(synth) {
1180
+ if (_speech.active) return;
1181
+ const index = _speech.queue.findIndex((entry2) => !entry2.owner.paused);
1182
+ if (index === -1) return;
1183
+ const [entry] = _speech.queue.splice(index, 1);
1184
+ _speech.active = entry;
1185
+ const finish = () => {
1186
+ if (_speech.active !== entry) return;
1187
+ _speech.active = null;
1188
+ pump(synth);
1189
+ syncOwners();
1190
+ };
1191
+ entry.utterance.addEventListener("end", finish, { once: true });
1192
+ entry.utterance.addEventListener("error", finish, { once: true });
1193
+ synth.speak(entry.utterance);
1194
+ }
1195
+ function __resetSpeechCoordinator() {
1196
+ _speech.queue.length = 0;
1197
+ _speech.active = null;
1198
+ _speech.owners.clear();
1199
+ }
1066
1200
  function speech() {
1067
1201
  const [speaking, setSpeaking] = signal(false);
1068
1202
  const [paused, setPaused] = signal(false);
@@ -1083,19 +1217,16 @@ function speech() {
1083
1217
  };
1084
1218
  }
1085
1219
  const synth = window.speechSynthesis;
1086
- let interval = null;
1087
- function startPolling() {
1088
- if (interval !== null) return;
1089
- interval = setInterval(() => {
1090
- setSpeaking(synth.speaking);
1091
- setPaused(synth.paused);
1092
- if (!synth.speaking && !synth.paused) {
1093
- clearInterval(interval);
1094
- interval = null;
1095
- }
1096
- }, 200);
1097
- }
1098
1220
  let disposed = false;
1221
+ const owner = {
1222
+ paused: false,
1223
+ sync: () => {
1224
+ const mine = _speech.active?.owner === owner || _speech.queue.some((entry) => entry.owner === owner);
1225
+ setSpeaking(mine);
1226
+ setPaused(owner.paused);
1227
+ }
1228
+ };
1229
+ _speech.owners.add(owner);
1099
1230
  function speak(text, options = {}) {
1100
1231
  if (disposed) return;
1101
1232
  const u = new SpeechSynthesisUtterance(text);
@@ -1108,52 +1239,48 @@ function speech() {
1108
1239
  const match = voices.find((v) => v.name === options.voice);
1109
1240
  if (match) u.voice = match;
1110
1241
  }
1111
- u.addEventListener(
1112
- "start",
1113
- () => {
1114
- if (!disposed) setSpeaking(true);
1115
- },
1116
- { once: true }
1117
- );
1118
- u.addEventListener(
1119
- "end",
1120
- () => {
1121
- if (disposed) return;
1122
- setSpeaking(false);
1123
- setPaused(false);
1124
- },
1125
- { once: true }
1126
- );
1127
- u.addEventListener(
1128
- "error",
1129
- () => {
1130
- if (disposed) return;
1131
- setSpeaking(false);
1132
- setPaused(false);
1133
- },
1134
- { once: true }
1135
- );
1136
- synth.speak(u);
1137
- setSpeaking(true);
1138
- startPolling();
1242
+ _speech.queue.push({ owner, utterance: u });
1243
+ pump(synth);
1244
+ syncOwners();
1245
+ }
1246
+ function pause() {
1247
+ if (disposed || owner.paused) return;
1248
+ owner.paused = true;
1249
+ if (_speech.active?.owner === owner) synth.pause();
1250
+ syncOwners();
1251
+ }
1252
+ function resume() {
1253
+ if (disposed || !owner.paused) return;
1254
+ owner.paused = false;
1255
+ if (_speech.active?.owner === owner) synth.resume();
1256
+ else pump(synth);
1257
+ syncOwners();
1258
+ }
1259
+ function cancel() {
1260
+ if (disposed) return;
1261
+ cancelOwned();
1262
+ syncOwners();
1263
+ }
1264
+ function cancelOwned() {
1265
+ for (let i = _speech.queue.length - 1; i >= 0; i--) {
1266
+ if (_speech.queue[i].owner === owner) _speech.queue.splice(i, 1);
1267
+ }
1268
+ owner.paused = false;
1269
+ if (_speech.active?.owner === owner) {
1270
+ _speech.active = null;
1271
+ if (synth.paused) synth.resume();
1272
+ synth.cancel();
1273
+ pump(synth);
1274
+ }
1139
1275
  }
1140
1276
  function dispose() {
1277
+ if (disposed) return;
1278
+ cancelOwned();
1141
1279
  disposed = true;
1142
- if (interval !== null) {
1143
- clearInterval(interval);
1144
- interval = null;
1145
- }
1146
- synth.cancel();
1280
+ syncOwners();
1281
+ _speech.owners.delete(owner);
1147
1282
  }
1148
- return {
1149
- speaking,
1150
- paused,
1151
- speak,
1152
- pause: () => synth.pause(),
1153
- resume: () => synth.resume(),
1154
- cancel: () => synth.cancel(),
1155
- dispose
1156
- };
1283
+ return { speaking, paused, speak, pause, resume, cancel, dispose };
1157
1284
  }
1158
1285
 
1159
1286
  // src/browser/gamepad.ts
@@ -1164,6 +1291,7 @@ function gamepad() {
1164
1291
  } };
1165
1292
  }
1166
1293
  let rafId = null;
1294
+ let disposed = false;
1167
1295
  function snapshot(pad) {
1168
1296
  return {
1169
1297
  index: pad.index,
@@ -1178,7 +1306,7 @@ function gamepad() {
1178
1306
  for (let i = 0; i < a.length; i++) {
1179
1307
  const pa = a[i];
1180
1308
  const pb = b[i];
1181
- if (pa.index !== pb.index || pa.connected !== pb.connected) return false;
1309
+ if (pa.index !== pb.index || pa.id !== pb.id || pa.connected !== pb.connected) return false;
1182
1310
  if (pa.buttons.length !== pb.buttons.length) return false;
1183
1311
  for (let j = 0; j < pa.buttons.length; j++) {
1184
1312
  if (pa.buttons[j].pressed !== pb.buttons[j].pressed) return false;
@@ -1191,15 +1319,19 @@ function gamepad() {
1191
1319
  }
1192
1320
  return true;
1193
1321
  }
1322
+ function publish() {
1323
+ const snap = Array.from(navigator.getGamepads()).filter((g) => g !== null).map(snapshot);
1324
+ if (!equal(pads(), snap)) setPads(snap);
1325
+ return snap;
1326
+ }
1194
1327
  function poll() {
1195
- const raw = navigator.getGamepads();
1196
- const snap = Array.from(raw).filter((g) => g !== null).map(snapshot);
1197
- const current = pads();
1198
- if (!equal(current, snap)) setPads(snap);
1199
- rafId = requestAnimationFrame(poll);
1328
+ rafId = null;
1329
+ if (disposed) return;
1330
+ publish();
1331
+ if (!disposed && rafId === null) rafId = requestAnimationFrame(poll);
1200
1332
  }
1201
1333
  function startPolling() {
1202
- if (rafId === null) poll();
1334
+ if (!disposed && rafId === null) poll();
1203
1335
  }
1204
1336
  function stopPolling() {
1205
1337
  if (rafId !== null) {
@@ -1209,15 +1341,14 @@ function gamepad() {
1209
1341
  }
1210
1342
  const onConnect = () => startPolling();
1211
1343
  const onDisconnect = () => {
1212
- const raw = navigator.getGamepads();
1213
- const hasAny = Array.from(raw).some((g) => g !== null);
1214
- if (!hasAny) stopPolling();
1344
+ if (publish().length === 0) stopPolling();
1215
1345
  };
1216
1346
  window.addEventListener("gamepadconnected", onConnect);
1217
1347
  window.addEventListener("gamepaddisconnected", onDisconnect);
1218
1348
  const initial = Array.from(navigator.getGamepads()).some((g) => g !== null);
1219
1349
  if (initial) startPolling();
1220
1350
  function dispose() {
1351
+ disposed = true;
1221
1352
  stopPolling();
1222
1353
  window.removeEventListener("gamepadconnected", onConnect);
1223
1354
  window.removeEventListener("gamepaddisconnected", onDisconnect);
@@ -1231,8 +1362,7 @@ function pointerLock() {
1231
1362
  if (typeof document === "undefined") {
1232
1363
  return {
1233
1364
  locked,
1234
- request: () => {
1235
- },
1365
+ request: () => Promise.resolve(),
1236
1366
  exit: () => {
1237
1367
  },
1238
1368
  dispose: () => {
@@ -1244,8 +1374,14 @@ function pointerLock() {
1244
1374
  };
1245
1375
  document.addEventListener("pointerlockchange", handler);
1246
1376
  function request(element) {
1247
- if (typeof element.requestPointerLock === "function") {
1248
- element.requestPointerLock();
1377
+ if (typeof element.requestPointerLock !== "function") {
1378
+ return Promise.resolve();
1379
+ }
1380
+ try {
1381
+ const result = element.requestPointerLock();
1382
+ return Promise.resolve(result).then(() => void 0);
1383
+ } catch (err) {
1384
+ return Promise.reject(err);
1249
1385
  }
1250
1386
  }
1251
1387
  function exit() {
@@ -1348,25 +1484,46 @@ function imageLoader(src) {
1348
1484
  };
1349
1485
  }
1350
1486
  let current = null;
1487
+ let currentSettled = false;
1351
1488
  let disposed = false;
1489
+ let startToken = 0;
1490
+ function resetState() {
1491
+ batch(() => {
1492
+ setStatus("pending");
1493
+ setImage(null);
1494
+ setWidth(0);
1495
+ setHeight(0);
1496
+ });
1497
+ }
1498
+ function abandonCurrent() {
1499
+ if (!current) return;
1500
+ const prev = current;
1501
+ current = null;
1502
+ prev.onload = null;
1503
+ prev.onerror = null;
1504
+ if (!currentSettled) prev.src = "";
1505
+ }
1352
1506
  function start(url) {
1353
- if (current) {
1354
- current.onload = null;
1355
- current.onerror = null;
1356
- }
1357
- setStatus("pending");
1358
- setImage(null);
1507
+ const token = ++startToken;
1508
+ abandonCurrent();
1509
+ resetState();
1510
+ if (disposed || token !== startToken) return;
1359
1511
  const img = new Image();
1360
1512
  current = img;
1513
+ currentSettled = false;
1361
1514
  img.onload = () => {
1362
1515
  if (disposed || current !== img) return;
1363
- setImage(img);
1364
- setWidth(img.naturalWidth);
1365
- setHeight(img.naturalHeight);
1366
- setStatus("loaded");
1516
+ currentSettled = true;
1517
+ batch(() => {
1518
+ setImage(img);
1519
+ setWidth(img.naturalWidth);
1520
+ setHeight(img.naturalHeight);
1521
+ setStatus("loaded");
1522
+ });
1367
1523
  };
1368
1524
  img.onerror = () => {
1369
1525
  if (disposed || current !== img) return;
1526
+ currentSettled = true;
1370
1527
  setStatus("error");
1371
1528
  };
1372
1529
  img.src = url;
@@ -1381,16 +1538,15 @@ function imageLoader(src) {
1381
1538
  start(src);
1382
1539
  }
1383
1540
  function dispose() {
1541
+ if (disposed) return;
1384
1542
  disposed = true;
1543
+ startToken++;
1385
1544
  if (srcEffectTeardown) {
1386
1545
  srcEffectTeardown();
1387
1546
  srcEffectTeardown = null;
1388
1547
  }
1389
- if (current) {
1390
- current.onload = null;
1391
- current.onerror = null;
1392
- current = null;
1393
- }
1548
+ abandonCurrent();
1549
+ resetState();
1394
1550
  }
1395
1551
  return { status, image, width, height, dispose };
1396
1552
  }
@@ -1424,6 +1580,7 @@ export {
1424
1580
  mutationObserver,
1425
1581
  bounds,
1426
1582
  keyboard,
1583
+ __resetSpeechCoordinator,
1427
1584
  speech,
1428
1585
  gamepad,
1429
1586
  pointerLock,