sibujs 1.4.0 → 1.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 (79) hide show
  1. package/README.md +105 -119
  2. package/dist/browser.cjs +53 -14
  3. package/dist/browser.d.cts +14 -9
  4. package/dist/browser.d.ts +14 -9
  5. package/dist/browser.js +4 -4
  6. package/dist/build.cjs +124 -42
  7. package/dist/build.d.cts +1 -1
  8. package/dist/build.d.ts +1 -1
  9. package/dist/build.js +10 -10
  10. package/dist/cdn.global.js +7 -7
  11. package/dist/chunk-5ZYQ6KDD.js +154 -0
  12. package/dist/chunk-6BMPXPUW.js +26 -0
  13. package/dist/chunk-7GRNSCFT.js +1097 -0
  14. package/dist/chunk-BGTHZHJ5.js +1016 -0
  15. package/dist/chunk-BMPL52BF.js +654 -0
  16. package/dist/chunk-GJPXRJ45.js +37 -0
  17. package/dist/chunk-JCDUJN2F.js +2779 -0
  18. package/dist/chunk-K4G4ZQNR.js +286 -0
  19. package/dist/chunk-MB6QFH3I.js +2776 -0
  20. package/dist/chunk-MYRV7VDM.js +742 -0
  21. package/dist/chunk-NZIIMDWI.js +84 -0
  22. package/dist/chunk-P3XWXJZU.js +282 -0
  23. package/dist/chunk-PDZQY43A.js +616 -0
  24. package/dist/chunk-RJ46C3CS.js +1293 -0
  25. package/dist/chunk-SFKNRVCU.js +292 -0
  26. package/dist/chunk-TDGZL5CU.js +365 -0
  27. package/dist/chunk-VAPYJN4X.js +368 -0
  28. package/dist/chunk-VQDZK23A.js +1023 -0
  29. package/dist/chunk-VQNQZCWJ.js +61 -0
  30. package/dist/chunk-XHK6BDAJ.js +76 -0
  31. package/dist/chunk-XUEEGU5O.js +409 -0
  32. package/dist/contracts-ey_Qh8ef.d.cts +239 -0
  33. package/dist/contracts-ey_Qh8ef.d.ts +239 -0
  34. package/dist/customElement-BL3Uo8dL.d.cts +318 -0
  35. package/dist/customElement-BL3Uo8dL.d.ts +318 -0
  36. package/dist/data.cjs +52 -11
  37. package/dist/data.js +6 -6
  38. package/dist/devtools.cjs +22 -24
  39. package/dist/devtools.js +26 -28
  40. package/dist/ecosystem.cjs +31 -6
  41. package/dist/ecosystem.d.cts +4 -4
  42. package/dist/ecosystem.d.ts +4 -4
  43. package/dist/ecosystem.js +7 -7
  44. package/dist/extras.cjs +304 -108
  45. package/dist/extras.d.cts +3 -3
  46. package/dist/extras.d.ts +3 -3
  47. package/dist/extras.js +19 -19
  48. package/dist/index.cjs +124 -42
  49. package/dist/index.d.cts +58 -48
  50. package/dist/index.d.ts +58 -48
  51. package/dist/index.js +10 -10
  52. package/dist/motion.cjs +13 -2
  53. package/dist/motion.d.cts +1 -1
  54. package/dist/motion.d.ts +1 -1
  55. package/dist/motion.js +3 -3
  56. package/dist/patterns.cjs +91 -24
  57. package/dist/patterns.d.cts +46 -12
  58. package/dist/patterns.d.ts +46 -12
  59. package/dist/patterns.js +5 -5
  60. package/dist/performance.cjs +97 -12
  61. package/dist/performance.d.cts +6 -1
  62. package/dist/performance.d.ts +6 -1
  63. package/dist/performance.js +5 -3
  64. package/dist/plugins.cjs +19 -13
  65. package/dist/plugins.d.cts +3 -3
  66. package/dist/plugins.d.ts +3 -3
  67. package/dist/plugins.js +16 -18
  68. package/dist/ssr.cjs +9 -0
  69. package/dist/ssr.d.cts +1 -1
  70. package/dist/ssr.d.ts +1 -1
  71. package/dist/ssr.js +7 -7
  72. package/dist/testing.js +2 -2
  73. package/dist/ui.cjs +130 -48
  74. package/dist/ui.d.cts +13 -16
  75. package/dist/ui.d.ts +13 -16
  76. package/dist/ui.js +6 -6
  77. package/dist/widgets.cjs +31 -6
  78. package/dist/widgets.js +5 -5
  79. package/package.json +1 -1
package/dist/extras.cjs CHANGED
@@ -403,12 +403,21 @@ function queueSignalNotification(signal2) {
403
403
  }
404
404
  }
405
405
  }
406
+ var MAX_DRAIN_ITERATIONS = 1e3;
406
407
  function drainNotificationQueue() {
407
408
  if (notifyDepth > 0) return;
408
409
  notifyDepth++;
409
410
  try {
410
411
  let i2 = 0;
411
412
  while (i2 < pendingQueue.length) {
413
+ if (i2 >= MAX_DRAIN_ITERATIONS) {
414
+ if (typeof console !== "undefined") {
415
+ console.error(
416
+ `[SibuJS] Notification queue exceeded ${MAX_DRAIN_ITERATIONS} iterations \u2014 likely an effect that writes to a signal it reads. Breaking to prevent infinite loop.`
417
+ );
418
+ }
419
+ break;
420
+ }
412
421
  safeInvoke(pendingQueue[i2]);
413
422
  i2++;
414
423
  }
@@ -575,21 +584,37 @@ function derived(getter, options) {
575
584
  cs._v = getter();
576
585
  }, markDirty);
577
586
  const hook = globalThis.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
587
+ let evaluating = false;
578
588
  function computedGetter() {
589
+ if (evaluating) {
590
+ throw new Error(
591
+ `[SibuJS] Circular dependency detected in derived${debugName ? ` "${debugName}"` : ""}. A derived signal cannot read itself (directly or through a chain).`
592
+ );
593
+ }
579
594
  if (trackingSuspended) {
580
595
  if (cs._d) {
581
- cs._d = false;
582
- cs._v = getter();
596
+ evaluating = true;
597
+ try {
598
+ cs._d = false;
599
+ cs._v = getter();
600
+ } finally {
601
+ evaluating = false;
602
+ }
583
603
  }
584
604
  return cs._v;
585
605
  }
586
606
  recordDependency(cs);
587
607
  if (cs._d) {
588
608
  const oldValue = cs._v;
589
- track(() => {
590
- cs._d = false;
591
- cs._v = getter();
592
- }, markDirty);
609
+ evaluating = true;
610
+ try {
611
+ track(() => {
612
+ cs._d = false;
613
+ cs._v = getter();
614
+ }, markDirty);
615
+ } finally {
616
+ evaluating = false;
617
+ }
593
618
  if (hook && oldValue !== cs._v) {
594
619
  hook.emit("computed:update", { signal: cs, oldValue, newValue: cs._v });
595
620
  }
@@ -745,9 +770,13 @@ async function withRetry(fn, options, onRetry, signal2) {
745
770
  const delay = calculateDelay(attempt, strategy, baseDelay, maxDelay, jitter);
746
771
  onRetry?.(error, attempt, delay);
747
772
  await new Promise((resolve, reject) => {
748
- const timer = setTimeout(resolve, delay);
773
+ let onAbort = null;
774
+ const timer = setTimeout(() => {
775
+ if (onAbort && signal2) signal2.removeEventListener("abort", onAbort);
776
+ resolve();
777
+ }, delay);
749
778
  if (signal2) {
750
- const onAbort = () => {
779
+ onAbort = () => {
751
780
  clearTimeout(timer);
752
781
  reject(new DOMException("Aborted", "AbortError"));
753
782
  };
@@ -898,9 +927,10 @@ function query(key, fetcher, options = {}) {
898
927
  }
899
928
  }
900
929
  }
930
+ const keyChanged = currentKey !== key2;
901
931
  currentKey = key2;
902
932
  const entry = getOrCreateEntry(key2, initialData);
903
- entry.subscribers++;
933
+ if (keyChanged || entry.subscribers === 0) entry.subscribers++;
904
934
  if (entry.gcTimer !== null) {
905
935
  clearTimeout(entry.gcTimer);
906
936
  entry.gcTimer = null;
@@ -1014,7 +1044,9 @@ function mutation(mutationFn, options = {}) {
1014
1044
  const [status, setStatus] = signal("idle");
1015
1045
  const isSuccess = derived(() => status() === "success");
1016
1046
  const isIdle = derived(() => status() === "idle");
1047
+ let runId = 0;
1017
1048
  async function execute(variables) {
1049
+ const myRun = ++runId;
1018
1050
  let context2;
1019
1051
  batch(() => {
1020
1052
  setLoading(true);
@@ -1026,6 +1058,7 @@ function mutation(mutationFn, options = {}) {
1026
1058
  context2 = await options.onMutate(variables);
1027
1059
  }
1028
1060
  const result = await withRetry(() => mutationFn(variables), options.retry);
1061
+ if (myRun !== runId) return result;
1029
1062
  batch(() => {
1030
1063
  setData(result);
1031
1064
  setLoading(false);
@@ -1036,6 +1069,7 @@ function mutation(mutationFn, options = {}) {
1036
1069
  return result;
1037
1070
  } catch (err) {
1038
1071
  const errorObj = err instanceof Error ? err : new Error(String(err));
1072
+ if (myRun !== runId) throw errorObj;
1039
1073
  batch(() => {
1040
1074
  setError(errorObj);
1041
1075
  setLoading(false);
@@ -1047,6 +1081,7 @@ function mutation(mutationFn, options = {}) {
1047
1081
  }
1048
1082
  }
1049
1083
  function reset() {
1084
+ runId++;
1050
1085
  batch(() => {
1051
1086
  setData(void 0);
1052
1087
  setError(void 0);
@@ -1296,7 +1331,10 @@ function resource(sourceOrFetcher, fetcherOrOptions, maybeOptions) {
1296
1331
  options.onSuccess?.(result);
1297
1332
  } catch (err) {
1298
1333
  if (version !== fetchVersion || disposed) return;
1299
- if (err instanceof DOMException && err.name === "AbortError") return;
1334
+ if (err instanceof DOMException && err.name === "AbortError") {
1335
+ if (version === fetchVersion) setLoading(false);
1336
+ return;
1337
+ }
1300
1338
  const errorObj = err instanceof Error ? err : new Error(String(err));
1301
1339
  batch(() => {
1302
1340
  setError(errorObj);
@@ -1550,6 +1588,7 @@ function socket(url, options) {
1550
1588
  let reconnectTimer = null;
1551
1589
  let heartbeatTimer = null;
1552
1590
  let disposed = false;
1591
+ let manuallyClosed = false;
1553
1592
  function getUrl() {
1554
1593
  return typeof url === "function" ? url() : url;
1555
1594
  }
@@ -1573,12 +1612,13 @@ function socket(url, options) {
1573
1612
  ws.onclose = () => {
1574
1613
  setStatus("closed");
1575
1614
  stopHeartbeat();
1576
- if (autoReconnect && !disposed && reconnectCount < maxReconnects) {
1615
+ if (autoReconnect && !disposed && !manuallyClosed && reconnectCount < maxReconnects) {
1577
1616
  reconnectCount++;
1578
1617
  reconnectTimer = setTimeout(() => {
1579
1618
  connect();
1580
1619
  }, reconnectDelay);
1581
1620
  }
1621
+ manuallyClosed = false;
1582
1622
  };
1583
1623
  ws.onerror = () => {
1584
1624
  };
@@ -1604,6 +1644,7 @@ function socket(url, options) {
1604
1644
  }
1605
1645
  }
1606
1646
  function close() {
1647
+ manuallyClosed = true;
1607
1648
  if (reconnectTimer !== null) {
1608
1649
  clearTimeout(reconnectTimer);
1609
1650
  reconnectTimer = null;
@@ -1774,6 +1815,8 @@ function scroll(target) {
1774
1815
  const [y, setY] = signal(0);
1775
1816
  const [isScrolling, setIsScrolling] = signal(false);
1776
1817
  let scrollTimer = null;
1818
+ let currentTarget = null;
1819
+ let effectCleanup = null;
1777
1820
  if (typeof window === "undefined") {
1778
1821
  return { x, y, isScrolling, dispose: () => {
1779
1822
  } };
@@ -1796,11 +1839,26 @@ function scroll(target) {
1796
1839
  scrollTimer = null;
1797
1840
  }, 150);
1798
1841
  };
1799
- const scrollTarget = target ? target() : null;
1800
- const eventTarget = scrollTarget || window;
1801
- eventTarget.addEventListener("scroll", handler, { passive: true });
1842
+ function attachListener(eventTarget) {
1843
+ if (currentTarget === eventTarget) return;
1844
+ if (currentTarget) currentTarget.removeEventListener("scroll", handler);
1845
+ currentTarget = eventTarget;
1846
+ currentTarget.addEventListener("scroll", handler, { passive: true });
1847
+ }
1848
+ if (target) {
1849
+ effectCleanup = effect(() => {
1850
+ const el = target();
1851
+ attachListener(el || window);
1852
+ });
1853
+ } else {
1854
+ attachListener(window);
1855
+ }
1802
1856
  function dispose() {
1803
- eventTarget.removeEventListener("scroll", handler);
1857
+ effectCleanup?.();
1858
+ if (currentTarget) {
1859
+ currentTarget.removeEventListener("scroll", handler);
1860
+ currentTarget = null;
1861
+ }
1804
1862
  if (scrollTimer !== null) {
1805
1863
  clearTimeout(scrollTimer);
1806
1864
  scrollTimer = null;
@@ -2319,31 +2377,44 @@ function urlState() {
2319
2377
  }
2320
2378
  };
2321
2379
  }
2322
- const [params, setParamsSignal] = signal(new URLSearchParams(window.location.search));
2323
- const [hash, setHashSignal] = signal(window.location.hash);
2324
- const syncFromLocation = () => {
2325
- setParamsSignal(new URLSearchParams(window.location.search));
2326
- setHashSignal(window.location.hash);
2327
- };
2328
- const onPopState = () => syncFromLocation();
2329
- window.addEventListener("popstate", onPopState);
2380
+ let lastSearch = window.location.search;
2381
+ let lastHash = window.location.hash;
2382
+ const [params, setParamsSignal] = signal(new URLSearchParams(lastSearch));
2383
+ const [hash, setHashSignal] = signal(lastHash);
2384
+ function syncFromLocation() {
2385
+ const currentSearch = window.location.search;
2386
+ const currentHash = window.location.hash;
2387
+ if (currentSearch !== lastSearch) {
2388
+ lastSearch = currentSearch;
2389
+ setParamsSignal(new URLSearchParams(currentSearch));
2390
+ }
2391
+ if (currentHash !== lastHash) {
2392
+ lastHash = currentHash;
2393
+ setHashSignal(currentHash);
2394
+ }
2395
+ }
2396
+ window.addEventListener("popstate", syncFromLocation);
2397
+ window.addEventListener("hashchange", syncFromLocation);
2330
2398
  function setParams(next, opts = {}) {
2331
2399
  const p2 = next instanceof URLSearchParams ? next : new URLSearchParams(next);
2332
2400
  const query2 = p2.toString();
2333
2401
  const newUrl = `${window.location.pathname}${query2 ? `?${query2}` : ""}${window.location.hash}`;
2334
2402
  if (opts.replace) window.history.replaceState(null, "", newUrl);
2335
2403
  else window.history.pushState(null, "", newUrl);
2404
+ lastSearch = window.location.search;
2336
2405
  setParamsSignal(new URLSearchParams(p2));
2337
2406
  }
2338
2407
  function setHash(next, opts = {}) {
2339
- const normalized = next.startsWith("#") ? next : next ? `#${next}` : "";
2408
+ const normalized = next && next !== "#" ? next.startsWith("#") ? next : `#${next}` : "";
2340
2409
  const newUrl = `${window.location.pathname}${window.location.search}${normalized}`;
2341
2410
  if (opts.replace) window.history.replaceState(null, "", newUrl);
2342
2411
  else window.history.pushState(null, "", newUrl);
2412
+ lastHash = normalized;
2343
2413
  setHashSignal(normalized);
2344
2414
  }
2345
2415
  function dispose() {
2346
- window.removeEventListener("popstate", onPopState);
2416
+ window.removeEventListener("popstate", syncFromLocation);
2417
+ window.removeEventListener("hashchange", syncFromLocation);
2347
2418
  }
2348
2419
  return { params, hash, setParams, setHash, dispose };
2349
2420
  }
@@ -3007,7 +3078,7 @@ function persisted(key, initial, options = {}) {
3007
3078
  }
3008
3079
  const [value, setValue] = signal(restored);
3009
3080
  let applyingFromStorage = false;
3010
- effect(() => {
3081
+ const stopPersistEffect = effect(() => {
3011
3082
  const current = value();
3012
3083
  if (applyingFromStorage) return;
3013
3084
  try {
@@ -3047,6 +3118,7 @@ function persisted(key, initial, options = {}) {
3047
3118
  window.addEventListener("storage", storageListener);
3048
3119
  }
3049
3120
  const dispose = () => {
3121
+ stopPersistEffect();
3050
3122
  if (storageListener && typeof window !== "undefined") {
3051
3123
  window.removeEventListener("storage", storageListener);
3052
3124
  storageListener = null;
@@ -3064,27 +3136,53 @@ function persisted(key, initial, options = {}) {
3064
3136
  // src/patterns/optimistic.ts
3065
3137
  function optimistic(initialValue) {
3066
3138
  const [value, setValue] = signal(initialValue);
3067
- const [_pending, setPending] = signal(false);
3068
- async function addOptimistic(optimisticValue, asyncAction) {
3139
+ const [pending, setPending] = signal(false);
3140
+ let inflightCount = 0;
3141
+ let version = 0;
3142
+ async function update(optimisticValue, asyncAction) {
3143
+ const myVersion = ++version;
3069
3144
  const previousValue = value();
3070
3145
  setValue(optimisticValue);
3146
+ inflightCount++;
3071
3147
  setPending(true);
3072
3148
  try {
3073
3149
  const result = await asyncAction();
3074
- setValue(result);
3150
+ if (version === myVersion) {
3151
+ setValue(result);
3152
+ }
3075
3153
  } catch {
3076
- setValue(previousValue);
3154
+ if (version === myVersion) {
3155
+ setValue(previousValue);
3156
+ }
3077
3157
  } finally {
3078
- setPending(false);
3158
+ inflightCount--;
3159
+ if (inflightCount === 0) setPending(false);
3079
3160
  }
3080
3161
  }
3081
- return [value, addOptimistic];
3162
+ return { value, pending, update };
3082
3163
  }
3083
3164
  function optimisticList(initialValue) {
3084
3165
  const [items, setItems] = signal([...initialValue]);
3085
- async function addOptimistic(item, asyncAction) {
3166
+ const [pending, setPending] = signal(false);
3167
+ let inflightCount = 0;
3168
+ let version = 0;
3169
+ function begin() {
3170
+ const v = ++version;
3171
+ inflightCount++;
3172
+ setPending(true);
3173
+ return v;
3174
+ }
3175
+ function end(myVersion, revertFn) {
3176
+ if (revertFn && version === myVersion) {
3177
+ revertFn();
3178
+ }
3179
+ inflightCount--;
3180
+ if (inflightCount === 0) setPending(false);
3181
+ }
3182
+ async function add(item, asyncAction) {
3086
3183
  const prev = items();
3087
3184
  setItems([...prev, item]);
3185
+ const myVersion = begin();
3088
3186
  try {
3089
3187
  const result = await asyncAction();
3090
3188
  setItems((current) => {
@@ -3094,32 +3192,56 @@ function optimisticList(initialValue) {
3094
3192
  next[idx] = result;
3095
3193
  return next;
3096
3194
  }
3097
- return [...current.filter((i2) => i2 !== item), result];
3195
+ return [...current, result];
3098
3196
  });
3197
+ end(myVersion);
3099
3198
  } catch {
3100
- setItems(prev);
3199
+ end(myVersion, () => setItems(prev));
3101
3200
  }
3102
3201
  }
3103
- async function removeOptimistic(predicate, asyncAction) {
3202
+ async function remove(predicate, asyncAction) {
3104
3203
  const prev = items();
3105
3204
  setItems(prev.filter((item) => !predicate(item)));
3205
+ const myVersion = begin();
3106
3206
  try {
3107
3207
  await asyncAction();
3208
+ end(myVersion);
3108
3209
  } catch {
3109
- setItems(prev);
3210
+ end(myVersion, () => setItems(prev));
3110
3211
  }
3111
3212
  }
3112
- async function updateOptimistic(predicate, update, asyncAction) {
3213
+ async function updateItem(predicate, patch, asyncAction) {
3113
3214
  const prev = items();
3114
- setItems(prev.map((item) => predicate(item) ? { ...item, ...update } : item));
3215
+ const patchedRefs = [];
3216
+ setItems(
3217
+ prev.map((item) => {
3218
+ if (predicate(item)) {
3219
+ const patched = { ...item, ...patch };
3220
+ patchedRefs.push(patched);
3221
+ return patched;
3222
+ }
3223
+ return item;
3224
+ })
3225
+ );
3226
+ const myVersion = begin();
3115
3227
  try {
3116
3228
  const result = await asyncAction();
3117
- setItems((current) => current.map((item) => predicate(item) ? result : item));
3229
+ setItems((current) => current.map((item) => patchedRefs.includes(item) ? result : item));
3230
+ end(myVersion);
3118
3231
  } catch {
3119
- setItems(prev);
3232
+ end(myVersion, () => setItems(prev));
3120
3233
  }
3121
3234
  }
3122
- return { items, addOptimistic, removeOptimistic, updateOptimistic };
3235
+ return {
3236
+ items,
3237
+ pending,
3238
+ add,
3239
+ remove,
3240
+ update: updateItem,
3241
+ addOptimistic: add,
3242
+ removeOptimistic: remove,
3243
+ updateOptimistic: updateItem
3244
+ };
3123
3245
  }
3124
3246
 
3125
3247
  // src/patterns/timeTravel.ts
@@ -3171,7 +3293,7 @@ function timeline(initial, maxHistory = 100) {
3171
3293
 
3172
3294
  // src/patterns/globalStore.ts
3173
3295
  function globalStore(config) {
3174
- const initialState = { ...config.state };
3296
+ const initialState = JSON.parse(JSON.stringify(config.state));
3175
3297
  const [getState, setState] = signal({ ...initialState });
3176
3298
  const listeners = /* @__PURE__ */ new Set();
3177
3299
  const middlewares = config.middleware || [];
@@ -3400,20 +3522,29 @@ function transition(element, options = {}) {
3400
3522
  onLeaveDone
3401
3523
  } = options;
3402
3524
  const transitionValue = `${property} ${duration}ms ${easing} ${delay}ms`;
3525
+ let activeTimer = null;
3526
+ function cancelPending() {
3527
+ if (activeTimer !== null) {
3528
+ clearTimeout(activeTimer);
3529
+ activeTimer = null;
3530
+ }
3531
+ }
3403
3532
  function enter() {
3404
3533
  return new Promise((resolve) => {
3534
+ cancelPending();
3405
3535
  element.style.transition = transitionValue;
3406
3536
  if (enterClass) element.classList.add(enterClass);
3407
3537
  if (leaveClass) element.classList.remove(leaveClass);
3408
3538
  void element.offsetHeight;
3409
3539
  if (activeClass) element.classList.add(activeClass);
3410
3540
  const done = () => {
3541
+ activeTimer = null;
3411
3542
  if (enterClass) element.classList.remove(enterClass);
3412
3543
  onEnterDone?.();
3413
3544
  resolve();
3414
3545
  };
3415
3546
  if (duration > 0) {
3416
- setTimeout(done, duration + delay);
3547
+ activeTimer = setTimeout(done, duration + delay);
3417
3548
  } else {
3418
3549
  done();
3419
3550
  }
@@ -3421,17 +3552,19 @@ function transition(element, options = {}) {
3421
3552
  }
3422
3553
  function leave() {
3423
3554
  return new Promise((resolve) => {
3555
+ cancelPending();
3424
3556
  element.style.transition = transitionValue;
3425
3557
  if (activeClass) element.classList.remove(activeClass);
3426
3558
  if (leaveClass) element.classList.add(leaveClass);
3427
3559
  if (enterClass) element.classList.remove(enterClass);
3428
3560
  const done = () => {
3561
+ activeTimer = null;
3429
3562
  if (leaveClass) element.classList.remove(leaveClass);
3430
3563
  onLeaveDone?.();
3431
3564
  resolve();
3432
3565
  };
3433
3566
  if (duration > 0) {
3434
- setTimeout(done, duration + delay);
3567
+ activeTimer = setTimeout(done, duration + delay);
3435
3568
  } else {
3436
3569
  done();
3437
3570
  }
@@ -3834,7 +3967,7 @@ function bindField(field, extras) {
3834
3967
  blur: () => field.touch()
3835
3968
  };
3836
3969
  const { on: extraOn, value: _ignoreValue, ...restExtras } = extras ?? {};
3837
- const mergedOn = extraOn && typeof extraOn === "object" ? { ...fieldOn, ...extraOn } : fieldOn;
3970
+ const mergedOn = extraOn && typeof extraOn === "object" ? { ...extraOn, ...fieldOn } : fieldOn;
3838
3971
  return {
3839
3972
  value: field.value,
3840
3973
  on: mergedOn,
@@ -3905,14 +4038,23 @@ function form(config) {
3905
4038
  }
3906
4039
  return result;
3907
4040
  });
4041
+ const [submitting, setSubmitting] = signal(false);
3908
4042
  function handleSubmit(onSubmit) {
3909
4043
  return (e) => {
3910
4044
  if (e) e.preventDefault();
4045
+ if (submitting()) return;
3911
4046
  for (const field of Object.values(fieldMap)) {
3912
4047
  field.touch();
3913
4048
  }
3914
4049
  if (isValid()) {
3915
- onSubmit(values());
4050
+ const result = onSubmit(values());
4051
+ if (result && typeof result.then === "function") {
4052
+ setSubmitting(true);
4053
+ result.then(
4054
+ () => setSubmitting(false),
4055
+ () => setSubmitting(false)
4056
+ );
4057
+ }
3916
4058
  }
3917
4059
  };
3918
4060
  }
@@ -3929,6 +4071,7 @@ function form(config) {
3929
4071
  errors,
3930
4072
  isValid,
3931
4073
  isDirty,
4074
+ submitting,
3932
4075
  touched: touchedState,
3933
4076
  values,
3934
4077
  handleSubmit,
@@ -3937,6 +4080,20 @@ function form(config) {
3937
4080
  };
3938
4081
  }
3939
4082
 
4083
+ // src/core/rendering/dispose.ts
4084
+ var elementDisposers = /* @__PURE__ */ new WeakMap();
4085
+ var _isDev4 = isDev();
4086
+ var activeBindingCount = 0;
4087
+ function registerDisposer(node, teardown) {
4088
+ let disposers = elementDisposers.get(node);
4089
+ if (!disposers) {
4090
+ disposers = [];
4091
+ elementDisposers.set(node, disposers);
4092
+ }
4093
+ disposers.push(teardown);
4094
+ if (_isDev4) activeBindingCount++;
4095
+ }
4096
+
3940
4097
  // src/ui/virtualList.ts
3941
4098
  function VirtualList(props) {
3942
4099
  const overscan = props.overscan ?? 3;
@@ -3954,9 +4111,9 @@ function VirtualList(props) {
3954
4111
  content.style.right = "0";
3955
4112
  spacer.appendChild(content);
3956
4113
  container.appendChild(spacer);
3957
- container.addEventListener("scroll", () => {
3958
- setScrollTop(container.scrollTop);
3959
- });
4114
+ const onScroll = () => setScrollTop(container.scrollTop);
4115
+ container.addEventListener("scroll", onScroll);
4116
+ registerDisposer(container, () => container.removeEventListener("scroll", onScroll));
3960
4117
  const update = () => {
3961
4118
  const items = props.items();
3962
4119
  const totalHeight = items.length * props.itemHeight;
@@ -4064,13 +4221,52 @@ function inputMask(options) {
4064
4221
  }
4065
4222
  return raw;
4066
4223
  }
4224
+ function isSlot(c) {
4225
+ return c === "9" || c === "A" || c === "*";
4226
+ }
4227
+ function buildStripRegex() {
4228
+ const hasDigit = options.pattern.includes("9");
4229
+ const hasLetter = options.pattern.includes("A");
4230
+ const hasAny = options.pattern.includes("*");
4231
+ if (hasAny) {
4232
+ const literals = /* @__PURE__ */ new Set();
4233
+ for (const c of options.pattern) {
4234
+ if (!isSlot(c)) literals.add(c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
4235
+ }
4236
+ return literals.size > 0 ? new RegExp(`[${Array.from(literals).join("")}]`, "g") : /(?!)/g;
4237
+ }
4238
+ if (hasDigit && hasLetter) return /[^a-zA-Z0-9]/g;
4239
+ if (hasDigit) return /[^0-9]/g;
4240
+ if (hasLetter) return /[^a-zA-Z]/g;
4241
+ return /[^a-zA-Z0-9]/g;
4242
+ }
4243
+ const stripRegex = buildStripRegex();
4244
+ const rawCharTest = options.pattern.includes("*") ? () => true : (c) => /[a-zA-Z0-9]/.test(c);
4067
4245
  function bind(input2) {
4068
4246
  input2.addEventListener("input", () => {
4069
- const raw = input2.value.replace(/[^a-zA-Z0-9]/g, "");
4247
+ const cursorBefore = input2.selectionStart ?? input2.value.length;
4248
+ const oldValue = input2.value;
4249
+ const raw = oldValue.replace(stripRegex, "");
4070
4250
  const masked = applyMask(raw);
4071
4251
  setValue(masked);
4072
4252
  setRawValue(extractRaw(masked));
4073
4253
  input2.value = masked;
4254
+ let rawBefore = 0;
4255
+ for (let i2 = 0; i2 < cursorBefore && i2 < oldValue.length; i2++) {
4256
+ if (rawCharTest(oldValue[i2])) rawBefore++;
4257
+ }
4258
+ let newCursor = 0;
4259
+ let counted = 0;
4260
+ for (; newCursor < masked.length; newCursor++) {
4261
+ if (newCursor < options.pattern.length && isSlot(options.pattern[newCursor])) {
4262
+ counted++;
4263
+ if (counted >= rawBefore) {
4264
+ newCursor++;
4265
+ break;
4266
+ }
4267
+ }
4268
+ }
4269
+ input2.setSelectionRange(newCursor, newCursor);
4074
4270
  });
4075
4271
  input2.addEventListener("focus", () => {
4076
4272
  if (!input2.value) {
@@ -4140,7 +4336,10 @@ function FocusTrap(nodes, options = {}) {
4140
4336
  const focusable = container.querySelectorAll(
4141
4337
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
4142
4338
  );
4143
- if (focusable.length === 0) return;
4339
+ if (focusable.length === 0) {
4340
+ e.preventDefault();
4341
+ return;
4342
+ }
4144
4343
  const first = focusable[0];
4145
4344
  const last = focusable[focusable.length - 1];
4146
4345
  if (e.shiftKey) {
@@ -4163,24 +4362,27 @@ function FocusTrap(nodes, options = {}) {
4163
4362
  first?.focus();
4164
4363
  });
4165
4364
  }
4365
+ let trapObserver = null;
4366
+ function restoreFocusAndCleanup() {
4367
+ if (options.restoreFocus !== false) previouslyFocused?.focus();
4368
+ if (trapObserver) {
4369
+ trapObserver.disconnect();
4370
+ trapObserver = null;
4371
+ }
4372
+ }
4166
4373
  if (options.restoreFocus !== false) {
4167
- const observer = new MutationObserver((mutations) => {
4168
- for (const mutation2 of mutations) {
4169
- for (const removed of Array.from(mutation2.removedNodes)) {
4170
- if (removed === container || removed.contains(container)) {
4171
- previouslyFocused?.focus();
4172
- observer.disconnect();
4173
- return;
4174
- }
4175
- }
4374
+ trapObserver = new MutationObserver(() => {
4375
+ if (!container.isConnected) {
4376
+ restoreFocusAndCleanup();
4176
4377
  }
4177
4378
  });
4178
4379
  queueMicrotask(() => {
4179
- if (container.parentNode) {
4180
- observer.observe(container.parentNode, { childList: true });
4380
+ if (container.isConnected) {
4381
+ trapObserver.observe(document.body, { childList: true, subtree: true });
4181
4382
  }
4182
4383
  });
4183
4384
  }
4385
+ registerDisposer(container, restoreFocusAndCleanup);
4184
4386
  return container;
4185
4387
  }
4186
4388
  function hotkey(combo, handler, options = {}) {
@@ -4266,6 +4468,10 @@ function scopedStyle(css) {
4266
4468
  if (trimmed.startsWith("@") || trimmed.startsWith("from") || trimmed.startsWith("to") || /^\d+%$/.test(trimmed)) {
4267
4469
  return match;
4268
4470
  }
4471
+ const pseudoIdx = trimmed.indexOf("::");
4472
+ if (pseudoIdx >= 0) {
4473
+ return `${trimmed.slice(0, pseudoIdx)}[${attr}]${trimmed.slice(pseudoIdx)}${delimiter}`;
4474
+ }
4269
4475
  return `${trimmed}[${attr}]${delimiter}`;
4270
4476
  });
4271
4477
  if (typeof document !== "undefined") {
@@ -4301,7 +4507,7 @@ function removeScopedStyle(scopeId) {
4301
4507
  }
4302
4508
 
4303
4509
  // src/reactivity/bindAttribute.ts
4304
- var _isDev4 = isDev();
4510
+ var _isDev5 = isDev();
4305
4511
  function isEventHandlerAttr(name) {
4306
4512
  if (name.length < 3) return false;
4307
4513
  const lower = name.toLowerCase();
@@ -4309,7 +4515,7 @@ function isEventHandlerAttr(name) {
4309
4515
  }
4310
4516
  function bindAttribute(el, attr, getter) {
4311
4517
  if (isEventHandlerAttr(attr)) {
4312
- if (_isDev4)
4518
+ if (_isDev5)
4313
4519
  devWarn(
4314
4520
  `bindAttribute: refusing to bind event-handler attribute "${attr}". Use on:{ ${attr.slice(2)}: fn } instead.`
4315
4521
  );
@@ -4321,7 +4527,7 @@ function bindAttribute(el, attr, getter) {
4321
4527
  try {
4322
4528
  value = getter();
4323
4529
  } catch (err) {
4324
- if (_isDev4)
4530
+ if (_isDev5)
4325
4531
  devWarn(`bindAttribute: getter for "${attr}" threw: ${err instanceof Error ? err.message : String(err)}`);
4326
4532
  return;
4327
4533
  }
@@ -4415,28 +4621,35 @@ function dialog() {
4415
4621
  close();
4416
4622
  }
4417
4623
  }
4418
- function open() {
4419
- setIsOpen(true);
4624
+ function attachListener() {
4420
4625
  if (typeof window !== "undefined" && !listenerAttached) {
4421
4626
  window.addEventListener("keydown", handleKeydown);
4422
4627
  listenerAttached = true;
4423
4628
  }
4424
4629
  }
4425
- function close() {
4426
- setIsOpen(false);
4630
+ function detachListener() {
4427
4631
  if (typeof window !== "undefined" && listenerAttached) {
4428
4632
  window.removeEventListener("keydown", handleKeydown);
4429
4633
  listenerAttached = false;
4430
4634
  }
4431
4635
  }
4636
+ function open() {
4637
+ setIsOpen(true);
4638
+ attachListener();
4639
+ }
4640
+ function close() {
4641
+ setIsOpen(false);
4642
+ detachListener();
4643
+ }
4432
4644
  function toggle() {
4433
- if (isOpen()) {
4434
- close();
4435
- } else {
4436
- open();
4437
- }
4645
+ if (isOpen()) close();
4646
+ else open();
4438
4647
  }
4439
- return { open, close, isOpen, toggle };
4648
+ function dispose() {
4649
+ detachListener();
4650
+ setIsOpen(false);
4651
+ }
4652
+ return { open, close, isOpen, toggle, dispose };
4440
4653
  }
4441
4654
 
4442
4655
  // src/ui/toast.ts
@@ -4757,15 +4970,12 @@ function startTransition(callback) {
4757
4970
  }
4758
4971
  function deferredValue(getter) {
4759
4972
  const [deferred, setDeferred] = signal(getter());
4760
- const sync = () => {
4761
- const current = getter();
4762
- setDeferred(current);
4763
- };
4764
- scheduleUpdate(Priority.LOW, sync);
4765
- return () => {
4766
- scheduleUpdate(Priority.LOW, sync);
4767
- return deferred();
4768
- };
4973
+ let latest = deferred();
4974
+ effect(() => {
4975
+ latest = getter();
4976
+ scheduleUpdate(Priority.LOW, () => setDeferred(latest));
4977
+ });
4978
+ return deferred;
4769
4979
  }
4770
4980
  function transitionState() {
4771
4981
  const [isPending, setIsPending] = signal(false);
@@ -5368,7 +5578,7 @@ function setCanonical(url) {
5368
5578
  }
5369
5579
 
5370
5580
  // src/platform/ssr.ts
5371
- var _isDev5 = isDev();
5581
+ var _isDev6 = isDev();
5372
5582
  var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
5373
5583
  function isSafeAttrName(name) {
5374
5584
  return SAFE_ATTR_NAME.test(name);
@@ -5393,7 +5603,7 @@ var URL_ATTRS = /* @__PURE__ */ new Set([
5393
5603
  "xlink:href"
5394
5604
  ]);
5395
5605
  function ssrErrorComment(err) {
5396
- if (_isDev5) {
5606
+ if (_isDev6) {
5397
5607
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
5398
5608
  return `<!--SSR error: ${safeCommentText(msg)}-->`;
5399
5609
  }
@@ -5439,10 +5649,10 @@ function renderToString(element) {
5439
5649
  }
5440
5650
  const tag = element.tagName.toLowerCase();
5441
5651
  if (tag === "script" || tag === "style") {
5442
- return _isDev5 ? `<!--ssr:${tag}-stripped-->` : "";
5652
+ return _isDev6 ? `<!--ssr:${tag}-stripped-->` : "";
5443
5653
  }
5444
5654
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
5445
- return _isDev5 ? "<!--ssr:invalid-tag-->" : "";
5655
+ return _isDev6 ? "<!--ssr:invalid-tag-->" : "";
5446
5656
  }
5447
5657
  let html2 = `<${tag}`;
5448
5658
  for (const attr of Array.from(element.attributes)) {
@@ -5483,7 +5693,7 @@ function hydrate(component, container, options = {}) {
5483
5693
  const first = mismatches[0];
5484
5694
  if (options.onMismatch) {
5485
5695
  options.onMismatch(first);
5486
- } else if (_isDev5) {
5696
+ } else if (_isDev6) {
5487
5697
  console.warn(
5488
5698
  `[Sibu hydration] ${first.message}
5489
5699
  at ${first.path}
@@ -5681,11 +5891,11 @@ async function* renderToStream(element) {
5681
5891
  }
5682
5892
  const tag = element.tagName.toLowerCase();
5683
5893
  if (tag === "script" || tag === "style") {
5684
- if (_isDev5) yield `<!--ssr:${tag}-stripped-->`;
5894
+ if (_isDev6) yield `<!--ssr:${tag}-stripped-->`;
5685
5895
  return;
5686
5896
  }
5687
5897
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
5688
- if (_isDev5) yield "<!--ssr:invalid-tag-->";
5898
+ if (_isDev6) yield "<!--ssr:invalid-tag-->";
5689
5899
  return;
5690
5900
  }
5691
5901
  let openTag = `<${tag}`;
@@ -6187,7 +6397,7 @@ function isWasmCached(key) {
6187
6397
  }
6188
6398
 
6189
6399
  // src/reactivity/bindChildNode.ts
6190
- var _isDev6 = isDev();
6400
+ var _isDev7 = isDev();
6191
6401
  function bindChildNode(placeholder, getter) {
6192
6402
  let lastNodes = [];
6193
6403
  function commit() {
@@ -6195,7 +6405,7 @@ function bindChildNode(placeholder, getter) {
6195
6405
  try {
6196
6406
  result = getter();
6197
6407
  } catch (err) {
6198
- if (_isDev6) devWarn(`bindChildNode: getter threw: ${err instanceof Error ? err.message : String(err)}`);
6408
+ if (_isDev7) devWarn(`bindChildNode: getter threw: ${err instanceof Error ? err.message : String(err)}`);
6199
6409
  return;
6200
6410
  }
6201
6411
  if (result == null || typeof result === "boolean") {
@@ -6255,20 +6465,6 @@ function bindChildNode(placeholder, getter) {
6255
6465
  return track(commit);
6256
6466
  }
6257
6467
 
6258
- // src/core/rendering/dispose.ts
6259
- var elementDisposers = /* @__PURE__ */ new WeakMap();
6260
- var _isDev7 = isDev();
6261
- var activeBindingCount = 0;
6262
- function registerDisposer(node, teardown) {
6263
- let disposers = elementDisposers.get(node);
6264
- if (!disposers) {
6265
- disposers = [];
6266
- elementDisposers.set(node, disposers);
6267
- }
6268
- disposers.push(teardown);
6269
- if (_isDev7) activeBindingCount++;
6270
- }
6271
-
6272
6468
  // src/core/rendering/tagFactory.ts
6273
6469
  var SVG_NS = "http://www.w3.org/2000/svg";
6274
6470
  var kebabCache = /* @__PURE__ */ new Map();