sibujs 4.5.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +1 -1
  2. package/dist/browser.cjs +326 -133
  3. package/dist/browser.d.cts +46 -14
  4. package/dist/browser.d.ts +46 -14
  5. package/dist/browser.js +8 -6
  6. package/dist/build.cjs +2094 -1033
  7. package/dist/build.d.cts +133 -44
  8. package/dist/build.d.ts +133 -44
  9. package/dist/build.js +1372 -574
  10. package/dist/cdn.dev.global.js +10 -10
  11. package/dist/cdn.full.dev.global.js +11 -11
  12. package/dist/cdn.full.global.js +10 -10
  13. package/dist/cdn.global.js +10 -10
  14. package/dist/{chunk-2DCGACUU.js → chunk-2BPG2XDA.js} +251 -66
  15. package/dist/{chunk-IXKSNWV5.js → chunk-2INLLLMZ.js} +1 -1
  16. package/dist/{chunk-4PMLNECI.js → chunk-3QWBSL5R.js} +98 -29
  17. package/dist/{chunk-KKLW7YWL.js → chunk-4AWA2PVD.js} +275 -144
  18. package/dist/chunk-5HZXGZ6T.js +24 -0
  19. package/dist/{chunk-PCT43HW3.js → chunk-7LUJQAOJ.js} +1 -1
  20. package/dist/{chunk-5DXA2J44.js → chunk-7XHATCIH.js} +5 -2
  21. package/dist/chunk-BC2SECJD.js +44 -0
  22. package/dist/{chunk-TBYTO6BS.js → chunk-BSY63EM6.js} +136 -80
  23. package/dist/{chunk-FTIR4QW2.js → chunk-DKXRACVN.js} +6 -8
  24. package/dist/{chunk-ONOHFDLG.js → chunk-FNJXNGYZ.js} +3 -3
  25. package/dist/{chunk-XZZOBQAY.js → chunk-FQRUXCEE.js} +6 -5
  26. package/dist/{chunk-NIOYEGBQ.js → chunk-HURREPU2.js} +27 -13
  27. package/dist/chunk-J6FW5TV6.js +233 -0
  28. package/dist/{chunk-UGRX3S57.js → chunk-JWKYU5GV.js} +22 -4
  29. package/dist/chunk-NYNYSPK7.js +318 -0
  30. package/dist/{chunk-RBTPLM32.js → chunk-RUSSKG6G.js} +13 -12
  31. package/dist/{chunk-7LN645I6.js → chunk-SLM3IA34.js} +3 -3
  32. package/dist/{chunk-R25EFXXC.js → chunk-TUCPL2HB.js} +3 -3
  33. package/dist/{chunk-B3WHI2QA.js → chunk-UNWRJRKC.js} +55 -31
  34. package/dist/{chunk-KEISJXBU.js → chunk-UOL2ECCS.js} +44 -17
  35. package/dist/{chunk-RJE2BNI4.js → chunk-VZSG24LS.js} +108 -45
  36. package/dist/{chunk-RIXRAYIU.js → chunk-WEQ3DMVL.js} +10 -4
  37. package/dist/{chunk-3JZ4L5TJ.js → chunk-WOLJZUFQ.js} +354 -90
  38. package/dist/{chunk-S373NSMK.js → chunk-XYV3EDB7.js} +283 -123
  39. package/dist/{chunk-VZKNK2V7.js → chunk-XZR4PXRE.js} +296 -38
  40. package/dist/{chunk-GW3SCCZG.js → chunk-Z3OHK6QT.js} +153 -110
  41. package/dist/{chunk-OMJJM3KM.js → chunk-ZVL7TY4K.js} +287 -130
  42. package/dist/{contracts-DBdg9J_a.d.ts → contracts-CLqzJnOV.d.ts} +36 -17
  43. package/dist/{contracts-DBdg9J_a.d.cts → contracts-CTOJXu-x.d.cts} +36 -17
  44. package/dist/{customElement-OB9CIsc5.d.cts → customElement-MmInOW1U.d.cts} +21 -0
  45. package/dist/{customElement-OB9CIsc5.d.ts → customElement-MmInOW1U.d.ts} +21 -0
  46. package/dist/data.cjs +410 -155
  47. package/dist/data.d.cts +166 -12
  48. package/dist/data.d.ts +166 -12
  49. package/dist/data.js +12 -9
  50. package/dist/devtools.cjs +98 -61
  51. package/dist/devtools.js +7 -8
  52. package/dist/dispose-GEIG2KOF.js +28 -0
  53. package/dist/ecosystem.cjs +372 -126
  54. package/dist/ecosystem.d.cts +20 -3
  55. package/dist/ecosystem.d.ts +20 -3
  56. package/dist/ecosystem.js +12 -12
  57. package/dist/extras.cjs +2366 -909
  58. package/dist/extras.d.cts +11 -9
  59. package/dist/extras.d.ts +11 -9
  60. package/dist/extras.js +39 -28
  61. package/dist/index.cjs +421 -158
  62. package/dist/index.d.cts +213 -171
  63. package/dist/index.d.ts +213 -171
  64. package/dist/index.js +24 -27
  65. package/dist/motion.cjs +118 -44
  66. package/dist/motion.js +5 -5
  67. package/dist/patterns.cjs +344 -53
  68. package/dist/patterns.d.cts +28 -9
  69. package/dist/patterns.d.ts +28 -9
  70. package/dist/patterns.js +8 -8
  71. package/dist/performance.cjs +325 -220
  72. package/dist/performance.d.cts +2 -2
  73. package/dist/performance.d.ts +2 -2
  74. package/dist/performance.js +8 -9
  75. package/dist/plugin-DVgSnTfK.d.cts +112 -0
  76. package/dist/plugin-DVgSnTfK.d.ts +112 -0
  77. package/dist/plugins.cjs +664 -233
  78. package/dist/plugins.d.cts +127 -14
  79. package/dist/plugins.d.ts +127 -14
  80. package/dist/plugins.js +96 -42
  81. package/dist/signal-EotCj4hS.d.cts +110 -0
  82. package/dist/signal-EotCj4hS.d.ts +110 -0
  83. package/dist/{ssr-BiPRdZ6n.d.cts → ssr-Bli9XRW5.d.cts} +5 -0
  84. package/dist/{ssr-BiPRdZ6n.d.ts → ssr-Bli9XRW5.d.ts} +5 -0
  85. package/dist/{ssr-Y7XOEPEN.js → ssr-XOTUASDO.js} +4 -5
  86. package/dist/ssr.cjs +229 -84
  87. package/dist/ssr.d.cts +9 -3
  88. package/dist/ssr.d.ts +9 -3
  89. package/dist/ssr.js +11 -12
  90. package/dist/{startup-BMpaiMhP.d.ts → startup-BLfSeL15.d.cts} +73 -22
  91. package/dist/{startup-BMpaiMhP.d.cts → startup-BLfSeL15.d.ts} +73 -22
  92. package/dist/tagFactory-8qL9LCIx.d.cts +156 -0
  93. package/dist/tagFactory-BL2fymez.d.ts +156 -0
  94. package/dist/testing.cjs +2503 -2191
  95. package/dist/testing.d.cts +56 -5
  96. package/dist/testing.d.ts +56 -5
  97. package/dist/testing.js +580 -307
  98. package/dist/types-CJFViL6Q.d.cts +26 -0
  99. package/dist/types-CJFViL6Q.d.ts +26 -0
  100. package/dist/ui.cjs +732 -329
  101. package/dist/ui.d.cts +41 -7
  102. package/dist/ui.d.ts +41 -7
  103. package/dist/ui.js +151 -56
  104. package/dist/widgets.cjs +267 -291
  105. package/dist/widgets.js +9 -10
  106. package/package.json +4 -2
  107. package/dist/chunk-2WLZ6757.js +0 -149
  108. package/dist/chunk-CCSJMTRN.js +0 -15
  109. package/dist/chunk-QKRPLZ2V.js +0 -108
  110. package/dist/chunk-VUF4ALSW.js +0 -60
  111. package/dist/chunk-WWV3SJ3L.js +0 -131
  112. package/dist/dispose-46BOMMQJ.js +0 -19
  113. package/dist/plugin-D30wlGW5.d.cts +0 -71
  114. package/dist/plugin-D30wlGW5.d.ts +0 -71
  115. package/dist/tagFactory-DVoDpHye.d.cts +0 -215
  116. package/dist/tagFactory-DVoDpHye.d.ts +0 -215
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
  }
@@ -428,6 +501,7 @@ function retrack(effectFn, subscriber) {
428
501
  }
429
502
  currentSubscriber = subscriber;
430
503
  const sub = subscriber;
504
+ if (sub._cap === void 0) sub._cap = currentDisposerCapture();
431
505
  const epoch = ++subscriberEpochCounter;
432
506
  sub._epoch = epoch;
433
507
  sub._structDirty = false;
@@ -619,7 +693,12 @@ function drainQueue() {
619
693
  quarantined.add(sub);
620
694
  continue;
621
695
  }
622
- safeInvoke(sub);
696
+ const pushed = beginDisposerCapture(sub._cap ?? null);
697
+ try {
698
+ safeInvoke(sub);
699
+ } finally {
700
+ if (pushed) endDisposerCapture();
701
+ }
623
702
  }
624
703
  }
625
704
  function drainNotificationQueue() {
@@ -755,7 +834,7 @@ function forEachSubscriber(signal2, visit) {
755
834
  }
756
835
 
757
836
  // src/reactivity/track.ts
758
- var _runtimeVersion = true ? "4.5.0" : "dev";
837
+ var _runtimeVersion = true ? "4.7.0" : "dev";
759
838
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
760
839
  function resolveReactiveApi() {
761
840
  const g = globalThis;
@@ -811,6 +890,14 @@ var getSubscriberCount2 = API.getSubscriberCount;
811
890
  var getSubscriberDeps2 = API.getSubscriberDeps;
812
891
  var forEachSubscriber2 = API.forEachSubscriber;
813
892
 
893
+ // src/core/devtoolsHook.ts
894
+ function emitDevtools(hook, event, payload) {
895
+ try {
896
+ hook.emit(event, payload);
897
+ } catch {
898
+ }
899
+ }
900
+
814
901
  // src/core/signals/derived.ts
815
902
  function derived(getter, options) {
816
903
  devAssert(typeof getter === "function", "derived: argument must be a getter function.");
@@ -837,17 +924,17 @@ function derived(getter, options) {
837
924
  cs._d = false;
838
925
  cs._init = true;
839
926
  };
840
- track2(() => {
841
- let threw = true;
842
- try {
927
+ try {
928
+ track2(() => {
843
929
  cs._v = getter();
844
930
  cs._d = false;
845
931
  cs._init = true;
846
- threw = false;
847
- } finally {
848
- if (threw) cs._d = true;
849
- }
850
- }, markDirty);
932
+ }, markDirty);
933
+ } catch (err) {
934
+ disposed = true;
935
+ cleanup2(markDirty);
936
+ throw err;
937
+ }
851
938
  const hook = globalThis.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
852
939
  const validate = () => {
853
940
  if (!cs._d || disposed || pendingError !== void 0) return;
@@ -866,7 +953,7 @@ function derived(getter, options) {
866
953
  if (disposed) cleanup2(markDirty);
867
954
  }
868
955
  if (hook && !disposed && !Object.is(oldValue, cs._v)) {
869
- hook.emit("computed:update", { signal: cs, oldValue, newValue: cs._v });
956
+ emitDevtools(hook, "computed:update", { signal: cs, oldValue, newValue: cs._v });
870
957
  }
871
958
  };
872
959
  cs._validate = validate;
@@ -917,173 +1004,10 @@ function derived(getter, options) {
917
1004
  }
918
1005
  }
919
1006
  };
920
- if (hook) hook.emit("computed:create", { signal: cs, name: debugName, getter: computedGetter });
1007
+ if (hook) emitDevtools(hook, "computed:create", { signal: cs, name: debugName, getter: computedGetter });
921
1008
  return computedGetter;
922
1009
  }
923
1010
 
924
- // src/core/ssr-context.ts
925
- var SSR_KEY = /* @__PURE__ */ Symbol.for("sibujs.ssr.v1");
926
- function detectSSRShared() {
927
- let detected = null;
928
- try {
929
- if (typeof process !== "undefined" && process.versions && process.versions.node) {
930
- let mod = null;
931
- const getBuiltin = process.getBuiltinModule;
932
- if (typeof getBuiltin === "function") {
933
- mod = getBuiltin("node:async_hooks");
934
- } else if (typeof require === "function") {
935
- mod = require("async_hooks");
936
- }
937
- if (mod) detected = new mod.AsyncLocalStorage();
938
- }
939
- } catch {
940
- detected = null;
941
- }
942
- return {
943
- als: detected,
944
- fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
945
- fallbackDepth: 0
946
- };
947
- }
948
- var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
949
- var als = _shared.als;
950
- var fallbackStore = _shared.fallbackStore;
951
- _shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
952
- function getSSRStore() {
953
- if (als) {
954
- const s = als.getStore();
955
- if (s) return s;
956
- }
957
- return fallbackStore;
958
- }
959
- function isSSR() {
960
- return getSSRStore().ssr;
961
- }
962
-
963
- // src/core/signals/effect.ts
964
- var _g = globalThis;
965
- var MAX_RERUNS = 100;
966
- function flushUserCleanups(ctx) {
967
- const list = ctx.userCleanups;
968
- if (list.length === 0) return;
969
- ctx.userCleanups = [];
970
- for (let i = list.length - 1; i >= 0; i--) {
971
- try {
972
- list[i]();
973
- } catch (err) {
974
- reportError(err, { phase: "cleanup" });
975
- }
976
- }
977
- }
978
- function drainReruns(ctx) {
979
- let reruns = 1;
980
- do {
981
- ctx.rerunPending = false;
982
- if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
983
- retrack2(ctx.bodyFn, ctx.subscriber);
984
- } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
985
- if (ctx.rerunPending) {
986
- ctx.rerunPending = false;
987
- reportError(
988
- new Error(
989
- `effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
990
- ),
991
- { phase: "effect" }
992
- );
993
- }
994
- }
995
- function disposeEffect(ctx) {
996
- if (ctx.disposed) return;
997
- ctx.disposed = true;
998
- const h = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
999
- if (h) {
1000
- try {
1001
- h.emit("effect:destroy", { effectFn: ctx.fn });
1002
- } catch {
1003
- }
1004
- }
1005
- try {
1006
- if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
1007
- } catch (err) {
1008
- if (typeof console !== "undefined") {
1009
- console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
1010
- }
1011
- }
1012
- try {
1013
- cleanup2(ctx.subscriber);
1014
- } catch (err) {
1015
- if (typeof console !== "undefined") {
1016
- console.warn("[SibuJS effect] dispose threw:", err);
1017
- }
1018
- }
1019
- }
1020
- function effect(effectFn, options) {
1021
- devAssert(typeof effectFn === "function", "effect: argument must be a function.");
1022
- if (isSSR()) return () => {
1023
- };
1024
- const ctx = {
1025
- fn: effectFn,
1026
- onError: options?.onError,
1027
- userCleanups: [],
1028
- running: false,
1029
- rerunPending: false,
1030
- disposed: false,
1031
- onCleanup: null,
1032
- subscriber: null,
1033
- bodyFn: null
1034
- };
1035
- ctx.onCleanup = (fn) => {
1036
- ctx.userCleanups.push(fn);
1037
- };
1038
- const onErrorCaptured = ctx.onError;
1039
- ctx.bodyFn = onErrorCaptured ? () => {
1040
- try {
1041
- ctx.fn(ctx.onCleanup);
1042
- } catch (err) {
1043
- onErrorCaptured(err);
1044
- }
1045
- } : () => {
1046
- ctx.fn(ctx.onCleanup);
1047
- };
1048
- const sub = (() => {
1049
- if (ctx.disposed) return;
1050
- if (ctx.running) {
1051
- ctx.rerunPending = true;
1052
- return;
1053
- }
1054
- ctx.running = true;
1055
- try {
1056
- ctx.rerunPending = false;
1057
- if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
1058
- retrack2(ctx.bodyFn, sub);
1059
- if (ctx.rerunPending) drainReruns(ctx);
1060
- } finally {
1061
- ctx.running = false;
1062
- ctx.rerunPending = false;
1063
- }
1064
- });
1065
- sub.depsHead = null;
1066
- sub.depsTail = null;
1067
- sub._epoch = 0;
1068
- sub._structDirty = false;
1069
- sub._hasComputedDep = false;
1070
- sub._errorPhase = "effect";
1071
- sub._runEpoch = 0;
1072
- sub._runs = 0;
1073
- ctx.subscriber = sub;
1074
- ctx.running = true;
1075
- try {
1076
- retrack2(ctx.bodyFn, ctx.subscriber);
1077
- if (ctx.rerunPending) drainReruns(ctx);
1078
- } finally {
1079
- ctx.running = false;
1080
- ctx.rerunPending = false;
1081
- }
1082
- const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1083
- if (hook) hook.emit("effect:create", { effectFn });
1084
- return () => disposeEffect(ctx);
1085
- }
1086
-
1087
1011
  // src/reactivity/batch.ts
1088
1012
  var batchDepth = 0;
1089
1013
  var pendingSignals = /* @__PURE__ */ new Set();
@@ -1135,7 +1059,7 @@ var enqueueBatchedSignal = API2.enqueueBatchedSignal;
1135
1059
  var isBatching = API2.isBatching;
1136
1060
 
1137
1061
  // src/core/signals/signal.ts
1138
- var _g2 = globalThis;
1062
+ var _g = globalThis;
1139
1063
  function signal(initial, options) {
1140
1064
  const state2 = {
1141
1065
  value: initial,
@@ -1166,8 +1090,8 @@ function signal(initial, options) {
1166
1090
  state2.value = newValue;
1167
1091
  state2.__v++;
1168
1092
  if (DEV) {
1169
- const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1170
- if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1093
+ const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1094
+ if (hook) emitDevtools(hook, "signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1171
1095
  }
1172
1096
  if (!enqueueBatchedSignal(state2)) {
1173
1097
  notifySubscribers2(state2);
@@ -1180,8 +1104,8 @@ function signal(initial, options) {
1180
1104
  if (Object.is(newValue, prev)) return;
1181
1105
  state2.value = newValue;
1182
1106
  state2.__v++;
1183
- const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1184
- if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1107
+ const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1108
+ if (hook) emitDevtools(hook, "signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1185
1109
  if (!enqueueBatchedSignal(state2)) {
1186
1110
  notifySubscribers2(state2);
1187
1111
  }
@@ -1198,11 +1122,82 @@ function signal(initial, options) {
1198
1122
  }
1199
1123
  };
1200
1124
  }
1201
- if (DEV) {
1202
- const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1203
- if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
1125
+ if (DEV) {
1126
+ const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1127
+ if (hook) emitDevtools(hook, "signal:create", { signal: state2, name: debugName, getter: get, initial });
1128
+ }
1129
+ return [get, set];
1130
+ }
1131
+
1132
+ // src/core/ssr-context.ts
1133
+ var SSR_KEY = /* @__PURE__ */ Symbol.for("sibujs.ssr.v1");
1134
+ function detectSSRShared() {
1135
+ let detected = null;
1136
+ try {
1137
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
1138
+ let mod = null;
1139
+ const getBuiltin = process.getBuiltinModule;
1140
+ if (typeof getBuiltin === "function") {
1141
+ mod = getBuiltin("node:async_hooks");
1142
+ } else if (typeof require === "function") {
1143
+ mod = require("async_hooks");
1144
+ }
1145
+ if (mod) detected = new mod.AsyncLocalStorage();
1146
+ }
1147
+ } catch {
1148
+ detected = null;
1149
+ }
1150
+ return {
1151
+ als: detected,
1152
+ fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
1153
+ fallbackDepth: 0
1154
+ };
1155
+ }
1156
+ var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
1157
+ var als = _shared.als;
1158
+ var fallbackStore = _shared.fallbackStore;
1159
+ _shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
1160
+ function getSSRStore() {
1161
+ if (als) {
1162
+ const s = als.getStore();
1163
+ if (s) return s;
1164
+ }
1165
+ return fallbackStore;
1166
+ }
1167
+ function getRequestStore() {
1168
+ if (als) return als.getStore() ?? null;
1169
+ return _shared.fallbackDepth > 0 ? fallbackStore : null;
1170
+ }
1171
+ function isSSR() {
1172
+ return getSSRStore().ssr;
1173
+ }
1174
+
1175
+ // src/reactivity/domBinding.ts
1176
+ function domBinding(commit, ownerNode) {
1177
+ if (isSSR()) return () => {
1178
+ };
1179
+ return reactiveBinding2(commit, ownerNode);
1180
+ }
1181
+
1182
+ // src/utils/adoptThenable.ts
1183
+ function adoptThenable(value) {
1184
+ if (value === null || typeof value !== "object" && typeof value !== "function") return null;
1185
+ let then;
1186
+ try {
1187
+ then = value.then;
1188
+ } catch (error) {
1189
+ return Promise.reject(error);
1204
1190
  }
1205
- return [get, set];
1191
+ if (typeof then !== "function") return null;
1192
+ return new Promise((resolve, reject) => {
1193
+ queueMicrotask(() => {
1194
+ try {
1195
+ Reflect.apply(then, value, [resolve, reject]);
1196
+ } catch (error) {
1197
+ reject(error);
1198
+ }
1199
+ });
1200
+ });
1206
1201
  }
1207
1202
 
1208
1203
  // src/ui/form.ts
@@ -1232,10 +1227,14 @@ function maxLength(max2, message) {
1232
1227
  }
1233
1228
  function matchesPattern(regex, message = "Invalid format") {
1234
1229
  return (value) => {
1235
- if (value && !regex.test(value)) {
1236
- return message;
1230
+ if (!value) return null;
1231
+ const savedLastIndex = regex.lastIndex;
1232
+ regex.lastIndex = 0;
1233
+ try {
1234
+ return regex.test(value) ? null : message;
1235
+ } finally {
1236
+ regex.lastIndex = savedLastIndex;
1237
1237
  }
1238
- return null;
1239
1238
  };
1240
1239
  }
1241
1240
  function email(message = "Invalid email address") {
@@ -1284,13 +1283,13 @@ function bindField(field, extras) {
1284
1283
  const mergedOn = extraOn && typeof extraOn === "object" ? { ...extraOn, ...fieldOn } : fieldOn;
1285
1284
  const onElement = (el) => {
1286
1285
  if (el instanceof HTMLSelectElement && el.multiple) {
1287
- const stop = effect(() => {
1286
+ const stop = domBinding(() => {
1288
1287
  const v = field.value();
1289
1288
  const selected = Array.isArray(v) ? v.map(String) : [];
1290
1289
  for (const opt of Array.from(el.options)) {
1291
1290
  opt.selected = selected.includes(opt.value);
1292
1291
  }
1293
- });
1292
+ }, el);
1294
1293
  registerDisposer(el, stop);
1295
1294
  }
1296
1295
  if (typeof extraOnElement === "function") {
@@ -1310,6 +1309,7 @@ function form(config) {
1310
1309
  const fieldEntries = Object.entries(config);
1311
1310
  const fieldMap = {};
1312
1311
  const [manualErrors, setManualErrors] = signal({});
1312
+ const fieldErrors = [];
1313
1313
  for (const [name, cfg] of fieldEntries) {
1314
1314
  const [value, setValue] = signal(cfg.initial);
1315
1315
  const [isTouched, setTouched] = signal(false);
@@ -1324,6 +1324,7 @@ function form(config) {
1324
1324
  }
1325
1325
  return null;
1326
1326
  });
1327
+ fieldErrors.push(error);
1327
1328
  const wrappedSet = (next) => {
1328
1329
  setValue(next);
1329
1330
  setManualErrors((prev) => {
@@ -1400,12 +1401,22 @@ function form(config) {
1400
1401
  field.touch();
1401
1402
  }
1402
1403
  if (isValid()) {
1403
- const result = onSubmit(values());
1404
- if (result && typeof result.then === "function") {
1404
+ const report = (error) => reportError(error, { phase: "async", name: "form.handleSubmit" });
1405
+ let pending;
1406
+ try {
1407
+ pending = adoptThenable(onSubmit(values()));
1408
+ } catch (error) {
1409
+ report(error);
1410
+ return;
1411
+ }
1412
+ if (pending) {
1405
1413
  setSubmitting(true);
1406
- result.then(
1414
+ pending.then(
1407
1415
  () => setSubmitting(false),
1408
- () => setSubmitting(false)
1416
+ (error) => {
1417
+ setSubmitting(false);
1418
+ report(error);
1419
+ }
1409
1420
  );
1410
1421
  }
1411
1422
  }
@@ -1419,6 +1430,17 @@ function form(config) {
1419
1430
  function setError(field, message) {
1420
1431
  setManualErrors((prev) => ({ ...prev, [field]: message }));
1421
1432
  }
1433
+ let disposed = false;
1434
+ function dispose2() {
1435
+ if (disposed) return;
1436
+ disposed = true;
1437
+ errors.dispose();
1438
+ isValid.dispose();
1439
+ isDirty.dispose();
1440
+ touchedState.dispose();
1441
+ values.dispose();
1442
+ for (const error of fieldErrors) error.dispose();
1443
+ }
1422
1444
  return {
1423
1445
  fields: fieldMap,
1424
1446
  errors,
@@ -1429,7 +1451,8 @@ function form(config) {
1429
1451
  values,
1430
1452
  handleSubmit,
1431
1453
  reset,
1432
- setError
1454
+ setError,
1455
+ dispose: dispose2
1433
1456
  };
1434
1457
  }
1435
1458
 
@@ -1508,7 +1531,7 @@ function VirtualList(props) {
1508
1531
  content.appendChild(itemEl);
1509
1532
  }
1510
1533
  };
1511
- registerDisposer(container, effect(update));
1534
+ registerDisposer(container, domBinding(update, container));
1512
1535
  return container;
1513
1536
  }
1514
1537
 
@@ -1518,11 +1541,14 @@ function intersection(options) {
1518
1541
  const [ratio, setRatio] = signal(0);
1519
1542
  let observer = null;
1520
1543
  let currentElement = null;
1544
+ let generation = 0;
1521
1545
  function observe(element) {
1522
1546
  if (typeof IntersectionObserver === "undefined") return;
1523
1547
  unobserve();
1524
1548
  currentElement = element;
1549
+ const observation = ++generation;
1525
1550
  observer = new IntersectionObserver((entries) => {
1551
+ if (observation !== generation) return;
1526
1552
  const entry = entries[0];
1527
1553
  if (entry) {
1528
1554
  setIsIntersecting(entry.isIntersecting);
@@ -1532,8 +1558,10 @@ function intersection(options) {
1532
1558
  observer.observe(element);
1533
1559
  }
1534
1560
  function unobserve() {
1561
+ generation++;
1535
1562
  if (observer) {
1536
1563
  if (currentElement) observer.unobserve(currentElement);
1564
+ observer.takeRecords();
1537
1565
  observer.disconnect();
1538
1566
  observer = null;
1539
1567
  currentElement = null;
@@ -1552,17 +1580,26 @@ function lazyLoad(element, loader, options) {
1552
1580
  return () => {
1553
1581
  };
1554
1582
  }
1583
+ let done = false;
1584
+ const stop = () => {
1585
+ done = true;
1586
+ observer.takeRecords();
1587
+ observer.disconnect();
1588
+ };
1555
1589
  const observer = new IntersectionObserver((entries) => {
1590
+ if (done) return;
1556
1591
  for (const entry of entries) {
1557
1592
  if (entry.isIntersecting) {
1593
+ stop();
1558
1594
  loader();
1559
- observer.disconnect();
1560
1595
  break;
1561
1596
  }
1562
1597
  }
1563
1598
  }, options);
1564
1599
  observer.observe(element);
1565
- return () => observer.disconnect();
1600
+ return () => {
1601
+ if (!done) stop();
1602
+ };
1566
1603
  }
1567
1604
 
1568
1605
  // src/ui/inputMask.ts
@@ -1717,6 +1754,7 @@ function focus() {
1717
1754
  const dispose2 = () => {
1718
1755
  if (disposed) return;
1719
1756
  disposed = true;
1757
+ unregisterDisposer(element, dispose2);
1720
1758
  element.removeEventListener("focus", onFocus);
1721
1759
  element.removeEventListener("blur", onBlur);
1722
1760
  if (currentElement === element) currentElement = null;
@@ -1785,14 +1823,19 @@ function FocusTrap(nodes, options = {}) {
1785
1823
  }
1786
1824
  };
1787
1825
  container.addEventListener("keydown", onTrapKeydown);
1826
+ let disposed = false;
1788
1827
  if (options.autoFocus !== false) {
1789
1828
  queueMicrotask(() => {
1829
+ if (disposed || !container.isConnected) return;
1790
1830
  const first = getFocusable()[0];
1791
1831
  first?.focus();
1792
1832
  });
1793
1833
  }
1794
1834
  let trapObserver = null;
1795
1835
  function restoreFocusAndCleanup() {
1836
+ if (disposed) return;
1837
+ disposed = true;
1838
+ unregisterDisposer(container, restoreFocusAndCleanup);
1796
1839
  if (options.restoreFocus !== false) previouslyFocused?.focus();
1797
1840
  container.removeEventListener("keydown", onTrapKeydown);
1798
1841
  if (trapObserver) {
@@ -1807,29 +1850,45 @@ function FocusTrap(nodes, options = {}) {
1807
1850
  }
1808
1851
  });
1809
1852
  queueMicrotask(() => {
1810
- if (container.isConnected) {
1811
- trapObserver.observe(container, { childList: true, subtree: true });
1812
- }
1853
+ const observer = trapObserver;
1854
+ if (disposed || !container.isConnected || !observer) return;
1855
+ observer.observe(container, { childList: true, subtree: true });
1813
1856
  });
1814
1857
  }
1815
1858
  registerDisposer(container, restoreFocusAndCleanup);
1816
1859
  return container;
1817
1860
  }
1861
+ function isApplePlatform() {
1862
+ if (typeof navigator === "undefined") return false;
1863
+ const nav = navigator;
1864
+ return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || "");
1865
+ }
1818
1866
  function hotkey(combo, handler, options = {}) {
1819
1867
  let key = combo;
1820
1868
  let needCtrl = options.ctrl ?? false;
1821
1869
  let needShift = options.shift ?? false;
1822
1870
  let needAlt = options.alt ?? false;
1823
1871
  let needMeta = options.meta ?? false;
1824
- if (combo.includes("+")) {
1825
- const parts = combo.toLowerCase().split("+");
1872
+ if (combo === "+") {
1873
+ key = "+";
1874
+ } else if (combo.includes("+")) {
1875
+ const literalPlus = combo.endsWith("++");
1876
+ const parts = (literalPlus ? combo.slice(0, -2) : combo).toLowerCase().split("+");
1877
+ if (literalPlus) parts.push("+");
1826
1878
  key = parts[parts.length - 1];
1879
+ if (key === "") throw new Error(`hotkey("${combo}"): missing key`);
1827
1880
  for (let i = 0; i < parts.length - 1; i++) {
1828
1881
  const mod = parts[i];
1829
1882
  if (mod === "ctrl" || mod === "control") needCtrl = true;
1830
1883
  else if (mod === "shift") needShift = true;
1831
- else if (mod === "alt") needAlt = true;
1884
+ else if (mod === "alt" || mod === "option") needAlt = true;
1832
1885
  else if (mod === "meta" || mod === "cmd" || mod === "command") needMeta = true;
1886
+ else if (mod === "mod") {
1887
+ if (isApplePlatform()) needMeta = true;
1888
+ else needCtrl = true;
1889
+ } else {
1890
+ throw new Error(`hotkey("${combo}"): unknown modifier "${mod}"`);
1891
+ }
1833
1892
  }
1834
1893
  }
1835
1894
  const listener = (e) => {
@@ -1895,6 +1954,11 @@ function announce(message, priority = "polite") {
1895
1954
  var COUNTER_KEY = /* @__PURE__ */ Symbol.for("sibujs.createId.v1");
1896
1955
  var _counter = globalThis[COUNTER_KEY] ?? (globalThis[COUNTER_KEY] = { n: 0 });
1897
1956
  function createId(prefix = "sibu") {
1957
+ const request = getRequestStore();
1958
+ if (request) {
1959
+ request.idCounter = (request.idCounter ?? 0) + 1;
1960
+ return `${prefix}-${request.idCounter}`;
1961
+ }
1898
1962
  _counter.n++;
1899
1963
  return `${prefix}-${_counter.n}`;
1900
1964
  }
@@ -1954,22 +2018,43 @@ function createListbox(container, options = {}) {
1954
2018
  if (multiple) container.setAttribute("aria-multiselectable", "true");
1955
2019
  if (!container.hasAttribute("tabindex")) container.setAttribute("tabindex", "0");
1956
2020
  const [activeValue, setActiveValue] = signal(null);
2021
+ const [selectedValues, setSelectedValues] = signal([]);
1957
2022
  const [selectedValue, setSelectedValue] = signal(null);
1958
2023
  const [activeDescendantId, setActiveDescendantId] = signal(null);
1959
- function stampIds() {
2024
+ let activeValueRef = null;
2025
+ let currentSelection = [];
2026
+ function reconcileOptions() {
1960
2027
  const opts = Array.from(container.querySelectorAll(optionSelector));
2028
+ const selectedSet = new Set(currentSelection);
2029
+ let activeStillPresent = false;
2030
+ const active = activeValueRef;
1961
2031
  for (const opt of opts) {
1962
2032
  if (!opt.id) opt.id = createId("listbox-option");
1963
- if (!opt.hasAttribute("aria-selected")) opt.setAttribute("aria-selected", "false");
2033
+ const ov = opt.dataset.value;
2034
+ if (!opt.hasAttribute("aria-selected")) {
2035
+ opt.setAttribute("aria-selected", ov !== void 0 && selectedSet.has(ov) ? "true" : "false");
2036
+ }
2037
+ if (active !== null && ov === active) activeStillPresent = true;
1964
2038
  }
2039
+ if (active !== null && !activeStillPresent) setActive(null, opts);
2040
+ return opts;
1965
2041
  }
1966
- stampIds();
1967
2042
  function getOptions() {
1968
- return Array.from(container.querySelectorAll(optionSelector));
2043
+ return reconcileOptions();
2044
+ }
2045
+ function isDisabled(opt) {
2046
+ return opt.getAttribute("aria-disabled") === "true" || opt.hasAttribute("disabled");
1969
2047
  }
1970
- function setActive(value) {
2048
+ function enabledOptions() {
2049
+ return getOptions().filter((opt) => !isDisabled(opt) && opt.dataset.value !== void 0);
2050
+ }
2051
+ function optionByValue(value) {
2052
+ return getOptions().find((opt) => opt.dataset.value === value);
2053
+ }
2054
+ function setActive(value, known) {
2055
+ activeValueRef = value;
1971
2056
  setActiveValue(value);
1972
- const opts = getOptions();
2057
+ const opts = known ?? getOptions();
1973
2058
  for (const opt of opts) {
1974
2059
  if (opt.dataset.value === value) {
1975
2060
  opt.setAttribute("data-highlighted", "");
@@ -1985,29 +2070,33 @@ function createListbox(container, options = {}) {
1985
2070
  }
1986
2071
  }
1987
2072
  function select(value) {
1988
- const previous = selectedValue();
1989
- let nextSelectedSet;
2073
+ const target = optionByValue(value);
2074
+ if (!target || isDisabled(target)) return;
2075
+ const previous = selectedValues();
2076
+ let next;
1990
2077
  if (multiple) {
1991
- nextSelectedSet = new Set((previous ?? "").split(",").filter(Boolean));
1992
- if (nextSelectedSet.has(value)) nextSelectedSet.delete(value);
1993
- else nextSelectedSet.add(value);
1994
- setSelectedValue(Array.from(nextSelectedSet).join(","));
2078
+ next = previous.includes(value) ? previous.filter((v) => v !== value) : [...previous, value];
1995
2079
  } else {
1996
- nextSelectedSet = /* @__PURE__ */ new Set([value]);
1997
- setSelectedValue(value);
2080
+ next = [value];
1998
2081
  }
2082
+ currentSelection = next;
2083
+ batch(() => {
2084
+ setSelectedValues(next);
2085
+ setSelectedValue(multiple ? next.join(",") : value);
2086
+ });
1999
2087
  options.onSelect?.(value);
2088
+ const nextSelectedSet = new Set(next);
2000
2089
  const opts = getOptions();
2001
2090
  for (const opt of opts) {
2002
- const ov = opt.dataset.value ?? "";
2003
- opt.setAttribute("aria-selected", nextSelectedSet.has(ov) ? "true" : "false");
2091
+ const ov = opt.dataset.value;
2092
+ opt.setAttribute("aria-selected", ov !== void 0 && nextSelectedSet.has(ov) ? "true" : "false");
2004
2093
  }
2005
2094
  }
2006
2095
  function moveActive(delta) {
2007
- const opts = getOptions();
2096
+ const opts = enabledOptions();
2008
2097
  if (opts.length === 0) return;
2009
- const currentIdx = opts.findIndex((o) => o.dataset.value === activeValue());
2010
- let next = currentIdx + delta;
2098
+ const currentIdx = opts.findIndex((o) => o.dataset.value === activeValueRef);
2099
+ let next = currentIdx === -1 ? delta > 0 ? 0 : opts.length - 1 : currentIdx + delta;
2011
2100
  if (next < 0) next = opts.length - 1;
2012
2101
  if (next >= opts.length) next = 0;
2013
2102
  const nextValue = opts[next].dataset.value ?? null;
@@ -2028,13 +2117,13 @@ function createListbox(container, options = {}) {
2028
2117
  break;
2029
2118
  case "Home": {
2030
2119
  e.preventDefault();
2031
- const opts = getOptions();
2120
+ const opts = enabledOptions();
2032
2121
  if (opts.length > 0) setActive(opts[0].dataset.value ?? null);
2033
2122
  break;
2034
2123
  }
2035
2124
  case "End": {
2036
2125
  e.preventDefault();
2037
- const opts = getOptions();
2126
+ const opts = enabledOptions();
2038
2127
  if (opts.length > 0) setActive(opts[opts.length - 1].dataset.value ?? null);
2039
2128
  break;
2040
2129
  }
@@ -2049,7 +2138,7 @@ function createListbox(container, options = {}) {
2049
2138
  }
2050
2139
  function onClick(e) {
2051
2140
  const target = e.target.closest(optionSelector);
2052
- if (!target || !container.contains(target)) return;
2141
+ if (!target || !container.contains(target) || isDisabled(target)) return;
2053
2142
  const value = target.dataset.value ?? null;
2054
2143
  if (value !== null) {
2055
2144
  setActive(value);
@@ -2058,12 +2147,26 @@ function createListbox(container, options = {}) {
2058
2147
  }
2059
2148
  container.addEventListener("keydown", onKeyDown);
2060
2149
  container.addEventListener("click", onClick);
2150
+ reconcileOptions();
2151
+ let observer = null;
2152
+ if (typeof MutationObserver !== "undefined") {
2153
+ observer = new MutationObserver(() => {
2154
+ if (!disposed) reconcileOptions();
2155
+ });
2156
+ observer.observe(container, { childList: true, subtree: true });
2157
+ }
2158
+ let disposed = false;
2061
2159
  function dispose2() {
2160
+ if (disposed) return;
2161
+ disposed = true;
2162
+ unregisterDisposer(container, dispose2);
2163
+ observer?.disconnect();
2164
+ observer = null;
2062
2165
  container.removeEventListener("keydown", onKeyDown);
2063
2166
  container.removeEventListener("click", onClick);
2064
2167
  }
2065
2168
  registerDisposer(container, dispose2);
2066
- return { activeValue, selectedValue, activeDescendantId, dispose: dispose2 };
2169
+ return { activeValue, selectedValues, selectedValue, activeDescendantId, refresh: reconcileOptions, dispose: dispose2 };
2067
2170
  }
2068
2171
  function createDialogAria(element, options = {}) {
2069
2172
  const titleId = options.labelledBy ?? createId("dialog-title");
@@ -2686,6 +2789,8 @@ function removeGlobalListenerIfIdle() {
2686
2789
  function dialog() {
2687
2790
  const [isOpen, setIsOpen] = signal(false);
2688
2791
  const entry = { close: () => close() };
2792
+ let opened = false;
2793
+ let disposed = false;
2689
2794
  function pushOnStack() {
2690
2795
  if (dialogStack.indexOf(entry) !== -1) return;
2691
2796
  dialogStack.push(entry);
@@ -2697,23 +2802,27 @@ function dialog() {
2697
2802
  removeGlobalListenerIfIdle();
2698
2803
  }
2699
2804
  function open() {
2700
- if (isOpen()) return;
2701
- setIsOpen(true);
2805
+ if (disposed || opened) return;
2806
+ opened = true;
2702
2807
  pushOnStack();
2808
+ setIsOpen(true);
2703
2809
  }
2704
2810
  function close() {
2705
- if (!isOpen()) {
2811
+ if (!opened) {
2706
2812
  removeFromStack();
2707
2813
  return;
2708
2814
  }
2709
- setIsOpen(false);
2815
+ opened = false;
2710
2816
  removeFromStack();
2817
+ setIsOpen(false);
2711
2818
  }
2712
2819
  function toggle() {
2713
- if (isOpen()) close();
2820
+ if (opened) close();
2714
2821
  else open();
2715
2822
  }
2716
2823
  function dispose2() {
2824
+ disposed = true;
2825
+ opened = false;
2717
2826
  removeFromStack();
2718
2827
  setIsOpen(false);
2719
2828
  }
@@ -2918,37 +3027,183 @@ function pagination(options) {
2918
3027
  function eventBus() {
2919
3028
  const listeners = /* @__PURE__ */ new Map();
2920
3029
  function on(event, handler) {
2921
- let set = listeners.get(event);
2922
- if (!set) {
2923
- set = /* @__PURE__ */ new Set();
2924
- listeners.set(event, set);
3030
+ let subs = listeners.get(event);
3031
+ if (!subs) {
3032
+ subs = /* @__PURE__ */ new Map();
3033
+ listeners.set(event, subs);
3034
+ }
3035
+ let subscription = subs.get(handler);
3036
+ if (!subscription) {
3037
+ subscription = { handler, active: true };
3038
+ subs.set(handler, subscription);
2925
3039
  }
2926
- set.add(handler);
2927
- return () => off(event, handler);
3040
+ const own = subscription;
3041
+ return () => {
3042
+ if (listeners.get(event)?.get(handler) !== own) return;
3043
+ off(event, handler);
3044
+ };
2928
3045
  }
2929
3046
  function emit(event, data) {
2930
- const set = listeners.get(event);
2931
- if (set) {
2932
- for (const handler of set) {
2933
- handler(data);
3047
+ const subs = listeners.get(event);
3048
+ if (!subs || subs.size === 0) return;
3049
+ for (const subscription of Array.from(subs.values())) {
3050
+ if (!subscription.active) continue;
3051
+ try {
3052
+ subscription.handler(data);
3053
+ } catch (err) {
3054
+ reportError(err, { phase: "event", name: `eventBus(${String(event)})` });
2934
3055
  }
2935
3056
  }
2936
3057
  }
2937
3058
  function off(event, handler) {
2938
- const set = listeners.get(event);
2939
- if (set) {
2940
- set.delete(handler);
2941
- if (set.size === 0) {
2942
- listeners.delete(event);
2943
- }
3059
+ const subs = listeners.get(event);
3060
+ if (!subs) return;
3061
+ const subscription = subs.get(handler);
3062
+ if (!subscription) return;
3063
+ subscription.active = false;
3064
+ subs.delete(handler);
3065
+ if (subs.size === 0) {
3066
+ listeners.delete(event);
2944
3067
  }
2945
3068
  }
2946
3069
  function clear() {
3070
+ for (const subs of listeners.values()) {
3071
+ for (const subscription of subs.values()) subscription.active = false;
3072
+ }
2947
3073
  listeners.clear();
2948
3074
  }
2949
3075
  return { on, emit, off, clear };
2950
3076
  }
2951
3077
 
3078
+ // src/core/signals/effect.ts
3079
+ var _g2 = globalThis;
3080
+ var MAX_RERUNS = 100;
3081
+ function flushUserCleanups(ctx) {
3082
+ const list = ctx.userCleanups;
3083
+ if (list.length === 0) return;
3084
+ ctx.userCleanups = [];
3085
+ for (let i = list.length - 1; i >= 0; i--) {
3086
+ try {
3087
+ list[i]();
3088
+ } catch (err) {
3089
+ reportError(err, { phase: "cleanup" });
3090
+ }
3091
+ }
3092
+ }
3093
+ function drainReruns(ctx) {
3094
+ let reruns = 1;
3095
+ do {
3096
+ ctx.rerunPending = false;
3097
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
3098
+ retrack2(ctx.bodyFn, ctx.subscriber);
3099
+ } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
3100
+ if (ctx.rerunPending) {
3101
+ ctx.rerunPending = false;
3102
+ reportError(
3103
+ new Error(
3104
+ `effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
3105
+ ),
3106
+ { phase: "effect" }
3107
+ );
3108
+ }
3109
+ }
3110
+ function disposeEffect(ctx) {
3111
+ if (ctx.disposed) return;
3112
+ ctx.disposed = true;
3113
+ const h = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
3114
+ if (h) {
3115
+ try {
3116
+ h.emit("effect:destroy", { effectFn: ctx.fn });
3117
+ } catch {
3118
+ }
3119
+ }
3120
+ try {
3121
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
3122
+ } catch (err) {
3123
+ if (typeof console !== "undefined") {
3124
+ console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
3125
+ }
3126
+ }
3127
+ try {
3128
+ cleanup2(ctx.subscriber);
3129
+ } catch (err) {
3130
+ if (typeof console !== "undefined") {
3131
+ console.warn("[SibuJS effect] dispose threw:", err);
3132
+ }
3133
+ }
3134
+ }
3135
+ function effect(effectFn, options) {
3136
+ devAssert(typeof effectFn === "function", "effect: argument must be a function.");
3137
+ if (isSSR()) return () => {
3138
+ };
3139
+ const ctx = {
3140
+ fn: effectFn,
3141
+ onError: options?.onError,
3142
+ userCleanups: [],
3143
+ running: false,
3144
+ rerunPending: false,
3145
+ disposed: false,
3146
+ onCleanup: null,
3147
+ subscriber: null,
3148
+ bodyFn: null
3149
+ };
3150
+ ctx.onCleanup = (fn) => {
3151
+ ctx.userCleanups.push(fn);
3152
+ };
3153
+ const onErrorCaptured = ctx.onError;
3154
+ ctx.bodyFn = onErrorCaptured ? () => {
3155
+ try {
3156
+ ctx.fn(ctx.onCleanup);
3157
+ } catch (err) {
3158
+ onErrorCaptured(err);
3159
+ }
3160
+ } : () => {
3161
+ ctx.fn(ctx.onCleanup);
3162
+ };
3163
+ const sub = (() => {
3164
+ if (ctx.disposed) return;
3165
+ if (ctx.running) {
3166
+ ctx.rerunPending = true;
3167
+ return;
3168
+ }
3169
+ ctx.running = true;
3170
+ try {
3171
+ ctx.rerunPending = false;
3172
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
3173
+ retrack2(ctx.bodyFn, sub);
3174
+ if (ctx.rerunPending) drainReruns(ctx);
3175
+ } finally {
3176
+ ctx.running = false;
3177
+ ctx.rerunPending = false;
3178
+ }
3179
+ });
3180
+ sub.depsHead = null;
3181
+ sub.depsTail = null;
3182
+ sub._epoch = 0;
3183
+ sub._structDirty = false;
3184
+ sub._hasComputedDep = false;
3185
+ sub._errorPhase = "effect";
3186
+ sub._runEpoch = 0;
3187
+ sub._runs = 0;
3188
+ ctx.subscriber = sub;
3189
+ ctx.running = true;
3190
+ try {
3191
+ retrack2(ctx.bodyFn, ctx.subscriber);
3192
+ if (ctx.rerunPending) drainReruns(ctx);
3193
+ } catch (err) {
3194
+ ctx.disposed = true;
3195
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
3196
+ cleanup2(ctx.subscriber);
3197
+ throw err;
3198
+ } finally {
3199
+ ctx.running = false;
3200
+ ctx.rerunPending = false;
3201
+ }
3202
+ const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
3203
+ if (hook) emitDevtools(hook, "effect:create", { effectFn });
3204
+ return () => disposeEffect(ctx);
3205
+ }
3206
+
2952
3207
  // src/ui/lazyEffect.ts
2953
3208
  function lazyEffect(element, effectFn, options) {
2954
3209
  if (typeof IntersectionObserver === "undefined") {
@@ -2984,25 +3239,59 @@ function lazyEffect(element, effectFn, options) {
2984
3239
 
2985
3240
  // src/ui/timers.ts
2986
3241
  function interval(fn, ms) {
2987
- let id = null;
3242
+ let intervalId = null;
3243
+ let resumeId = null;
2988
3244
  let running = false;
2989
- function start() {
3245
+ let nextDue = 0;
3246
+ let remaining = ms;
3247
+ function tick() {
3248
+ nextDue = Date.now() + ms;
3249
+ fn();
3250
+ }
3251
+ function clearTimers() {
3252
+ if (intervalId !== null) {
3253
+ clearInterval(intervalId);
3254
+ intervalId = null;
3255
+ }
3256
+ if (resumeId !== null) {
3257
+ clearTimeout(resumeId);
3258
+ resumeId = null;
3259
+ }
3260
+ }
3261
+ function startRegular() {
3262
+ nextDue = Date.now() + ms;
3263
+ intervalId = setInterval(tick, ms);
3264
+ }
3265
+ function resume() {
2990
3266
  if (running) return;
2991
- id = setInterval(fn, ms);
2992
3267
  running = true;
3268
+ if (remaining >= ms) {
3269
+ startRegular();
3270
+ return;
3271
+ }
3272
+ nextDue = Date.now() + remaining;
3273
+ resumeId = setTimeout(() => {
3274
+ resumeId = null;
3275
+ startRegular();
3276
+ fn();
3277
+ }, remaining);
3278
+ }
3279
+ function pause() {
3280
+ if (!running) return;
3281
+ remaining = Math.min(ms, Math.max(0, nextDue - Date.now()));
3282
+ clearTimers();
3283
+ running = false;
2993
3284
  }
2994
3285
  function stop() {
2995
- if (id !== null) {
2996
- clearInterval(id);
2997
- id = null;
2998
- }
3286
+ clearTimers();
2999
3287
  running = false;
3288
+ remaining = ms;
3000
3289
  }
3001
- start();
3290
+ resume();
3002
3291
  return {
3003
3292
  stop,
3004
- pause: stop,
3005
- resume: start,
3293
+ pause,
3294
+ resume,
3006
3295
  isRunning: () => running
3007
3296
  };
3008
3297
  }
@@ -3034,7 +3323,11 @@ function hover(target) {
3034
3323
  const onLeave = () => setHovered(false);
3035
3324
  target.addEventListener("pointerenter", onEnter);
3036
3325
  target.addEventListener("pointerleave", onLeave);
3326
+ let disposed = false;
3037
3327
  function dispose2() {
3328
+ if (disposed) return;
3329
+ disposed = true;
3330
+ unregisterDisposer(target, dispose2);
3038
3331
  target.removeEventListener("pointerenter", onEnter);
3039
3332
  target.removeEventListener("pointerleave", onLeave);
3040
3333
  }
@@ -3048,21 +3341,37 @@ var _lock = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.scrollLock.v1"),
3048
3341
  savedOverflow: null,
3049
3342
  savedPaddingRight: null
3050
3343
  }));
3344
+ function applyBodyLock() {
3345
+ const body = document.body;
3346
+ if (!body) throw new Error("[scrollLock] document.body is not available yet");
3347
+ const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
3348
+ const savedOverflow = body.style.overflow;
3349
+ const savedPaddingRight = body.style.paddingRight;
3350
+ try {
3351
+ body.style.overflow = "hidden";
3352
+ if (scrollBarWidth > 0) {
3353
+ body.style.paddingRight = `${scrollBarWidth}px`;
3354
+ }
3355
+ } catch (err) {
3356
+ try {
3357
+ body.style.overflow = savedOverflow;
3358
+ body.style.paddingRight = savedPaddingRight;
3359
+ } catch {
3360
+ }
3361
+ throw err;
3362
+ }
3363
+ _lock.savedOverflow = savedOverflow;
3364
+ _lock.savedPaddingRight = savedPaddingRight;
3365
+ }
3051
3366
  function scrollLock() {
3052
3367
  let owned = false;
3053
3368
  function lock() {
3054
3369
  if (owned) return;
3370
+ if (_lock.count === 0 && typeof document !== "undefined") {
3371
+ applyBodyLock();
3372
+ }
3055
3373
  owned = true;
3056
3374
  _lock.count++;
3057
- if (_lock.count !== 1 || typeof document === "undefined") return;
3058
- const body = document.body;
3059
- const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
3060
- _lock.savedOverflow = body.style.overflow;
3061
- _lock.savedPaddingRight = body.style.paddingRight;
3062
- body.style.overflow = "hidden";
3063
- if (scrollBarWidth > 0) {
3064
- body.style.paddingRight = `${scrollBarWidth}px`;
3065
- }
3066
3375
  }
3067
3376
  function unlock() {
3068
3377
  if (!owned) return;
@@ -3080,13 +3389,33 @@ function scrollLock() {
3080
3389
 
3081
3390
  // src/platform/customElement.ts
3082
3391
  var SVG_NS2 = "http://www.w3.org/2000/svg";
3392
+ var MAX_RENDER_PASSES = 10;
3393
+ var ABANDONED_RENDER = /* @__PURE__ */ Symbol("sibujs.defineElement.abandonedRender");
3083
3394
  function defineElement(name, component, options = {}) {
3084
3395
  if (customElements.get(name)) return;
3085
3396
  const observed = options.observedAttributes || [];
3086
3397
  class SibuElement extends HTMLElement {
3087
3398
  constructor() {
3088
3399
  super();
3089
- this._rendered = null;
3400
+ // Re-rendering is keyed on connection, not on a previous render: a first render
3401
+ // that throws leaves nothing rendered, and the element must still re-render
3402
+ // when a later attribute change fixes the input.
3403
+ this._connected = false;
3404
+ // A render in progress, and whether an attribute changed during it. The
3405
+ // component may write its host's observed attributes while rendering; with
3406
+ // the old subtree kept during the build, rendering again from inside the
3407
+ // callback recursed until the stack overflowed.
3408
+ this._rendering = false;
3409
+ this._dirty = false;
3410
+ // How many teardowns are draining. A host teardown may reconnect the element
3411
+ // (and a nested disconnect may run inside it), and rendering the next
3412
+ // generation from inside that drain would let the drain tear down what the
3413
+ // new render registers. A counter, not a flag: a nested teardown returning
3414
+ // must not declare the outer one finished.
3415
+ this._teardownDepth = 0;
3416
+ // Disconnects so far. A render compares it across its old-subtree teardown
3417
+ // to detect a disconnect that happened inside it.
3418
+ this._disconnects = 0;
3090
3419
  if (options.shadow !== false) {
3091
3420
  this._root = this.attachShadow({ mode: options.mode || "open" });
3092
3421
  } else {
@@ -3096,32 +3425,106 @@ function defineElement(name, component, options = {}) {
3096
3425
  static get observedAttributes() {
3097
3426
  return observed;
3098
3427
  }
3428
+ /** Whether rendering must wait for work already in progress. */
3429
+ get _busy() {
3430
+ return this._rendering || this._teardownDepth > 0;
3431
+ }
3099
3432
  connectedCallback() {
3433
+ this._connected = true;
3434
+ if (this._busy) {
3435
+ this._dirty = true;
3436
+ return;
3437
+ }
3100
3438
  this._render();
3101
3439
  }
3102
3440
  disconnectedCallback() {
3103
- this._teardown();
3441
+ this._connected = false;
3442
+ this._disconnects++;
3443
+ this._teardownDepth++;
3444
+ try {
3445
+ this._teardown();
3446
+ } finally {
3447
+ this._teardownDepth--;
3448
+ if (this._teardownDepth === 0 && this._connected && this._dirty) {
3449
+ this._dirty = false;
3450
+ if (!this._rendering) this._render();
3451
+ }
3452
+ }
3104
3453
  }
3105
- attributeChangedCallback() {
3106
- if (this._rendered) {
3107
- this._render();
3454
+ attributeChangedCallback(_name, oldValue, newValue) {
3455
+ if (oldValue === newValue || !this._connected) return;
3456
+ if (this._busy) {
3457
+ this._dirty = true;
3458
+ return;
3108
3459
  }
3460
+ this._render();
3109
3461
  }
3110
3462
  _teardown() {
3111
- this._rendered = null;
3112
3463
  replaceChildrenSafely(this._root);
3113
- }
3464
+ disposeNodeOwn(this);
3465
+ }
3466
+ /**
3467
+ * Render as a transaction: build the replacement first, commit only if that
3468
+ * succeeds. Tearing the current subtree down before calling the factory
3469
+ * meant a throwing rerender (an invalid attribute, say) left the element
3470
+ * blank with its live state already disposed. Now a failure keeps the
3471
+ * working subtree, releases whatever the failed attempt registered, and is
3472
+ * reported with this element as its node so an enclosing ErrorBoundary can
3473
+ * claim it.
3474
+ */
3114
3475
  _render() {
3115
- this._teardown();
3476
+ this._rendering = true;
3477
+ try {
3478
+ let passes = 0;
3479
+ do {
3480
+ this._dirty = false;
3481
+ if (++passes > MAX_RENDER_PASSES) {
3482
+ reportError(
3483
+ new Error(
3484
+ `[SibuJS] defineElement(${name}): the component changed its own observed attributes on ${MAX_RENDER_PASSES} consecutive renders; stopped re-rendering.`
3485
+ ),
3486
+ { phase: "render", name: `defineElement(${name})`, node: this }
3487
+ );
3488
+ break;
3489
+ }
3490
+ this._renderOnce();
3491
+ } while (this._dirty && this._connected);
3492
+ } finally {
3493
+ this._rendering = false;
3494
+ this._dirty = false;
3495
+ }
3496
+ }
3497
+ _renderOnce() {
3116
3498
  const props = this._getProps();
3499
+ let el;
3500
+ try {
3501
+ el = withDisposerRollback(() => {
3502
+ const built = component(props, this);
3503
+ if (!this._connected) throw ABANDONED_RENDER;
3504
+ return built;
3505
+ });
3506
+ } catch (err) {
3507
+ if (err !== ABANDONED_RENDER) {
3508
+ reportError(err, { phase: "render", name: `defineElement(${name})`, node: this });
3509
+ }
3510
+ return;
3511
+ }
3512
+ const next = [];
3117
3513
  if (options.styles && this._root instanceof ShadowRoot) {
3118
3514
  const styleEl = document.createElement("style");
3119
3515
  styleEl.textContent = options.styles;
3120
- this._root.appendChild(styleEl);
3516
+ next.push(styleEl);
3517
+ }
3518
+ next.push(el);
3519
+ const disconnects = this._disconnects;
3520
+ for (const node of next) node.parentNode?.removeChild(node);
3521
+ replaceChildrenSafely(this._root);
3522
+ if (this._disconnects !== disconnects) {
3523
+ for (const node of next) dispose(node);
3524
+ if (this._connected) this._dirty = true;
3525
+ return;
3121
3526
  }
3122
- const el = component(props, this);
3123
- this._root.appendChild(el);
3124
- this._rendered = el;
3527
+ this._root.replaceChildren(...next);
3125
3528
  }
3126
3529
  _getProps() {
3127
3530
  const props = {};
@@ -3272,7 +3675,7 @@ function validateProps(props, schema) {
3272
3675
  const errors = [];
3273
3676
  for (const [key, def] of Object.entries(schema)) {
3274
3677
  const propDef = typeof def === "function" ? { type: def } : def;
3275
- if (result[key] == null && propDef.default !== void 0) {
3678
+ if (result[key] === void 0 && propDef.default !== void 0) {
3276
3679
  result[key] = typeof propDef.default === "function" ? propDef.default() : propDef.default;
3277
3680
  }
3278
3681
  if (propDef.required && result[key] == null) {
@@ -3298,7 +3701,7 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`);
3298
3701
  for (const [key, def] of Object.entries(schema)) {
3299
3702
  if (typeof def === "function") continue;
3300
3703
  const fallback = def.default;
3301
- if (result[key] == null && fallback !== void 0) {
3704
+ if (result[key] === void 0 && fallback !== void 0) {
3302
3705
  result[key] = typeof fallback === "function" ? fallback() : fallback;
3303
3706
  }
3304
3707
  }