sibujs 4.4.0 → 4.6.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 (112) hide show
  1. package/README.md +11 -0
  2. package/dist/browser.cjs +339 -130
  3. package/dist/browser.d.cts +61 -16
  4. package/dist/browser.d.ts +61 -16
  5. package/dist/browser.js +8 -6
  6. package/dist/build.cjs +273 -82
  7. package/dist/build.js +15 -16
  8. package/dist/cdn.dev.global.js +12 -12
  9. package/dist/cdn.full.dev.global.js +11 -11
  10. package/dist/cdn.full.global.js +10 -10
  11. package/dist/cdn.global.js +12 -12
  12. package/dist/{chunk-IXKSNWV5.js → chunk-2INLLLMZ.js} +1 -1
  13. package/dist/chunk-3ISI6ACU.js +44 -0
  14. package/dist/{chunk-VCTAEPSB.js → chunk-47M47FOM.js} +11 -3
  15. package/dist/chunk-5HZXGZ6T.js +24 -0
  16. package/dist/{chunk-KKLW7YWL.js → chunk-5MT6SJ3P.js} +275 -144
  17. package/dist/{chunk-3BTTCZ5J.js → chunk-5Q4R7HCL.js} +42 -4
  18. package/dist/{chunk-ADM46X22.js → chunk-7GQCWFOE.js} +157 -61
  19. package/dist/{chunk-4Z3SPQ67.js → chunk-7NCARGJW.js} +16 -8
  20. package/dist/{chunk-5DXA2J44.js → chunk-7XHATCIH.js} +5 -2
  21. package/dist/{chunk-LQFGQNMV.js → chunk-AIF3Z2T7.js} +302 -39
  22. package/dist/{chunk-UCALUKB5.js → chunk-BAAG6ZTI.js} +98 -29
  23. package/dist/{chunk-NVNJH22U.js → chunk-D33YTSX5.js} +2 -2
  24. package/dist/{chunk-M2F7TZHH.js → chunk-DKGBBKOF.js} +136 -80
  25. package/dist/{chunk-NIOYEGBQ.js → chunk-HURREPU2.js} +27 -13
  26. package/dist/{chunk-HFCOH2GN.js → chunk-HYXDKS4N.js} +153 -110
  27. package/dist/chunk-J6FW5TV6.js +233 -0
  28. package/dist/{chunk-36S2YPP4.js → chunk-JSXPZCET.js} +1 -1
  29. package/dist/chunk-NYNYSPK7.js +318 -0
  30. package/dist/{chunk-HQSEH5F6.js → chunk-ORMZXBKQ.js} +108 -45
  31. package/dist/{chunk-LYVUX7NT.js → chunk-OXUY2A6L.js} +287 -130
  32. package/dist/{chunk-6GSIXTWX.js → chunk-PK6FK2G2.js} +187 -66
  33. package/dist/{chunk-W2EQ7X2L.js → chunk-QE4TTDU3.js} +47 -9
  34. package/dist/{chunk-XLO7SLIX.js → chunk-SXXVZMKZ.js} +262 -70
  35. package/dist/{chunk-R25EFXXC.js → chunk-TUCPL2HB.js} +3 -3
  36. package/dist/{chunk-RR7M3FHM.js → chunk-UZQ6ALFS.js} +3 -3
  37. package/dist/{chunk-PNIRUQ4C.js → chunk-VVWPJ543.js} +6 -8
  38. package/dist/{chunk-7GIHSAWB.js → chunk-XC4MEKGA.js} +3 -3
  39. package/dist/{chunk-SKPTERHP.js → chunk-XRRZKZYX.js} +55 -31
  40. package/dist/{contracts-DBdg9J_a.d.cts → contracts-DRIuclVT.d.cts} +10 -2
  41. package/dist/{contracts-DBdg9J_a.d.ts → contracts-DRIuclVT.d.ts} +10 -2
  42. package/dist/{customElement-CNZxEB9G.d.ts → customElement-MmInOW1U.d.cts} +59 -10
  43. package/dist/{customElement-CNZxEB9G.d.cts → customElement-MmInOW1U.d.ts} +59 -10
  44. package/dist/data.cjs +293 -125
  45. package/dist/data.d.cts +47 -9
  46. package/dist/data.d.ts +47 -9
  47. package/dist/data.js +12 -9
  48. package/dist/devtools.cjs +121 -58
  49. package/dist/devtools.js +7 -8
  50. package/dist/dispose-GEIG2KOF.js +28 -0
  51. package/dist/ecosystem.cjs +391 -112
  52. package/dist/ecosystem.d.cts +31 -7
  53. package/dist/ecosystem.d.ts +31 -7
  54. package/dist/ecosystem.js +12 -12
  55. package/dist/extras.cjs +2250 -879
  56. package/dist/extras.d.cts +10 -9
  57. package/dist/extras.d.ts +10 -9
  58. package/dist/extras.js +39 -28
  59. package/dist/index.cjs +273 -82
  60. package/dist/index.d.cts +44 -154
  61. package/dist/index.d.ts +44 -154
  62. package/dist/index.js +24 -27
  63. package/dist/motion.cjs +137 -42
  64. package/dist/motion.js +5 -5
  65. package/dist/patterns.cjs +397 -48
  66. package/dist/patterns.d.cts +32 -9
  67. package/dist/patterns.d.ts +32 -9
  68. package/dist/patterns.js +8 -8
  69. package/dist/performance.cjs +338 -217
  70. package/dist/performance.d.cts +2 -2
  71. package/dist/performance.d.ts +2 -2
  72. package/dist/performance.js +8 -9
  73. package/dist/plugin-DVgSnTfK.d.cts +112 -0
  74. package/dist/plugin-DVgSnTfK.d.ts +112 -0
  75. package/dist/plugins.cjs +587 -203
  76. package/dist/plugins.d.cts +3 -3
  77. package/dist/plugins.d.ts +3 -3
  78. package/dist/plugins.js +35 -23
  79. package/dist/signal-EotCj4hS.d.cts +110 -0
  80. package/dist/signal-EotCj4hS.d.ts +110 -0
  81. package/dist/{ssr-BiPRdZ6n.d.cts → ssr-Bli9XRW5.d.cts} +5 -0
  82. package/dist/{ssr-BiPRdZ6n.d.ts → ssr-Bli9XRW5.d.ts} +5 -0
  83. package/dist/{ssr-Y7XOEPEN.js → ssr-XOTUASDO.js} +4 -5
  84. package/dist/ssr.cjs +214 -73
  85. package/dist/ssr.d.cts +9 -3
  86. package/dist/ssr.d.ts +9 -3
  87. package/dist/ssr.js +11 -12
  88. package/dist/{startup-BMpaiMhP.d.ts → startup-BLfSeL15.d.cts} +73 -22
  89. package/dist/{startup-BMpaiMhP.d.cts → startup-BLfSeL15.d.ts} +73 -22
  90. package/dist/tagFactory-DFstCLQV.d.cts +117 -0
  91. package/dist/tagFactory-DkaNVUNV.d.ts +117 -0
  92. package/dist/testing.cjs +2481 -2153
  93. package/dist/testing.d.cts +56 -5
  94. package/dist/testing.d.ts +56 -5
  95. package/dist/testing.js +580 -307
  96. package/dist/ui.cjs +798 -328
  97. package/dist/ui.d.cts +40 -7
  98. package/dist/ui.d.ts +40 -7
  99. package/dist/ui.js +151 -56
  100. package/dist/widgets.cjs +319 -285
  101. package/dist/widgets.js +9 -10
  102. package/package.json +2 -2
  103. package/dist/chunk-2WLZ6757.js +0 -149
  104. package/dist/chunk-7NBDXVHS.js +0 -60
  105. package/dist/chunk-CCSJMTRN.js +0 -15
  106. package/dist/chunk-QKRPLZ2V.js +0 -108
  107. package/dist/chunk-WWV3SJ3L.js +0 -131
  108. package/dist/dispose-46BOMMQJ.js +0 -19
  109. package/dist/plugin-D30wlGW5.d.cts +0 -71
  110. package/dist/plugin-D30wlGW5.d.ts +0 -71
  111. package/dist/tagFactory-Bzupt4Pj.d.cts +0 -55
  112. package/dist/tagFactory-Bzupt4Pj.d.ts +0 -55
package/dist/ui.cjs CHANGED
@@ -85,28 +85,6 @@ __export(ui_exports, {
85
85
  });
86
86
  module.exports = __toCommonJS(ui_exports);
87
87
 
88
- // src/core/dev.ts
89
- var DEV = typeof __SIBU_DEV__ !== "undefined" ? !!__SIBU_DEV__ : typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
90
- // safe default: off in browser, on in test/dev Node
91
- typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
92
- );
93
- function devAssert(condition, message) {
94
- if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
95
- throw new Error(`[SibuJS] ${message}`);
96
- }
97
- }
98
- function devWarnLazy(build) {
99
- if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
100
- const message = build();
101
- if (message) console.warn(`[SibuJS] ${message}`);
102
- }
103
- }
104
- function devWarn(message) {
105
- if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
106
- console.warn(`[SibuJS] ${message}`);
107
- }
108
- }
109
-
110
88
  // src/core/errors.ts
111
89
  var ERROR_STATE_KEY = /* @__PURE__ */ Symbol.for("sibujs.runtime-errors.v1");
112
90
  function state() {
@@ -195,6 +173,28 @@ function reportError(error, context) {
195
173
  });
196
174
  }
197
175
 
176
+ // src/core/dev.ts
177
+ var DEV = typeof __SIBU_DEV__ !== "undefined" ? !!__SIBU_DEV__ : typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
178
+ // safe default: off in browser, on in test/dev Node
179
+ typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
180
+ );
181
+ function devAssert(condition, message) {
182
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
183
+ throw new Error(`[SibuJS] ${message}`);
184
+ }
185
+ }
186
+ function devWarnLazy(build) {
187
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
188
+ const message = build();
189
+ if (message) console.warn(`[SibuJS] ${message}`);
190
+ }
191
+ }
192
+ function devWarn(message) {
193
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
194
+ console.warn(`[SibuJS] ${message}`);
195
+ }
196
+ }
197
+
198
198
  // src/core/rendering/dispose.ts
199
199
  var elementDisposers = /* @__PURE__ */ new WeakMap();
200
200
  var MAX_DRAIN_TEARDOWNS = 1e4;
@@ -206,6 +206,23 @@ function reportDrainRunaway(label, executed, remaining) {
206
206
  );
207
207
  }
208
208
  var activeBindingCount = 0;
209
+ function resolveCapture(capture) {
210
+ let frame = capture;
211
+ while (frame?.closed) frame = frame.forwardTo;
212
+ return frame ?? null;
213
+ }
214
+ var registrationCaptures = [];
215
+ function currentDisposerCapture() {
216
+ return resolveCapture(registrationCaptures[registrationCaptures.length - 1]);
217
+ }
218
+ function beginDisposerCapture(capture) {
219
+ if (capture === null && registrationCaptures.length === 0) return false;
220
+ registrationCaptures.push(resolveCapture(capture));
221
+ return true;
222
+ }
223
+ function endDisposerCapture() {
224
+ registrationCaptures.pop();
225
+ }
209
226
  function registerDisposer(node, teardown) {
210
227
  let disposers = elementDisposers.get(node);
211
228
  if (!disposers) {
@@ -214,6 +231,61 @@ function registerDisposer(node, teardown) {
214
231
  }
215
232
  disposers.push(teardown);
216
233
  if (DEV) activeBindingCount++;
234
+ const capture = resolveCapture(registrationCaptures[registrationCaptures.length - 1]);
235
+ if (capture) capture.entries.push([node, teardown]);
236
+ }
237
+ function withDisposerRollback(build) {
238
+ const frame = { entries: [], closed: false };
239
+ const captured = frame.entries;
240
+ registrationCaptures.push(frame);
241
+ let result;
242
+ try {
243
+ result = build();
244
+ } catch (err) {
245
+ let executed = 0;
246
+ try {
247
+ while (captured.length > 0) {
248
+ if (executed >= MAX_DRAIN_TEARDOWNS) {
249
+ reportDrainRunaway("rollback", executed, captured.length);
250
+ break;
251
+ }
252
+ const [node, teardown] = captured.pop();
253
+ if (!unregisterDisposer(node, teardown)) continue;
254
+ executed++;
255
+ try {
256
+ teardown();
257
+ } catch (cleanupErr) {
258
+ reportError(cleanupErr, { phase: "cleanup", name: "disposer" });
259
+ }
260
+ }
261
+ } finally {
262
+ frame.closed = true;
263
+ captured.length = 0;
264
+ registrationCaptures.pop();
265
+ }
266
+ throw err;
267
+ }
268
+ frame.closed = true;
269
+ registrationCaptures.pop();
270
+ const parent = resolveCapture(registrationCaptures[registrationCaptures.length - 1]);
271
+ if (parent) {
272
+ for (const entry of captured) {
273
+ if (elementDisposers.get(entry[0])?.includes(entry[1])) parent.entries.push(entry);
274
+ }
275
+ frame.forwardTo = parent;
276
+ }
277
+ captured.length = 0;
278
+ return result;
279
+ }
280
+ function unregisterDisposer(node, teardown) {
281
+ const disposers = elementDisposers.get(node);
282
+ if (!disposers) return false;
283
+ const index = disposers.indexOf(teardown);
284
+ if (index === -1) return false;
285
+ disposers.splice(index, 1);
286
+ if (DEV) activeBindingCount--;
287
+ if (disposers.length === 0) elementDisposers.delete(node);
288
+ return true;
217
289
  }
218
290
  function dispose(node) {
219
291
  const stack = [node];
@@ -227,33 +299,34 @@ function dispose(node) {
227
299
  }
228
300
  }
229
301
  for (let i = order.length - 1; i >= 0; i--) {
230
- const current = order[i];
231
- if (elementDisposers.has(current)) {
232
- let executed = 0;
233
- let runaway = false;
234
- while (!runaway) {
235
- const pending = elementDisposers.get(current);
236
- if (!pending || pending.length === 0) break;
237
- const snapshot = pending.slice();
238
- elementDisposers.delete(current);
239
- if (DEV) activeBindingCount -= snapshot.length;
240
- for (let i2 = 0; i2 < snapshot.length; i2++) {
241
- if (executed >= MAX_DRAIN_TEARDOWNS) {
242
- const rest = snapshot.slice(i2);
243
- const added = elementDisposers.get(current);
244
- elementDisposers.set(current, added ? rest.concat(added) : rest);
245
- if (DEV) activeBindingCount += rest.length;
246
- reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
247
- runaway = true;
248
- break;
249
- }
250
- executed++;
251
- try {
252
- snapshot[i2]();
253
- } catch (err) {
254
- reportError(err, { phase: "cleanup", name: "disposer" });
255
- }
256
- }
302
+ disposeNodeOwn(order[i]);
303
+ }
304
+ }
305
+ function disposeNodeOwn(node) {
306
+ if (!elementDisposers.has(node)) return;
307
+ let executed = 0;
308
+ let runaway = false;
309
+ while (!runaway) {
310
+ const pending = elementDisposers.get(node);
311
+ if (!pending || pending.length === 0) break;
312
+ const snapshot = pending.slice();
313
+ elementDisposers.delete(node);
314
+ if (DEV) activeBindingCount -= snapshot.length;
315
+ for (let i = 0; i < snapshot.length; i++) {
316
+ if (executed >= MAX_DRAIN_TEARDOWNS) {
317
+ const rest = snapshot.slice(i);
318
+ const added = elementDisposers.get(node);
319
+ elementDisposers.set(node, added ? rest.concat(added) : rest);
320
+ if (DEV) activeBindingCount += rest.length;
321
+ reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
322
+ runaway = true;
323
+ break;
324
+ }
325
+ executed++;
326
+ try {
327
+ snapshot[i]();
328
+ } catch (err) {
329
+ reportError(err, { phase: "cleanup", name: "disposer" });
257
330
  }
258
331
  }
259
332
  }
@@ -417,8 +490,18 @@ function untracked(fn) {
417
490
  var subscriberEpochCounter = 0;
418
491
  function retrack(effectFn, subscriber) {
419
492
  const prev = currentSubscriber;
493
+ let savedDepth = 0;
494
+ let savedSuspendSub = null;
495
+ if (trackingSuspended) {
496
+ savedDepth = suspendDepth;
497
+ savedSuspendSub = suspendSavedSub;
498
+ suspendDepth = 0;
499
+ suspendSavedSub = null;
500
+ trackingSuspended = false;
501
+ }
420
502
  currentSubscriber = subscriber;
421
503
  const sub = subscriber;
504
+ if (sub._cap === void 0) sub._cap = currentDisposerCapture();
422
505
  const epoch = ++subscriberEpochCounter;
423
506
  sub._epoch = epoch;
424
507
  sub._structDirty = false;
@@ -431,6 +514,11 @@ function retrack(effectFn, subscriber) {
431
514
  effectFn();
432
515
  } finally {
433
516
  currentSubscriber = prev;
517
+ if (savedDepth !== 0) {
518
+ suspendDepth = savedDepth;
519
+ suspendSavedSub = savedSuspendSub;
520
+ trackingSuspended = true;
521
+ }
434
522
  let node = sub.depsHead ?? null;
435
523
  while (node !== null) {
436
524
  const next = node.subNext;
@@ -450,11 +538,25 @@ function track(effectFn, subscriber) {
450
538
  if (!subscriber) return reactiveBinding(effectFn);
451
539
  cleanup(subscriber);
452
540
  const prev = currentSubscriber;
541
+ let savedDepth = 0;
542
+ let savedSuspendSub = null;
543
+ if (trackingSuspended) {
544
+ savedDepth = suspendDepth;
545
+ savedSuspendSub = suspendSavedSub;
546
+ suspendDepth = 0;
547
+ suspendSavedSub = null;
548
+ trackingSuspended = false;
549
+ }
453
550
  currentSubscriber = subscriber;
454
551
  try {
455
552
  effectFn();
456
553
  } finally {
457
554
  currentSubscriber = prev;
555
+ if (savedDepth !== 0) {
556
+ suspendDepth = savedDepth;
557
+ suspendSavedSub = savedSuspendSub;
558
+ trackingSuspended = true;
559
+ }
458
560
  const sub2 = subscriber;
459
561
  for (let n = sub2.depsHead ?? null; n !== null; n = n.subNext) {
460
562
  const sig = n.sig;
@@ -591,7 +693,12 @@ function drainQueue() {
591
693
  quarantined.add(sub);
592
694
  continue;
593
695
  }
594
- safeInvoke(sub);
696
+ const pushed = beginDisposerCapture(sub._cap ?? null);
697
+ try {
698
+ safeInvoke(sub);
699
+ } finally {
700
+ if (pushed) endDisposerCapture();
701
+ }
595
702
  }
596
703
  }
597
704
  function drainNotificationQueue() {
@@ -625,8 +732,9 @@ function propagateDirty(sub) {
625
732
  if (s._c) {
626
733
  const nSig = s._sig;
627
734
  if (nSig) {
628
- if (!nSig._d) {
735
+ if (!nSig._d || nSig._f === true) {
629
736
  nSig._d = true;
737
+ nSig._f = false;
630
738
  stack.push(nSig);
631
739
  }
632
740
  } else {
@@ -726,7 +834,7 @@ function forEachSubscriber(signal2, visit) {
726
834
  }
727
835
 
728
836
  // src/reactivity/track.ts
729
- var _runtimeVersion = true ? "4.4.0" : "dev";
837
+ var _runtimeVersion = true ? "4.6.0" : "dev";
730
838
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
731
839
  function resolveReactiveApi() {
732
840
  const g = globalThis;
@@ -790,10 +898,14 @@ function derived(getter, options) {
790
898
  const cs = {};
791
899
  cs._d = false;
792
900
  cs._init = false;
901
+ cs._f = false;
793
902
  cs._g = getter;
794
903
  cs.__v = 0;
904
+ let evaluating = false;
905
+ let disposed = false;
906
+ let pendingError;
795
907
  const markDirty = () => {
796
- if (cs._d) return;
908
+ if (cs._d || disposed) return;
797
909
  cs._d = true;
798
910
  };
799
911
  markDirty._c = 1;
@@ -816,33 +928,53 @@ function derived(getter, options) {
816
928
  }
817
929
  }, markDirty);
818
930
  const hook = globalThis.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
819
- let evaluating = false;
820
931
  const validate = () => {
821
- if (!cs._d) return;
932
+ if (!cs._d || disposed || pendingError !== void 0) return;
822
933
  const oldValue = cs._v;
823
934
  evaluating = true;
824
935
  try {
825
936
  retrack2(recompute, markDirty);
826
937
  if (!Object.is(oldValue, cs._v)) cs.__v++;
938
+ } catch (err) {
939
+ pendingError = { error: err };
940
+ cs.__v++;
941
+ if (disposed) cs._d = true;
942
+ else cs._f = true;
827
943
  } finally {
828
944
  evaluating = false;
945
+ if (disposed) cleanup2(markDirty);
829
946
  }
830
- if (hook && !Object.is(oldValue, cs._v)) {
947
+ if (hook && !disposed && !Object.is(oldValue, cs._v)) {
831
948
  hook.emit("computed:update", { signal: cs, oldValue, newValue: cs._v });
832
949
  }
833
950
  };
834
951
  cs._validate = validate;
952
+ const throwPending = () => {
953
+ const { error } = pendingError;
954
+ pendingError = void 0;
955
+ if (disposed) cs._d = false;
956
+ throw error;
957
+ };
835
958
  function computedGetter() {
836
959
  if (evaluating) {
837
960
  throw new Error(
838
961
  `[SibuJS] Circular dependency detected in derived${debugName ? ` "${debugName}"` : ""}. A derived signal cannot read itself (directly or through a chain).`
839
962
  );
840
963
  }
841
- if (isTrackingSuspended2()) {
842
- if (cs._d) validate();
964
+ if (isTrackingSuspended2() || disposed) {
965
+ if (cs._d) {
966
+ validate();
967
+ if (pendingError !== void 0) throwPending();
968
+ }
843
969
  return cs._v;
844
970
  }
845
- if (cs._d) validate();
971
+ if (cs._d) {
972
+ validate();
973
+ if (pendingError !== void 0) {
974
+ recordDependency2(cs);
975
+ throwPending();
976
+ }
977
+ }
846
978
  recordDependency2(cs);
847
979
  return cs._v;
848
980
  }
@@ -851,171 +983,21 @@ function derived(getter, options) {
851
983
  cs.__name = debugName;
852
984
  }
853
985
  computedGetter.__signal = cs;
854
- if (hook) hook.emit("computed:create", { signal: cs, name: debugName, getter: computedGetter });
855
- return computedGetter;
856
- }
857
-
858
- // src/core/ssr-context.ts
859
- var SSR_KEY = /* @__PURE__ */ Symbol.for("sibujs.ssr.v1");
860
- function detectSSRShared() {
861
- let detected = null;
862
- try {
863
- if (typeof process !== "undefined" && process.versions && process.versions.node) {
864
- let mod = null;
865
- const getBuiltin = process.getBuiltinModule;
866
- if (typeof getBuiltin === "function") {
867
- mod = getBuiltin("node:async_hooks");
868
- } else if (typeof require === "function") {
869
- mod = require("async_hooks");
986
+ computedGetter.dispose = () => {
987
+ if (disposed) return;
988
+ disposed = true;
989
+ cs._d = pendingError !== void 0;
990
+ cleanup2(markDirty);
991
+ const h = globalThis.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
992
+ if (h) {
993
+ try {
994
+ h.emit("computed:destroy", { signal: cs, getter: computedGetter });
995
+ } catch {
870
996
  }
871
- if (mod) detected = new mod.AsyncLocalStorage();
872
- }
873
- } catch {
874
- detected = null;
875
- }
876
- return {
877
- als: detected,
878
- fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
879
- fallbackDepth: 0
880
- };
881
- }
882
- var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
883
- var als = _shared.als;
884
- var fallbackStore = _shared.fallbackStore;
885
- _shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
886
- function getSSRStore() {
887
- if (als) {
888
- const s = als.getStore();
889
- if (s) return s;
890
- }
891
- return fallbackStore;
892
- }
893
- function isSSR() {
894
- return getSSRStore().ssr;
895
- }
896
-
897
- // src/core/signals/effect.ts
898
- var _g = globalThis;
899
- var MAX_RERUNS = 100;
900
- function flushUserCleanups(ctx) {
901
- const list = ctx.userCleanups;
902
- if (list.length === 0) return;
903
- ctx.userCleanups = [];
904
- for (let i = list.length - 1; i >= 0; i--) {
905
- try {
906
- list[i]();
907
- } catch (err) {
908
- reportError(err, { phase: "cleanup" });
909
997
  }
910
- }
911
- }
912
- function drainReruns(ctx) {
913
- let reruns = 1;
914
- do {
915
- ctx.rerunPending = false;
916
- if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
917
- retrack2(ctx.bodyFn, ctx.subscriber);
918
- } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
919
- if (ctx.rerunPending) {
920
- ctx.rerunPending = false;
921
- reportError(
922
- new Error(
923
- `effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
924
- ),
925
- { phase: "effect" }
926
- );
927
- }
928
- }
929
- function disposeEffect(ctx) {
930
- if (ctx.disposed) return;
931
- ctx.disposed = true;
932
- const h = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
933
- if (h) {
934
- try {
935
- h.emit("effect:destroy", { effectFn: ctx.fn });
936
- } catch {
937
- }
938
- }
939
- try {
940
- if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
941
- } catch (err) {
942
- if (typeof console !== "undefined") {
943
- console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
944
- }
945
- }
946
- try {
947
- cleanup2(ctx.subscriber);
948
- } catch (err) {
949
- if (typeof console !== "undefined") {
950
- console.warn("[SibuJS effect] dispose threw:", err);
951
- }
952
- }
953
- }
954
- function effect(effectFn, options) {
955
- devAssert(typeof effectFn === "function", "effect: argument must be a function.");
956
- if (isSSR()) return () => {
957
998
  };
958
- const ctx = {
959
- fn: effectFn,
960
- onError: options?.onError,
961
- userCleanups: [],
962
- running: false,
963
- rerunPending: false,
964
- disposed: false,
965
- onCleanup: null,
966
- subscriber: null,
967
- bodyFn: null
968
- };
969
- ctx.onCleanup = (fn) => {
970
- ctx.userCleanups.push(fn);
971
- };
972
- const onErrorCaptured = ctx.onError;
973
- ctx.bodyFn = onErrorCaptured ? () => {
974
- try {
975
- ctx.fn(ctx.onCleanup);
976
- } catch (err) {
977
- onErrorCaptured(err);
978
- }
979
- } : () => {
980
- ctx.fn(ctx.onCleanup);
981
- };
982
- const sub = (() => {
983
- if (ctx.disposed) return;
984
- if (ctx.running) {
985
- ctx.rerunPending = true;
986
- return;
987
- }
988
- ctx.running = true;
989
- try {
990
- ctx.rerunPending = false;
991
- if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
992
- retrack2(ctx.bodyFn, sub);
993
- if (ctx.rerunPending) drainReruns(ctx);
994
- } finally {
995
- ctx.running = false;
996
- ctx.rerunPending = false;
997
- }
998
- });
999
- sub.depsHead = null;
1000
- sub.depsTail = null;
1001
- sub._epoch = 0;
1002
- sub._structDirty = false;
1003
- sub._hasComputedDep = false;
1004
- sub._errorPhase = "effect";
1005
- sub._runEpoch = 0;
1006
- sub._runs = 0;
1007
- ctx.subscriber = sub;
1008
- ctx.running = true;
1009
- try {
1010
- retrack2(ctx.bodyFn, ctx.subscriber);
1011
- if (ctx.rerunPending) drainReruns(ctx);
1012
- } finally {
1013
- ctx.running = false;
1014
- ctx.rerunPending = false;
1015
- }
1016
- const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1017
- if (hook) hook.emit("effect:create", { effectFn });
1018
- return () => disposeEffect(ctx);
999
+ if (hook) hook.emit("computed:create", { signal: cs, name: debugName, getter: computedGetter });
1000
+ return computedGetter;
1019
1001
  }
1020
1002
 
1021
1003
  // src/reactivity/batch.ts
@@ -1069,7 +1051,7 @@ var enqueueBatchedSignal = API2.enqueueBatchedSignal;
1069
1051
  var isBatching = API2.isBatching;
1070
1052
 
1071
1053
  // src/core/signals/signal.ts
1072
- var _g2 = globalThis;
1054
+ var _g = globalThis;
1073
1055
  function signal(initial, options) {
1074
1056
  const state2 = {
1075
1057
  value: initial,
@@ -1100,7 +1082,7 @@ function signal(initial, options) {
1100
1082
  state2.value = newValue;
1101
1083
  state2.__v++;
1102
1084
  if (DEV) {
1103
- const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1085
+ const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1104
1086
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1105
1087
  }
1106
1088
  if (!enqueueBatchedSignal(state2)) {
@@ -1114,7 +1096,7 @@ function signal(initial, options) {
1114
1096
  if (Object.is(newValue, prev)) return;
1115
1097
  state2.value = newValue;
1116
1098
  state2.__v++;
1117
- const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1099
+ const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1118
1100
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1119
1101
  if (!enqueueBatchedSignal(state2)) {
1120
1102
  notifySubscribers2(state2);
@@ -1132,11 +1114,82 @@ function signal(initial, options) {
1132
1114
  }
1133
1115
  };
1134
1116
  }
1135
- if (DEV) {
1136
- const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1137
- if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
1117
+ if (DEV) {
1118
+ const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1119
+ if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
1120
+ }
1121
+ return [get, set];
1122
+ }
1123
+
1124
+ // src/core/ssr-context.ts
1125
+ var SSR_KEY = /* @__PURE__ */ Symbol.for("sibujs.ssr.v1");
1126
+ function detectSSRShared() {
1127
+ let detected = null;
1128
+ try {
1129
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
1130
+ let mod = null;
1131
+ const getBuiltin = process.getBuiltinModule;
1132
+ if (typeof getBuiltin === "function") {
1133
+ mod = getBuiltin("node:async_hooks");
1134
+ } else if (typeof require === "function") {
1135
+ mod = require("async_hooks");
1136
+ }
1137
+ if (mod) detected = new mod.AsyncLocalStorage();
1138
+ }
1139
+ } catch {
1140
+ detected = null;
1141
+ }
1142
+ return {
1143
+ als: detected,
1144
+ fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
1145
+ fallbackDepth: 0
1146
+ };
1147
+ }
1148
+ var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
1149
+ var als = _shared.als;
1150
+ var fallbackStore = _shared.fallbackStore;
1151
+ _shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
1152
+ function getSSRStore() {
1153
+ if (als) {
1154
+ const s = als.getStore();
1155
+ if (s) return s;
1156
+ }
1157
+ return fallbackStore;
1158
+ }
1159
+ function getRequestStore() {
1160
+ if (als) return als.getStore() ?? null;
1161
+ return _shared.fallbackDepth > 0 ? fallbackStore : null;
1162
+ }
1163
+ function isSSR() {
1164
+ return getSSRStore().ssr;
1165
+ }
1166
+
1167
+ // src/reactivity/domBinding.ts
1168
+ function domBinding(commit, ownerNode) {
1169
+ if (isSSR()) return () => {
1170
+ };
1171
+ return reactiveBinding2(commit, ownerNode);
1172
+ }
1173
+
1174
+ // src/utils/adoptThenable.ts
1175
+ function adoptThenable(value) {
1176
+ if (value === null || typeof value !== "object" && typeof value !== "function") return null;
1177
+ let then;
1178
+ try {
1179
+ then = value.then;
1180
+ } catch (error) {
1181
+ return Promise.reject(error);
1138
1182
  }
1139
- return [get, set];
1183
+ if (typeof then !== "function") return null;
1184
+ return new Promise((resolve, reject) => {
1185
+ queueMicrotask(() => {
1186
+ try {
1187
+ Reflect.apply(then, value, [resolve, reject]);
1188
+ } catch (error) {
1189
+ reject(error);
1190
+ }
1191
+ });
1192
+ });
1140
1193
  }
1141
1194
 
1142
1195
  // src/ui/form.ts
@@ -1166,10 +1219,14 @@ function maxLength(max2, message) {
1166
1219
  }
1167
1220
  function matchesPattern(regex, message = "Invalid format") {
1168
1221
  return (value) => {
1169
- if (value && !regex.test(value)) {
1170
- return message;
1222
+ if (!value) return null;
1223
+ const savedLastIndex = regex.lastIndex;
1224
+ regex.lastIndex = 0;
1225
+ try {
1226
+ return regex.test(value) ? null : message;
1227
+ } finally {
1228
+ regex.lastIndex = savedLastIndex;
1171
1229
  }
1172
- return null;
1173
1230
  };
1174
1231
  }
1175
1232
  function email(message = "Invalid email address") {
@@ -1218,13 +1275,13 @@ function bindField(field, extras) {
1218
1275
  const mergedOn = extraOn && typeof extraOn === "object" ? { ...extraOn, ...fieldOn } : fieldOn;
1219
1276
  const onElement = (el) => {
1220
1277
  if (el instanceof HTMLSelectElement && el.multiple) {
1221
- const stop = effect(() => {
1278
+ const stop = domBinding(() => {
1222
1279
  const v = field.value();
1223
1280
  const selected = Array.isArray(v) ? v.map(String) : [];
1224
1281
  for (const opt of Array.from(el.options)) {
1225
1282
  opt.selected = selected.includes(opt.value);
1226
1283
  }
1227
- });
1284
+ }, el);
1228
1285
  registerDisposer(el, stop);
1229
1286
  }
1230
1287
  if (typeof extraOnElement === "function") {
@@ -1244,6 +1301,7 @@ function form(config) {
1244
1301
  const fieldEntries = Object.entries(config);
1245
1302
  const fieldMap = {};
1246
1303
  const [manualErrors, setManualErrors] = signal({});
1304
+ const fieldErrors = [];
1247
1305
  for (const [name, cfg] of fieldEntries) {
1248
1306
  const [value, setValue] = signal(cfg.initial);
1249
1307
  const [isTouched, setTouched] = signal(false);
@@ -1258,6 +1316,7 @@ function form(config) {
1258
1316
  }
1259
1317
  return null;
1260
1318
  });
1319
+ fieldErrors.push(error);
1261
1320
  const wrappedSet = (next) => {
1262
1321
  setValue(next);
1263
1322
  setManualErrors((prev) => {
@@ -1334,12 +1393,22 @@ function form(config) {
1334
1393
  field.touch();
1335
1394
  }
1336
1395
  if (isValid()) {
1337
- const result = onSubmit(values());
1338
- if (result && typeof result.then === "function") {
1396
+ const report = (error) => reportError(error, { phase: "async", name: "form.handleSubmit" });
1397
+ let pending;
1398
+ try {
1399
+ pending = adoptThenable(onSubmit(values()));
1400
+ } catch (error) {
1401
+ report(error);
1402
+ return;
1403
+ }
1404
+ if (pending) {
1339
1405
  setSubmitting(true);
1340
- result.then(
1406
+ pending.then(
1341
1407
  () => setSubmitting(false),
1342
- () => setSubmitting(false)
1408
+ (error) => {
1409
+ setSubmitting(false);
1410
+ report(error);
1411
+ }
1343
1412
  );
1344
1413
  }
1345
1414
  }
@@ -1353,6 +1422,17 @@ function form(config) {
1353
1422
  function setError(field, message) {
1354
1423
  setManualErrors((prev) => ({ ...prev, [field]: message }));
1355
1424
  }
1425
+ let disposed = false;
1426
+ function dispose2() {
1427
+ if (disposed) return;
1428
+ disposed = true;
1429
+ errors.dispose();
1430
+ isValid.dispose();
1431
+ isDirty.dispose();
1432
+ touchedState.dispose();
1433
+ values.dispose();
1434
+ for (const error of fieldErrors) error.dispose();
1435
+ }
1356
1436
  return {
1357
1437
  fields: fieldMap,
1358
1438
  errors,
@@ -1363,7 +1443,8 @@ function form(config) {
1363
1443
  values,
1364
1444
  handleSubmit,
1365
1445
  reset,
1366
- setError
1446
+ setError,
1447
+ dispose: dispose2
1367
1448
  };
1368
1449
  }
1369
1450
 
@@ -1442,7 +1523,7 @@ function VirtualList(props) {
1442
1523
  content.appendChild(itemEl);
1443
1524
  }
1444
1525
  };
1445
- registerDisposer(container, effect(update));
1526
+ registerDisposer(container, domBinding(update, container));
1446
1527
  return container;
1447
1528
  }
1448
1529
 
@@ -1452,11 +1533,14 @@ function intersection(options) {
1452
1533
  const [ratio, setRatio] = signal(0);
1453
1534
  let observer = null;
1454
1535
  let currentElement = null;
1536
+ let generation = 0;
1455
1537
  function observe(element) {
1456
1538
  if (typeof IntersectionObserver === "undefined") return;
1457
1539
  unobserve();
1458
1540
  currentElement = element;
1541
+ const observation = ++generation;
1459
1542
  observer = new IntersectionObserver((entries) => {
1543
+ if (observation !== generation) return;
1460
1544
  const entry = entries[0];
1461
1545
  if (entry) {
1462
1546
  setIsIntersecting(entry.isIntersecting);
@@ -1466,8 +1550,10 @@ function intersection(options) {
1466
1550
  observer.observe(element);
1467
1551
  }
1468
1552
  function unobserve() {
1553
+ generation++;
1469
1554
  if (observer) {
1470
1555
  if (currentElement) observer.unobserve(currentElement);
1556
+ observer.takeRecords();
1471
1557
  observer.disconnect();
1472
1558
  observer = null;
1473
1559
  currentElement = null;
@@ -1486,17 +1572,26 @@ function lazyLoad(element, loader, options) {
1486
1572
  return () => {
1487
1573
  };
1488
1574
  }
1575
+ let done = false;
1576
+ const stop = () => {
1577
+ done = true;
1578
+ observer.takeRecords();
1579
+ observer.disconnect();
1580
+ };
1489
1581
  const observer = new IntersectionObserver((entries) => {
1582
+ if (done) return;
1490
1583
  for (const entry of entries) {
1491
1584
  if (entry.isIntersecting) {
1585
+ stop();
1492
1586
  loader();
1493
- observer.disconnect();
1494
1587
  break;
1495
1588
  }
1496
1589
  }
1497
1590
  }, options);
1498
1591
  observer.observe(element);
1499
- return () => observer.disconnect();
1592
+ return () => {
1593
+ if (!done) stop();
1594
+ };
1500
1595
  }
1501
1596
 
1502
1597
  // src/ui/inputMask.ts
@@ -1651,6 +1746,7 @@ function focus() {
1651
1746
  const dispose2 = () => {
1652
1747
  if (disposed) return;
1653
1748
  disposed = true;
1749
+ unregisterDisposer(element, dispose2);
1654
1750
  element.removeEventListener("focus", onFocus);
1655
1751
  element.removeEventListener("blur", onBlur);
1656
1752
  if (currentElement === element) currentElement = null;
@@ -1719,14 +1815,19 @@ function FocusTrap(nodes, options = {}) {
1719
1815
  }
1720
1816
  };
1721
1817
  container.addEventListener("keydown", onTrapKeydown);
1818
+ let disposed = false;
1722
1819
  if (options.autoFocus !== false) {
1723
1820
  queueMicrotask(() => {
1821
+ if (disposed || !container.isConnected) return;
1724
1822
  const first = getFocusable()[0];
1725
1823
  first?.focus();
1726
1824
  });
1727
1825
  }
1728
1826
  let trapObserver = null;
1729
1827
  function restoreFocusAndCleanup() {
1828
+ if (disposed) return;
1829
+ disposed = true;
1830
+ unregisterDisposer(container, restoreFocusAndCleanup);
1730
1831
  if (options.restoreFocus !== false) previouslyFocused?.focus();
1731
1832
  container.removeEventListener("keydown", onTrapKeydown);
1732
1833
  if (trapObserver) {
@@ -1741,29 +1842,45 @@ function FocusTrap(nodes, options = {}) {
1741
1842
  }
1742
1843
  });
1743
1844
  queueMicrotask(() => {
1744
- if (container.isConnected) {
1745
- trapObserver.observe(container, { childList: true, subtree: true });
1746
- }
1845
+ const observer = trapObserver;
1846
+ if (disposed || !container.isConnected || !observer) return;
1847
+ observer.observe(container, { childList: true, subtree: true });
1747
1848
  });
1748
1849
  }
1749
1850
  registerDisposer(container, restoreFocusAndCleanup);
1750
1851
  return container;
1751
1852
  }
1853
+ function isApplePlatform() {
1854
+ if (typeof navigator === "undefined") return false;
1855
+ const nav = navigator;
1856
+ return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || "");
1857
+ }
1752
1858
  function hotkey(combo, handler, options = {}) {
1753
1859
  let key = combo;
1754
1860
  let needCtrl = options.ctrl ?? false;
1755
1861
  let needShift = options.shift ?? false;
1756
1862
  let needAlt = options.alt ?? false;
1757
1863
  let needMeta = options.meta ?? false;
1758
- if (combo.includes("+")) {
1759
- const parts = combo.toLowerCase().split("+");
1864
+ if (combo === "+") {
1865
+ key = "+";
1866
+ } else if (combo.includes("+")) {
1867
+ const literalPlus = combo.endsWith("++");
1868
+ const parts = (literalPlus ? combo.slice(0, -2) : combo).toLowerCase().split("+");
1869
+ if (literalPlus) parts.push("+");
1760
1870
  key = parts[parts.length - 1];
1871
+ if (key === "") throw new Error(`hotkey("${combo}"): missing key`);
1761
1872
  for (let i = 0; i < parts.length - 1; i++) {
1762
1873
  const mod = parts[i];
1763
1874
  if (mod === "ctrl" || mod === "control") needCtrl = true;
1764
1875
  else if (mod === "shift") needShift = true;
1765
- else if (mod === "alt") needAlt = true;
1876
+ else if (mod === "alt" || mod === "option") needAlt = true;
1766
1877
  else if (mod === "meta" || mod === "cmd" || mod === "command") needMeta = true;
1878
+ else if (mod === "mod") {
1879
+ if (isApplePlatform()) needMeta = true;
1880
+ else needCtrl = true;
1881
+ } else {
1882
+ throw new Error(`hotkey("${combo}"): unknown modifier "${mod}"`);
1883
+ }
1767
1884
  }
1768
1885
  }
1769
1886
  const listener = (e) => {
@@ -1829,6 +1946,11 @@ function announce(message, priority = "polite") {
1829
1946
  var COUNTER_KEY = /* @__PURE__ */ Symbol.for("sibujs.createId.v1");
1830
1947
  var _counter = globalThis[COUNTER_KEY] ?? (globalThis[COUNTER_KEY] = { n: 0 });
1831
1948
  function createId(prefix = "sibu") {
1949
+ const request = getRequestStore();
1950
+ if (request) {
1951
+ request.idCounter = (request.idCounter ?? 0) + 1;
1952
+ return `${prefix}-${request.idCounter}`;
1953
+ }
1832
1954
  _counter.n++;
1833
1955
  return `${prefix}-${_counter.n}`;
1834
1956
  }
@@ -1888,22 +2010,43 @@ function createListbox(container, options = {}) {
1888
2010
  if (multiple) container.setAttribute("aria-multiselectable", "true");
1889
2011
  if (!container.hasAttribute("tabindex")) container.setAttribute("tabindex", "0");
1890
2012
  const [activeValue, setActiveValue] = signal(null);
2013
+ const [selectedValues, setSelectedValues] = signal([]);
1891
2014
  const [selectedValue, setSelectedValue] = signal(null);
1892
2015
  const [activeDescendantId, setActiveDescendantId] = signal(null);
1893
- function stampIds() {
2016
+ let activeValueRef = null;
2017
+ let currentSelection = [];
2018
+ function reconcileOptions() {
1894
2019
  const opts = Array.from(container.querySelectorAll(optionSelector));
2020
+ const selectedSet = new Set(currentSelection);
2021
+ let activeStillPresent = false;
2022
+ const active = activeValueRef;
1895
2023
  for (const opt of opts) {
1896
2024
  if (!opt.id) opt.id = createId("listbox-option");
1897
- if (!opt.hasAttribute("aria-selected")) opt.setAttribute("aria-selected", "false");
2025
+ const ov = opt.dataset.value;
2026
+ if (!opt.hasAttribute("aria-selected")) {
2027
+ opt.setAttribute("aria-selected", ov !== void 0 && selectedSet.has(ov) ? "true" : "false");
2028
+ }
2029
+ if (active !== null && ov === active) activeStillPresent = true;
1898
2030
  }
2031
+ if (active !== null && !activeStillPresent) setActive(null, opts);
2032
+ return opts;
1899
2033
  }
1900
- stampIds();
1901
2034
  function getOptions() {
1902
- return Array.from(container.querySelectorAll(optionSelector));
2035
+ return reconcileOptions();
2036
+ }
2037
+ function isDisabled(opt) {
2038
+ return opt.getAttribute("aria-disabled") === "true" || opt.hasAttribute("disabled");
2039
+ }
2040
+ function enabledOptions() {
2041
+ return getOptions().filter((opt) => !isDisabled(opt) && opt.dataset.value !== void 0);
2042
+ }
2043
+ function optionByValue(value) {
2044
+ return getOptions().find((opt) => opt.dataset.value === value);
1903
2045
  }
1904
- function setActive(value) {
2046
+ function setActive(value, known) {
2047
+ activeValueRef = value;
1905
2048
  setActiveValue(value);
1906
- const opts = getOptions();
2049
+ const opts = known ?? getOptions();
1907
2050
  for (const opt of opts) {
1908
2051
  if (opt.dataset.value === value) {
1909
2052
  opt.setAttribute("data-highlighted", "");
@@ -1919,29 +2062,33 @@ function createListbox(container, options = {}) {
1919
2062
  }
1920
2063
  }
1921
2064
  function select(value) {
1922
- const previous = selectedValue();
1923
- let nextSelectedSet;
2065
+ const target = optionByValue(value);
2066
+ if (!target || isDisabled(target)) return;
2067
+ const previous = selectedValues();
2068
+ let next;
1924
2069
  if (multiple) {
1925
- nextSelectedSet = new Set((previous ?? "").split(",").filter(Boolean));
1926
- if (nextSelectedSet.has(value)) nextSelectedSet.delete(value);
1927
- else nextSelectedSet.add(value);
1928
- setSelectedValue(Array.from(nextSelectedSet).join(","));
2070
+ next = previous.includes(value) ? previous.filter((v) => v !== value) : [...previous, value];
1929
2071
  } else {
1930
- nextSelectedSet = /* @__PURE__ */ new Set([value]);
1931
- setSelectedValue(value);
2072
+ next = [value];
1932
2073
  }
2074
+ currentSelection = next;
2075
+ batch(() => {
2076
+ setSelectedValues(next);
2077
+ setSelectedValue(multiple ? next.join(",") : value);
2078
+ });
1933
2079
  options.onSelect?.(value);
2080
+ const nextSelectedSet = new Set(next);
1934
2081
  const opts = getOptions();
1935
2082
  for (const opt of opts) {
1936
- const ov = opt.dataset.value ?? "";
1937
- opt.setAttribute("aria-selected", nextSelectedSet.has(ov) ? "true" : "false");
2083
+ const ov = opt.dataset.value;
2084
+ opt.setAttribute("aria-selected", ov !== void 0 && nextSelectedSet.has(ov) ? "true" : "false");
1938
2085
  }
1939
2086
  }
1940
2087
  function moveActive(delta) {
1941
- const opts = getOptions();
2088
+ const opts = enabledOptions();
1942
2089
  if (opts.length === 0) return;
1943
- const currentIdx = opts.findIndex((o) => o.dataset.value === activeValue());
1944
- let next = currentIdx + delta;
2090
+ const currentIdx = opts.findIndex((o) => o.dataset.value === activeValueRef);
2091
+ let next = currentIdx === -1 ? delta > 0 ? 0 : opts.length - 1 : currentIdx + delta;
1945
2092
  if (next < 0) next = opts.length - 1;
1946
2093
  if (next >= opts.length) next = 0;
1947
2094
  const nextValue = opts[next].dataset.value ?? null;
@@ -1962,13 +2109,13 @@ function createListbox(container, options = {}) {
1962
2109
  break;
1963
2110
  case "Home": {
1964
2111
  e.preventDefault();
1965
- const opts = getOptions();
2112
+ const opts = enabledOptions();
1966
2113
  if (opts.length > 0) setActive(opts[0].dataset.value ?? null);
1967
2114
  break;
1968
2115
  }
1969
2116
  case "End": {
1970
2117
  e.preventDefault();
1971
- const opts = getOptions();
2118
+ const opts = enabledOptions();
1972
2119
  if (opts.length > 0) setActive(opts[opts.length - 1].dataset.value ?? null);
1973
2120
  break;
1974
2121
  }
@@ -1983,7 +2130,7 @@ function createListbox(container, options = {}) {
1983
2130
  }
1984
2131
  function onClick(e) {
1985
2132
  const target = e.target.closest(optionSelector);
1986
- if (!target || !container.contains(target)) return;
2133
+ if (!target || !container.contains(target) || isDisabled(target)) return;
1987
2134
  const value = target.dataset.value ?? null;
1988
2135
  if (value !== null) {
1989
2136
  setActive(value);
@@ -1992,12 +2139,26 @@ function createListbox(container, options = {}) {
1992
2139
  }
1993
2140
  container.addEventListener("keydown", onKeyDown);
1994
2141
  container.addEventListener("click", onClick);
2142
+ reconcileOptions();
2143
+ let observer = null;
2144
+ if (typeof MutationObserver !== "undefined") {
2145
+ observer = new MutationObserver(() => {
2146
+ if (!disposed) reconcileOptions();
2147
+ });
2148
+ observer.observe(container, { childList: true, subtree: true });
2149
+ }
2150
+ let disposed = false;
1995
2151
  function dispose2() {
2152
+ if (disposed) return;
2153
+ disposed = true;
2154
+ unregisterDisposer(container, dispose2);
2155
+ observer?.disconnect();
2156
+ observer = null;
1996
2157
  container.removeEventListener("keydown", onKeyDown);
1997
2158
  container.removeEventListener("click", onClick);
1998
2159
  }
1999
2160
  registerDisposer(container, dispose2);
2000
- return { activeValue, selectedValue, activeDescendantId, dispose: dispose2 };
2161
+ return { activeValue, selectedValues, selectedValue, activeDescendantId, refresh: reconcileOptions, dispose: dispose2 };
2001
2162
  }
2002
2163
  function createDialogAria(element, options = {}) {
2003
2164
  const titleId = options.labelledBy ?? createId("dialog-title");
@@ -2488,6 +2649,14 @@ function setSafeAttribute(el, name, value, options = {}) {
2488
2649
  const html = isHtmlElement(el);
2489
2650
  const idlName = html ? name.toLowerCase() : name;
2490
2651
  if (typeof value === "boolean") {
2652
+ if (name.length > 5 && name.slice(0, 5).toLowerCase() === "aria-") {
2653
+ const token = value ? "true" : "false";
2654
+ if ((ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name)) !== token) {
2655
+ if (ns) el.setAttributeNS(ns, name, token);
2656
+ else el.setAttribute(name, token);
2657
+ }
2658
+ return true;
2659
+ }
2491
2660
  if (html && BOOLEAN_IDL_ATTRS.has(idlName) && idlName in el) {
2492
2661
  setProp(el, idlName, value);
2493
2662
  } else if (value) {
@@ -2561,13 +2730,13 @@ function bindBoolAttr(el, attr, getter) {
2561
2730
  let value;
2562
2731
  try {
2563
2732
  value = reactiveGetter();
2564
- } catch {
2733
+ } catch (err) {
2734
+ reportError(err, { phase: "binding", name: "bindBoolAttr", node: el });
2565
2735
  return;
2566
2736
  }
2567
2737
  setSafeAttribute(el, attr, Boolean(value), { label: "bindBoolAttr" });
2568
2738
  }
2569
- const teardown = track2(commit);
2570
- return teardown;
2739
+ return reactiveBinding2(commit, el);
2571
2740
  }
2572
2741
  function bindData(el, key, getter) {
2573
2742
  const dataAttr = `data-${key}`;
@@ -2612,6 +2781,8 @@ function removeGlobalListenerIfIdle() {
2612
2781
  function dialog() {
2613
2782
  const [isOpen, setIsOpen] = signal(false);
2614
2783
  const entry = { close: () => close() };
2784
+ let opened = false;
2785
+ let disposed = false;
2615
2786
  function pushOnStack() {
2616
2787
  if (dialogStack.indexOf(entry) !== -1) return;
2617
2788
  dialogStack.push(entry);
@@ -2623,23 +2794,27 @@ function dialog() {
2623
2794
  removeGlobalListenerIfIdle();
2624
2795
  }
2625
2796
  function open() {
2626
- if (isOpen()) return;
2627
- setIsOpen(true);
2797
+ if (disposed || opened) return;
2798
+ opened = true;
2628
2799
  pushOnStack();
2800
+ setIsOpen(true);
2629
2801
  }
2630
2802
  function close() {
2631
- if (!isOpen()) {
2803
+ if (!opened) {
2632
2804
  removeFromStack();
2633
2805
  return;
2634
2806
  }
2635
- setIsOpen(false);
2807
+ opened = false;
2636
2808
  removeFromStack();
2809
+ setIsOpen(false);
2637
2810
  }
2638
2811
  function toggle() {
2639
- if (isOpen()) close();
2812
+ if (opened) close();
2640
2813
  else open();
2641
2814
  }
2642
2815
  function dispose2() {
2816
+ disposed = true;
2817
+ opened = false;
2643
2818
  removeFromStack();
2644
2819
  setIsOpen(false);
2645
2820
  }
@@ -2831,44 +3006,191 @@ function pagination(options) {
2831
3006
  const clamped = Math.max(1, Math.min(target, totalPages()));
2832
3007
  setPage(clamped);
2833
3008
  }
2834
- return { page: currentPage, pageSize, totalPages, next, prev, goTo, startIndex, endIndex };
3009
+ function dispose2() {
3010
+ startIndex.dispose();
3011
+ endIndex.dispose();
3012
+ currentPage.dispose();
3013
+ totalPages.dispose();
3014
+ }
3015
+ return { page: currentPage, pageSize, totalPages, next, prev, goTo, startIndex, endIndex, dispose: dispose2 };
2835
3016
  }
2836
3017
 
2837
3018
  // src/ui/eventBus.ts
2838
3019
  function eventBus() {
2839
3020
  const listeners = /* @__PURE__ */ new Map();
2840
3021
  function on(event, handler) {
2841
- let set = listeners.get(event);
2842
- if (!set) {
2843
- set = /* @__PURE__ */ new Set();
2844
- listeners.set(event, set);
3022
+ let subs = listeners.get(event);
3023
+ if (!subs) {
3024
+ subs = /* @__PURE__ */ new Map();
3025
+ listeners.set(event, subs);
3026
+ }
3027
+ let subscription = subs.get(handler);
3028
+ if (!subscription) {
3029
+ subscription = { handler, active: true };
3030
+ subs.set(handler, subscription);
2845
3031
  }
2846
- set.add(handler);
2847
- return () => off(event, handler);
3032
+ const own = subscription;
3033
+ return () => {
3034
+ if (listeners.get(event)?.get(handler) !== own) return;
3035
+ off(event, handler);
3036
+ };
2848
3037
  }
2849
3038
  function emit(event, data) {
2850
- const set = listeners.get(event);
2851
- if (set) {
2852
- for (const handler of set) {
2853
- handler(data);
3039
+ const subs = listeners.get(event);
3040
+ if (!subs || subs.size === 0) return;
3041
+ for (const subscription of Array.from(subs.values())) {
3042
+ if (!subscription.active) continue;
3043
+ try {
3044
+ subscription.handler(data);
3045
+ } catch (err) {
3046
+ reportError(err, { phase: "event", name: `eventBus(${String(event)})` });
2854
3047
  }
2855
3048
  }
2856
3049
  }
2857
3050
  function off(event, handler) {
2858
- const set = listeners.get(event);
2859
- if (set) {
2860
- set.delete(handler);
2861
- if (set.size === 0) {
2862
- listeners.delete(event);
2863
- }
3051
+ const subs = listeners.get(event);
3052
+ if (!subs) return;
3053
+ const subscription = subs.get(handler);
3054
+ if (!subscription) return;
3055
+ subscription.active = false;
3056
+ subs.delete(handler);
3057
+ if (subs.size === 0) {
3058
+ listeners.delete(event);
2864
3059
  }
2865
3060
  }
2866
3061
  function clear() {
3062
+ for (const subs of listeners.values()) {
3063
+ for (const subscription of subs.values()) subscription.active = false;
3064
+ }
2867
3065
  listeners.clear();
2868
3066
  }
2869
3067
  return { on, emit, off, clear };
2870
3068
  }
2871
3069
 
3070
+ // src/core/signals/effect.ts
3071
+ var _g2 = globalThis;
3072
+ var MAX_RERUNS = 100;
3073
+ function flushUserCleanups(ctx) {
3074
+ const list = ctx.userCleanups;
3075
+ if (list.length === 0) return;
3076
+ ctx.userCleanups = [];
3077
+ for (let i = list.length - 1; i >= 0; i--) {
3078
+ try {
3079
+ list[i]();
3080
+ } catch (err) {
3081
+ reportError(err, { phase: "cleanup" });
3082
+ }
3083
+ }
3084
+ }
3085
+ function drainReruns(ctx) {
3086
+ let reruns = 1;
3087
+ do {
3088
+ ctx.rerunPending = false;
3089
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
3090
+ retrack2(ctx.bodyFn, ctx.subscriber);
3091
+ } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
3092
+ if (ctx.rerunPending) {
3093
+ ctx.rerunPending = false;
3094
+ reportError(
3095
+ new Error(
3096
+ `effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
3097
+ ),
3098
+ { phase: "effect" }
3099
+ );
3100
+ }
3101
+ }
3102
+ function disposeEffect(ctx) {
3103
+ if (ctx.disposed) return;
3104
+ ctx.disposed = true;
3105
+ const h = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
3106
+ if (h) {
3107
+ try {
3108
+ h.emit("effect:destroy", { effectFn: ctx.fn });
3109
+ } catch {
3110
+ }
3111
+ }
3112
+ try {
3113
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
3114
+ } catch (err) {
3115
+ if (typeof console !== "undefined") {
3116
+ console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
3117
+ }
3118
+ }
3119
+ try {
3120
+ cleanup2(ctx.subscriber);
3121
+ } catch (err) {
3122
+ if (typeof console !== "undefined") {
3123
+ console.warn("[SibuJS effect] dispose threw:", err);
3124
+ }
3125
+ }
3126
+ }
3127
+ function effect(effectFn, options) {
3128
+ devAssert(typeof effectFn === "function", "effect: argument must be a function.");
3129
+ if (isSSR()) return () => {
3130
+ };
3131
+ const ctx = {
3132
+ fn: effectFn,
3133
+ onError: options?.onError,
3134
+ userCleanups: [],
3135
+ running: false,
3136
+ rerunPending: false,
3137
+ disposed: false,
3138
+ onCleanup: null,
3139
+ subscriber: null,
3140
+ bodyFn: null
3141
+ };
3142
+ ctx.onCleanup = (fn) => {
3143
+ ctx.userCleanups.push(fn);
3144
+ };
3145
+ const onErrorCaptured = ctx.onError;
3146
+ ctx.bodyFn = onErrorCaptured ? () => {
3147
+ try {
3148
+ ctx.fn(ctx.onCleanup);
3149
+ } catch (err) {
3150
+ onErrorCaptured(err);
3151
+ }
3152
+ } : () => {
3153
+ ctx.fn(ctx.onCleanup);
3154
+ };
3155
+ const sub = (() => {
3156
+ if (ctx.disposed) return;
3157
+ if (ctx.running) {
3158
+ ctx.rerunPending = true;
3159
+ return;
3160
+ }
3161
+ ctx.running = true;
3162
+ try {
3163
+ ctx.rerunPending = false;
3164
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
3165
+ retrack2(ctx.bodyFn, sub);
3166
+ if (ctx.rerunPending) drainReruns(ctx);
3167
+ } finally {
3168
+ ctx.running = false;
3169
+ ctx.rerunPending = false;
3170
+ }
3171
+ });
3172
+ sub.depsHead = null;
3173
+ sub.depsTail = null;
3174
+ sub._epoch = 0;
3175
+ sub._structDirty = false;
3176
+ sub._hasComputedDep = false;
3177
+ sub._errorPhase = "effect";
3178
+ sub._runEpoch = 0;
3179
+ sub._runs = 0;
3180
+ ctx.subscriber = sub;
3181
+ ctx.running = true;
3182
+ try {
3183
+ retrack2(ctx.bodyFn, ctx.subscriber);
3184
+ if (ctx.rerunPending) drainReruns(ctx);
3185
+ } finally {
3186
+ ctx.running = false;
3187
+ ctx.rerunPending = false;
3188
+ }
3189
+ const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
3190
+ if (hook) hook.emit("effect:create", { effectFn });
3191
+ return () => disposeEffect(ctx);
3192
+ }
3193
+
2872
3194
  // src/ui/lazyEffect.ts
2873
3195
  function lazyEffect(element, effectFn, options) {
2874
3196
  if (typeof IntersectionObserver === "undefined") {
@@ -2904,25 +3226,59 @@ function lazyEffect(element, effectFn, options) {
2904
3226
 
2905
3227
  // src/ui/timers.ts
2906
3228
  function interval(fn, ms) {
2907
- let id = null;
3229
+ let intervalId = null;
3230
+ let resumeId = null;
2908
3231
  let running = false;
2909
- function start() {
3232
+ let nextDue = 0;
3233
+ let remaining = ms;
3234
+ function tick() {
3235
+ nextDue = Date.now() + ms;
3236
+ fn();
3237
+ }
3238
+ function clearTimers() {
3239
+ if (intervalId !== null) {
3240
+ clearInterval(intervalId);
3241
+ intervalId = null;
3242
+ }
3243
+ if (resumeId !== null) {
3244
+ clearTimeout(resumeId);
3245
+ resumeId = null;
3246
+ }
3247
+ }
3248
+ function startRegular() {
3249
+ nextDue = Date.now() + ms;
3250
+ intervalId = setInterval(tick, ms);
3251
+ }
3252
+ function resume() {
2910
3253
  if (running) return;
2911
- id = setInterval(fn, ms);
2912
3254
  running = true;
3255
+ if (remaining >= ms) {
3256
+ startRegular();
3257
+ return;
3258
+ }
3259
+ nextDue = Date.now() + remaining;
3260
+ resumeId = setTimeout(() => {
3261
+ resumeId = null;
3262
+ startRegular();
3263
+ fn();
3264
+ }, remaining);
3265
+ }
3266
+ function pause() {
3267
+ if (!running) return;
3268
+ remaining = Math.min(ms, Math.max(0, nextDue - Date.now()));
3269
+ clearTimers();
3270
+ running = false;
2913
3271
  }
2914
3272
  function stop() {
2915
- if (id !== null) {
2916
- clearInterval(id);
2917
- id = null;
2918
- }
3273
+ clearTimers();
2919
3274
  running = false;
3275
+ remaining = ms;
2920
3276
  }
2921
- start();
3277
+ resume();
2922
3278
  return {
2923
3279
  stop,
2924
- pause: stop,
2925
- resume: start,
3280
+ pause,
3281
+ resume,
2926
3282
  isRunning: () => running
2927
3283
  };
2928
3284
  }
@@ -2954,7 +3310,11 @@ function hover(target) {
2954
3310
  const onLeave = () => setHovered(false);
2955
3311
  target.addEventListener("pointerenter", onEnter);
2956
3312
  target.addEventListener("pointerleave", onLeave);
3313
+ let disposed = false;
2957
3314
  function dispose2() {
3315
+ if (disposed) return;
3316
+ disposed = true;
3317
+ unregisterDisposer(target, dispose2);
2958
3318
  target.removeEventListener("pointerenter", onEnter);
2959
3319
  target.removeEventListener("pointerleave", onLeave);
2960
3320
  }
@@ -2968,21 +3328,37 @@ var _lock = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.scrollLock.v1"),
2968
3328
  savedOverflow: null,
2969
3329
  savedPaddingRight: null
2970
3330
  }));
3331
+ function applyBodyLock() {
3332
+ const body = document.body;
3333
+ if (!body) throw new Error("[scrollLock] document.body is not available yet");
3334
+ const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
3335
+ const savedOverflow = body.style.overflow;
3336
+ const savedPaddingRight = body.style.paddingRight;
3337
+ try {
3338
+ body.style.overflow = "hidden";
3339
+ if (scrollBarWidth > 0) {
3340
+ body.style.paddingRight = `${scrollBarWidth}px`;
3341
+ }
3342
+ } catch (err) {
3343
+ try {
3344
+ body.style.overflow = savedOverflow;
3345
+ body.style.paddingRight = savedPaddingRight;
3346
+ } catch {
3347
+ }
3348
+ throw err;
3349
+ }
3350
+ _lock.savedOverflow = savedOverflow;
3351
+ _lock.savedPaddingRight = savedPaddingRight;
3352
+ }
2971
3353
  function scrollLock() {
2972
3354
  let owned = false;
2973
3355
  function lock() {
2974
3356
  if (owned) return;
3357
+ if (_lock.count === 0 && typeof document !== "undefined") {
3358
+ applyBodyLock();
3359
+ }
2975
3360
  owned = true;
2976
3361
  _lock.count++;
2977
- if (_lock.count !== 1 || typeof document === "undefined") return;
2978
- const body = document.body;
2979
- const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
2980
- _lock.savedOverflow = body.style.overflow;
2981
- _lock.savedPaddingRight = body.style.paddingRight;
2982
- body.style.overflow = "hidden";
2983
- if (scrollBarWidth > 0) {
2984
- body.style.paddingRight = `${scrollBarWidth}px`;
2985
- }
2986
3362
  }
2987
3363
  function unlock() {
2988
3364
  if (!owned) return;
@@ -3000,13 +3376,33 @@ function scrollLock() {
3000
3376
 
3001
3377
  // src/platform/customElement.ts
3002
3378
  var SVG_NS2 = "http://www.w3.org/2000/svg";
3379
+ var MAX_RENDER_PASSES = 10;
3380
+ var ABANDONED_RENDER = /* @__PURE__ */ Symbol("sibujs.defineElement.abandonedRender");
3003
3381
  function defineElement(name, component, options = {}) {
3004
3382
  if (customElements.get(name)) return;
3005
3383
  const observed = options.observedAttributes || [];
3006
3384
  class SibuElement extends HTMLElement {
3007
3385
  constructor() {
3008
3386
  super();
3009
- this._rendered = null;
3387
+ // Re-rendering is keyed on connection, not on a previous render: a first render
3388
+ // that throws leaves nothing rendered, and the element must still re-render
3389
+ // when a later attribute change fixes the input.
3390
+ this._connected = false;
3391
+ // A render in progress, and whether an attribute changed during it. The
3392
+ // component may write its host's observed attributes while rendering; with
3393
+ // the old subtree kept during the build, rendering again from inside the
3394
+ // callback recursed until the stack overflowed.
3395
+ this._rendering = false;
3396
+ this._dirty = false;
3397
+ // How many teardowns are draining. A host teardown may reconnect the element
3398
+ // (and a nested disconnect may run inside it), and rendering the next
3399
+ // generation from inside that drain would let the drain tear down what the
3400
+ // new render registers. A counter, not a flag: a nested teardown returning
3401
+ // must not declare the outer one finished.
3402
+ this._teardownDepth = 0;
3403
+ // Disconnects so far. A render compares it across its old-subtree teardown
3404
+ // to detect a disconnect that happened inside it.
3405
+ this._disconnects = 0;
3010
3406
  if (options.shadow !== false) {
3011
3407
  this._root = this.attachShadow({ mode: options.mode || "open" });
3012
3408
  } else {
@@ -3016,32 +3412,106 @@ function defineElement(name, component, options = {}) {
3016
3412
  static get observedAttributes() {
3017
3413
  return observed;
3018
3414
  }
3415
+ /** Whether rendering must wait for work already in progress. */
3416
+ get _busy() {
3417
+ return this._rendering || this._teardownDepth > 0;
3418
+ }
3019
3419
  connectedCallback() {
3420
+ this._connected = true;
3421
+ if (this._busy) {
3422
+ this._dirty = true;
3423
+ return;
3424
+ }
3020
3425
  this._render();
3021
3426
  }
3022
3427
  disconnectedCallback() {
3023
- this._teardown();
3428
+ this._connected = false;
3429
+ this._disconnects++;
3430
+ this._teardownDepth++;
3431
+ try {
3432
+ this._teardown();
3433
+ } finally {
3434
+ this._teardownDepth--;
3435
+ if (this._teardownDepth === 0 && this._connected && this._dirty) {
3436
+ this._dirty = false;
3437
+ if (!this._rendering) this._render();
3438
+ }
3439
+ }
3024
3440
  }
3025
- attributeChangedCallback() {
3026
- if (this._rendered) {
3027
- this._render();
3441
+ attributeChangedCallback(_name, oldValue, newValue) {
3442
+ if (oldValue === newValue || !this._connected) return;
3443
+ if (this._busy) {
3444
+ this._dirty = true;
3445
+ return;
3028
3446
  }
3447
+ this._render();
3029
3448
  }
3030
3449
  _teardown() {
3031
- this._rendered = null;
3032
3450
  replaceChildrenSafely(this._root);
3033
- }
3451
+ disposeNodeOwn(this);
3452
+ }
3453
+ /**
3454
+ * Render as a transaction: build the replacement first, commit only if that
3455
+ * succeeds. Tearing the current subtree down before calling the factory
3456
+ * meant a throwing rerender (an invalid attribute, say) left the element
3457
+ * blank with its live state already disposed. Now a failure keeps the
3458
+ * working subtree, releases whatever the failed attempt registered, and is
3459
+ * reported with this element as its node so an enclosing ErrorBoundary can
3460
+ * claim it.
3461
+ */
3034
3462
  _render() {
3035
- this._teardown();
3463
+ this._rendering = true;
3464
+ try {
3465
+ let passes = 0;
3466
+ do {
3467
+ this._dirty = false;
3468
+ if (++passes > MAX_RENDER_PASSES) {
3469
+ reportError(
3470
+ new Error(
3471
+ `[SibuJS] defineElement(${name}): the component changed its own observed attributes on ${MAX_RENDER_PASSES} consecutive renders; stopped re-rendering.`
3472
+ ),
3473
+ { phase: "render", name: `defineElement(${name})`, node: this }
3474
+ );
3475
+ break;
3476
+ }
3477
+ this._renderOnce();
3478
+ } while (this._dirty && this._connected);
3479
+ } finally {
3480
+ this._rendering = false;
3481
+ this._dirty = false;
3482
+ }
3483
+ }
3484
+ _renderOnce() {
3036
3485
  const props = this._getProps();
3486
+ let el;
3487
+ try {
3488
+ el = withDisposerRollback(() => {
3489
+ const built = component(props, this);
3490
+ if (!this._connected) throw ABANDONED_RENDER;
3491
+ return built;
3492
+ });
3493
+ } catch (err) {
3494
+ if (err !== ABANDONED_RENDER) {
3495
+ reportError(err, { phase: "render", name: `defineElement(${name})`, node: this });
3496
+ }
3497
+ return;
3498
+ }
3499
+ const next = [];
3037
3500
  if (options.styles && this._root instanceof ShadowRoot) {
3038
3501
  const styleEl = document.createElement("style");
3039
3502
  styleEl.textContent = options.styles;
3040
- this._root.appendChild(styleEl);
3503
+ next.push(styleEl);
3504
+ }
3505
+ next.push(el);
3506
+ const disconnects = this._disconnects;
3507
+ for (const node of next) node.parentNode?.removeChild(node);
3508
+ replaceChildrenSafely(this._root);
3509
+ if (this._disconnects !== disconnects) {
3510
+ for (const node of next) dispose(node);
3511
+ if (this._connected) this._dirty = true;
3512
+ return;
3041
3513
  }
3042
- const el = component(props, this);
3043
- this._root.appendChild(el);
3044
- this._rendered = el;
3514
+ this._root.replaceChildren(...next);
3045
3515
  }
3046
3516
  _getProps() {
3047
3517
  const props = {};
@@ -3192,7 +3662,7 @@ function validateProps(props, schema) {
3192
3662
  const errors = [];
3193
3663
  for (const [key, def] of Object.entries(schema)) {
3194
3664
  const propDef = typeof def === "function" ? { type: def } : def;
3195
- if (result[key] == null && propDef.default !== void 0) {
3665
+ if (result[key] === void 0 && propDef.default !== void 0) {
3196
3666
  result[key] = typeof propDef.default === "function" ? propDef.default() : propDef.default;
3197
3667
  }
3198
3668
  if (propDef.required && result[key] == null) {
@@ -3218,7 +3688,7 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`);
3218
3688
  for (const [key, def] of Object.entries(schema)) {
3219
3689
  if (typeof def === "function") continue;
3220
3690
  const fallback = def.default;
3221
- if (result[key] == null && fallback !== void 0) {
3691
+ if (result[key] === void 0 && fallback !== void 0) {
3222
3692
  result[key] = typeof fallback === "function" ? fallback() : fallback;
3223
3693
  }
3224
3694
  }