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
@@ -1,33 +1,38 @@
1
+ import {
2
+ domBinding
3
+ } from "./chunk-3ISI6ACU.js";
1
4
  import {
2
5
  bindAttribute,
3
6
  setSafeAttribute
4
- } from "./chunk-VCTAEPSB.js";
7
+ } from "./chunk-47M47FOM.js";
5
8
  import {
6
9
  derived
7
- } from "./chunk-W2EQ7X2L.js";
10
+ } from "./chunk-QE4TTDU3.js";
8
11
  import {
9
12
  isEventHandlerAttr
10
- } from "./chunk-IXKSNWV5.js";
13
+ } from "./chunk-2INLLLMZ.js";
11
14
  import {
12
15
  globalSingleton
13
16
  } from "./chunk-7ZHH77QA.js";
14
17
  import {
15
- effect
16
- } from "./chunk-7GIHSAWB.js";
17
- import {
18
- dispose,
19
- registerDisposer,
20
- replaceChildrenSafely
21
- } from "./chunk-QKRPLZ2V.js";
18
+ adoptThenable
19
+ } from "./chunk-5HZXGZ6T.js";
22
20
  import {
23
21
  signal
24
- } from "./chunk-NVNJH22U.js";
22
+ } from "./chunk-D33YTSX5.js";
25
23
  import {
24
+ reactiveBinding,
26
25
  track
27
- } from "./chunk-3BTTCZ5J.js";
26
+ } from "./chunk-5Q4R7HCL.js";
28
27
  import {
29
- reportError
30
- } from "./chunk-2WLZ6757.js";
28
+ dispose,
29
+ disposeNodeOwn,
30
+ registerDisposer,
31
+ replaceChildrenSafely,
32
+ reportError,
33
+ unregisterDisposer,
34
+ withDisposerRollback
35
+ } from "./chunk-NYNYSPK7.js";
31
36
 
32
37
  // src/ui/form.ts
33
38
  function required(message = "This field is required") {
@@ -56,10 +61,14 @@ function maxLength(max2, message) {
56
61
  }
57
62
  function matchesPattern(regex, message = "Invalid format") {
58
63
  return (value) => {
59
- if (value && !regex.test(value)) {
60
- return message;
64
+ if (!value) return null;
65
+ const savedLastIndex = regex.lastIndex;
66
+ regex.lastIndex = 0;
67
+ try {
68
+ return regex.test(value) ? null : message;
69
+ } finally {
70
+ regex.lastIndex = savedLastIndex;
61
71
  }
62
- return null;
63
72
  };
64
73
  }
65
74
  function email(message = "Invalid email address") {
@@ -108,13 +117,13 @@ function bindField(field, extras) {
108
117
  const mergedOn = extraOn && typeof extraOn === "object" ? { ...extraOn, ...fieldOn } : fieldOn;
109
118
  const onElement = (el) => {
110
119
  if (el instanceof HTMLSelectElement && el.multiple) {
111
- const stop = effect(() => {
120
+ const stop = domBinding(() => {
112
121
  const v = field.value();
113
122
  const selected = Array.isArray(v) ? v.map(String) : [];
114
123
  for (const opt of Array.from(el.options)) {
115
124
  opt.selected = selected.includes(opt.value);
116
125
  }
117
- });
126
+ }, el);
118
127
  registerDisposer(el, stop);
119
128
  }
120
129
  if (typeof extraOnElement === "function") {
@@ -134,6 +143,7 @@ function form(config) {
134
143
  const fieldEntries = Object.entries(config);
135
144
  const fieldMap = {};
136
145
  const [manualErrors, setManualErrors] = signal({});
146
+ const fieldErrors = [];
137
147
  for (const [name, cfg] of fieldEntries) {
138
148
  const [value, setValue] = signal(cfg.initial);
139
149
  const [isTouched, setTouched] = signal(false);
@@ -148,6 +158,7 @@ function form(config) {
148
158
  }
149
159
  return null;
150
160
  });
161
+ fieldErrors.push(error);
151
162
  const wrappedSet = (next) => {
152
163
  setValue(next);
153
164
  setManualErrors((prev) => {
@@ -224,12 +235,22 @@ function form(config) {
224
235
  field.touch();
225
236
  }
226
237
  if (isValid()) {
227
- const result = onSubmit(values());
228
- if (result && typeof result.then === "function") {
238
+ const report = (error) => reportError(error, { phase: "async", name: "form.handleSubmit" });
239
+ let pending;
240
+ try {
241
+ pending = adoptThenable(onSubmit(values()));
242
+ } catch (error) {
243
+ report(error);
244
+ return;
245
+ }
246
+ if (pending) {
229
247
  setSubmitting(true);
230
- result.then(
248
+ pending.then(
231
249
  () => setSubmitting(false),
232
- () => setSubmitting(false)
250
+ (error) => {
251
+ setSubmitting(false);
252
+ report(error);
253
+ }
233
254
  );
234
255
  }
235
256
  }
@@ -243,6 +264,17 @@ function form(config) {
243
264
  function setError(field, message) {
244
265
  setManualErrors((prev) => ({ ...prev, [field]: message }));
245
266
  }
267
+ let disposed = false;
268
+ function dispose2() {
269
+ if (disposed) return;
270
+ disposed = true;
271
+ errors.dispose();
272
+ isValid.dispose();
273
+ isDirty.dispose();
274
+ touchedState.dispose();
275
+ values.dispose();
276
+ for (const error of fieldErrors) error.dispose();
277
+ }
246
278
  return {
247
279
  fields: fieldMap,
248
280
  errors,
@@ -253,7 +285,8 @@ function form(config) {
253
285
  values,
254
286
  handleSubmit,
255
287
  reset,
256
- setError
288
+ setError,
289
+ dispose: dispose2
257
290
  };
258
291
  }
259
292
 
@@ -297,7 +330,7 @@ function VirtualList(props) {
297
330
  content.appendChild(itemEl);
298
331
  }
299
332
  };
300
- registerDisposer(container, effect(update));
333
+ registerDisposer(container, domBinding(update, container));
301
334
  return container;
302
335
  }
303
336
 
@@ -307,11 +340,14 @@ function intersection(options) {
307
340
  const [ratio, setRatio] = signal(0);
308
341
  let observer = null;
309
342
  let currentElement = null;
343
+ let generation = 0;
310
344
  function observe(element) {
311
345
  if (typeof IntersectionObserver === "undefined") return;
312
346
  unobserve();
313
347
  currentElement = element;
348
+ const observation = ++generation;
314
349
  observer = new IntersectionObserver((entries) => {
350
+ if (observation !== generation) return;
315
351
  const entry = entries[0];
316
352
  if (entry) {
317
353
  setIsIntersecting(entry.isIntersecting);
@@ -321,8 +357,10 @@ function intersection(options) {
321
357
  observer.observe(element);
322
358
  }
323
359
  function unobserve() {
360
+ generation++;
324
361
  if (observer) {
325
362
  if (currentElement) observer.unobserve(currentElement);
363
+ observer.takeRecords();
326
364
  observer.disconnect();
327
365
  observer = null;
328
366
  currentElement = null;
@@ -341,17 +379,26 @@ function lazyLoad(element, loader, options) {
341
379
  return () => {
342
380
  };
343
381
  }
382
+ let done = false;
383
+ const stop = () => {
384
+ done = true;
385
+ observer.takeRecords();
386
+ observer.disconnect();
387
+ };
344
388
  const observer = new IntersectionObserver((entries) => {
389
+ if (done) return;
345
390
  for (const entry of entries) {
346
391
  if (entry.isIntersecting) {
392
+ stop();
347
393
  loader();
348
- observer.disconnect();
349
394
  break;
350
395
  }
351
396
  }
352
397
  }, options);
353
398
  observer.observe(element);
354
- return () => observer.disconnect();
399
+ return () => {
400
+ if (!done) stop();
401
+ };
355
402
  }
356
403
 
357
404
  // src/ui/inputMask.ts
@@ -500,6 +547,7 @@ function focus() {
500
547
  const dispose2 = () => {
501
548
  if (disposed) return;
502
549
  disposed = true;
550
+ unregisterDisposer(element, dispose2);
503
551
  element.removeEventListener("focus", onFocus);
504
552
  element.removeEventListener("blur", onBlur);
505
553
  if (currentElement === element) currentElement = null;
@@ -568,14 +616,19 @@ function FocusTrap(nodes, options = {}) {
568
616
  }
569
617
  };
570
618
  container.addEventListener("keydown", onTrapKeydown);
619
+ let disposed = false;
571
620
  if (options.autoFocus !== false) {
572
621
  queueMicrotask(() => {
622
+ if (disposed || !container.isConnected) return;
573
623
  const first = getFocusable()[0];
574
624
  first?.focus();
575
625
  });
576
626
  }
577
627
  let trapObserver = null;
578
628
  function restoreFocusAndCleanup() {
629
+ if (disposed) return;
630
+ disposed = true;
631
+ unregisterDisposer(container, restoreFocusAndCleanup);
579
632
  if (options.restoreFocus !== false) previouslyFocused?.focus();
580
633
  container.removeEventListener("keydown", onTrapKeydown);
581
634
  if (trapObserver) {
@@ -590,29 +643,45 @@ function FocusTrap(nodes, options = {}) {
590
643
  }
591
644
  });
592
645
  queueMicrotask(() => {
593
- if (container.isConnected) {
594
- trapObserver.observe(container, { childList: true, subtree: true });
595
- }
646
+ const observer = trapObserver;
647
+ if (disposed || !container.isConnected || !observer) return;
648
+ observer.observe(container, { childList: true, subtree: true });
596
649
  });
597
650
  }
598
651
  registerDisposer(container, restoreFocusAndCleanup);
599
652
  return container;
600
653
  }
654
+ function isApplePlatform() {
655
+ if (typeof navigator === "undefined") return false;
656
+ const nav = navigator;
657
+ return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || "");
658
+ }
601
659
  function hotkey(combo, handler, options = {}) {
602
660
  let key = combo;
603
661
  let needCtrl = options.ctrl ?? false;
604
662
  let needShift = options.shift ?? false;
605
663
  let needAlt = options.alt ?? false;
606
664
  let needMeta = options.meta ?? false;
607
- if (combo.includes("+")) {
608
- const parts = combo.toLowerCase().split("+");
665
+ if (combo === "+") {
666
+ key = "+";
667
+ } else if (combo.includes("+")) {
668
+ const literalPlus = combo.endsWith("++");
669
+ const parts = (literalPlus ? combo.slice(0, -2) : combo).toLowerCase().split("+");
670
+ if (literalPlus) parts.push("+");
609
671
  key = parts[parts.length - 1];
672
+ if (key === "") throw new Error(`hotkey("${combo}"): missing key`);
610
673
  for (let i = 0; i < parts.length - 1; i++) {
611
674
  const mod = parts[i];
612
675
  if (mod === "ctrl" || mod === "control") needCtrl = true;
613
676
  else if (mod === "shift") needShift = true;
614
- else if (mod === "alt") needAlt = true;
677
+ else if (mod === "alt" || mod === "option") needAlt = true;
615
678
  else if (mod === "meta" || mod === "cmd" || mod === "command") needMeta = true;
679
+ else if (mod === "mod") {
680
+ if (isApplePlatform()) needMeta = true;
681
+ else needCtrl = true;
682
+ } else {
683
+ throw new Error(`hotkey("${combo}"): unknown modifier "${mod}"`);
684
+ }
616
685
  }
617
686
  }
618
687
  const listener = (e) => {
@@ -916,13 +985,13 @@ function bindBoolAttr(el, attr, getter) {
916
985
  let value;
917
986
  try {
918
987
  value = reactiveGetter();
919
- } catch {
988
+ } catch (err) {
989
+ reportError(err, { phase: "binding", name: "bindBoolAttr", node: el });
920
990
  return;
921
991
  }
922
992
  setSafeAttribute(el, attr, Boolean(value), { label: "bindBoolAttr" });
923
993
  }
924
- const teardown = track(commit);
925
- return teardown;
994
+ return reactiveBinding(commit, el);
926
995
  }
927
996
  function bindData(el, key, getter) {
928
997
  const dataAttr = `data-${key}`;
@@ -967,6 +1036,8 @@ function removeGlobalListenerIfIdle() {
967
1036
  function dialog() {
968
1037
  const [isOpen, setIsOpen] = signal(false);
969
1038
  const entry = { close: () => close() };
1039
+ let opened = false;
1040
+ let disposed = false;
970
1041
  function pushOnStack() {
971
1042
  if (dialogStack.indexOf(entry) !== -1) return;
972
1043
  dialogStack.push(entry);
@@ -978,23 +1049,27 @@ function dialog() {
978
1049
  removeGlobalListenerIfIdle();
979
1050
  }
980
1051
  function open() {
981
- if (isOpen()) return;
982
- setIsOpen(true);
1052
+ if (disposed || opened) return;
1053
+ opened = true;
983
1054
  pushOnStack();
1055
+ setIsOpen(true);
984
1056
  }
985
1057
  function close() {
986
- if (!isOpen()) {
1058
+ if (!opened) {
987
1059
  removeFromStack();
988
1060
  return;
989
1061
  }
990
- setIsOpen(false);
1062
+ opened = false;
991
1063
  removeFromStack();
1064
+ setIsOpen(false);
992
1065
  }
993
1066
  function toggle() {
994
- if (isOpen()) close();
1067
+ if (opened) close();
995
1068
  else open();
996
1069
  }
997
1070
  function dispose2() {
1071
+ disposed = true;
1072
+ opened = false;
998
1073
  removeFromStack();
999
1074
  setIsOpen(false);
1000
1075
  }
@@ -1186,39 +1261,62 @@ function pagination(options) {
1186
1261
  const clamped = Math.max(1, Math.min(target, totalPages()));
1187
1262
  setPage(clamped);
1188
1263
  }
1189
- return { page: currentPage, pageSize, totalPages, next, prev, goTo, startIndex, endIndex };
1264
+ function dispose2() {
1265
+ startIndex.dispose();
1266
+ endIndex.dispose();
1267
+ currentPage.dispose();
1268
+ totalPages.dispose();
1269
+ }
1270
+ return { page: currentPage, pageSize, totalPages, next, prev, goTo, startIndex, endIndex, dispose: dispose2 };
1190
1271
  }
1191
1272
 
1192
1273
  // src/ui/eventBus.ts
1193
1274
  function eventBus() {
1194
1275
  const listeners = /* @__PURE__ */ new Map();
1195
1276
  function on(event, handler) {
1196
- let set = listeners.get(event);
1197
- if (!set) {
1198
- set = /* @__PURE__ */ new Set();
1199
- listeners.set(event, set);
1277
+ let subs = listeners.get(event);
1278
+ if (!subs) {
1279
+ subs = /* @__PURE__ */ new Map();
1280
+ listeners.set(event, subs);
1281
+ }
1282
+ let subscription = subs.get(handler);
1283
+ if (!subscription) {
1284
+ subscription = { handler, active: true };
1285
+ subs.set(handler, subscription);
1200
1286
  }
1201
- set.add(handler);
1202
- return () => off(event, handler);
1287
+ const own = subscription;
1288
+ return () => {
1289
+ if (listeners.get(event)?.get(handler) !== own) return;
1290
+ off(event, handler);
1291
+ };
1203
1292
  }
1204
1293
  function emit(event, data) {
1205
- const set = listeners.get(event);
1206
- if (set) {
1207
- for (const handler of set) {
1208
- handler(data);
1294
+ const subs = listeners.get(event);
1295
+ if (!subs || subs.size === 0) return;
1296
+ for (const subscription of Array.from(subs.values())) {
1297
+ if (!subscription.active) continue;
1298
+ try {
1299
+ subscription.handler(data);
1300
+ } catch (err) {
1301
+ reportError(err, { phase: "event", name: `eventBus(${String(event)})` });
1209
1302
  }
1210
1303
  }
1211
1304
  }
1212
1305
  function off(event, handler) {
1213
- const set = listeners.get(event);
1214
- if (set) {
1215
- set.delete(handler);
1216
- if (set.size === 0) {
1217
- listeners.delete(event);
1218
- }
1306
+ const subs = listeners.get(event);
1307
+ if (!subs) return;
1308
+ const subscription = subs.get(handler);
1309
+ if (!subscription) return;
1310
+ subscription.active = false;
1311
+ subs.delete(handler);
1312
+ if (subs.size === 0) {
1313
+ listeners.delete(event);
1219
1314
  }
1220
1315
  }
1221
1316
  function clear() {
1317
+ for (const subs of listeners.values()) {
1318
+ for (const subscription of subs.values()) subscription.active = false;
1319
+ }
1222
1320
  listeners.clear();
1223
1321
  }
1224
1322
  return { on, emit, off, clear };
@@ -1226,13 +1324,33 @@ function eventBus() {
1226
1324
 
1227
1325
  // src/platform/customElement.ts
1228
1326
  var SVG_NS = "http://www.w3.org/2000/svg";
1327
+ var MAX_RENDER_PASSES = 10;
1328
+ var ABANDONED_RENDER = /* @__PURE__ */ Symbol("sibujs.defineElement.abandonedRender");
1229
1329
  function defineElement(name, component, options = {}) {
1230
1330
  if (customElements.get(name)) return;
1231
1331
  const observed = options.observedAttributes || [];
1232
1332
  class SibuElement extends HTMLElement {
1233
1333
  constructor() {
1234
1334
  super();
1235
- this._rendered = null;
1335
+ // Re-rendering is keyed on connection, not on a previous render: a first render
1336
+ // that throws leaves nothing rendered, and the element must still re-render
1337
+ // when a later attribute change fixes the input.
1338
+ this._connected = false;
1339
+ // A render in progress, and whether an attribute changed during it. The
1340
+ // component may write its host's observed attributes while rendering; with
1341
+ // the old subtree kept during the build, rendering again from inside the
1342
+ // callback recursed until the stack overflowed.
1343
+ this._rendering = false;
1344
+ this._dirty = false;
1345
+ // How many teardowns are draining. A host teardown may reconnect the element
1346
+ // (and a nested disconnect may run inside it), and rendering the next
1347
+ // generation from inside that drain would let the drain tear down what the
1348
+ // new render registers. A counter, not a flag: a nested teardown returning
1349
+ // must not declare the outer one finished.
1350
+ this._teardownDepth = 0;
1351
+ // Disconnects so far. A render compares it across its old-subtree teardown
1352
+ // to detect a disconnect that happened inside it.
1353
+ this._disconnects = 0;
1236
1354
  if (options.shadow !== false) {
1237
1355
  this._root = this.attachShadow({ mode: options.mode || "open" });
1238
1356
  } else {
@@ -1242,32 +1360,106 @@ function defineElement(name, component, options = {}) {
1242
1360
  static get observedAttributes() {
1243
1361
  return observed;
1244
1362
  }
1363
+ /** Whether rendering must wait for work already in progress. */
1364
+ get _busy() {
1365
+ return this._rendering || this._teardownDepth > 0;
1366
+ }
1245
1367
  connectedCallback() {
1368
+ this._connected = true;
1369
+ if (this._busy) {
1370
+ this._dirty = true;
1371
+ return;
1372
+ }
1246
1373
  this._render();
1247
1374
  }
1248
1375
  disconnectedCallback() {
1249
- this._teardown();
1376
+ this._connected = false;
1377
+ this._disconnects++;
1378
+ this._teardownDepth++;
1379
+ try {
1380
+ this._teardown();
1381
+ } finally {
1382
+ this._teardownDepth--;
1383
+ if (this._teardownDepth === 0 && this._connected && this._dirty) {
1384
+ this._dirty = false;
1385
+ if (!this._rendering) this._render();
1386
+ }
1387
+ }
1250
1388
  }
1251
- attributeChangedCallback() {
1252
- if (this._rendered) {
1253
- this._render();
1389
+ attributeChangedCallback(_name, oldValue, newValue) {
1390
+ if (oldValue === newValue || !this._connected) return;
1391
+ if (this._busy) {
1392
+ this._dirty = true;
1393
+ return;
1254
1394
  }
1395
+ this._render();
1255
1396
  }
1256
1397
  _teardown() {
1257
- this._rendered = null;
1258
1398
  replaceChildrenSafely(this._root);
1259
- }
1399
+ disposeNodeOwn(this);
1400
+ }
1401
+ /**
1402
+ * Render as a transaction: build the replacement first, commit only if that
1403
+ * succeeds. Tearing the current subtree down before calling the factory
1404
+ * meant a throwing rerender (an invalid attribute, say) left the element
1405
+ * blank with its live state already disposed. Now a failure keeps the
1406
+ * working subtree, releases whatever the failed attempt registered, and is
1407
+ * reported with this element as its node so an enclosing ErrorBoundary can
1408
+ * claim it.
1409
+ */
1260
1410
  _render() {
1261
- this._teardown();
1411
+ this._rendering = true;
1412
+ try {
1413
+ let passes = 0;
1414
+ do {
1415
+ this._dirty = false;
1416
+ if (++passes > MAX_RENDER_PASSES) {
1417
+ reportError(
1418
+ new Error(
1419
+ `[SibuJS] defineElement(${name}): the component changed its own observed attributes on ${MAX_RENDER_PASSES} consecutive renders; stopped re-rendering.`
1420
+ ),
1421
+ { phase: "render", name: `defineElement(${name})`, node: this }
1422
+ );
1423
+ break;
1424
+ }
1425
+ this._renderOnce();
1426
+ } while (this._dirty && this._connected);
1427
+ } finally {
1428
+ this._rendering = false;
1429
+ this._dirty = false;
1430
+ }
1431
+ }
1432
+ _renderOnce() {
1262
1433
  const props = this._getProps();
1434
+ let el;
1435
+ try {
1436
+ el = withDisposerRollback(() => {
1437
+ const built = component(props, this);
1438
+ if (!this._connected) throw ABANDONED_RENDER;
1439
+ return built;
1440
+ });
1441
+ } catch (err) {
1442
+ if (err !== ABANDONED_RENDER) {
1443
+ reportError(err, { phase: "render", name: `defineElement(${name})`, node: this });
1444
+ }
1445
+ return;
1446
+ }
1447
+ const next = [];
1263
1448
  if (options.styles && this._root instanceof ShadowRoot) {
1264
1449
  const styleEl = document.createElement("style");
1265
1450
  styleEl.textContent = options.styles;
1266
- this._root.appendChild(styleEl);
1451
+ next.push(styleEl);
1452
+ }
1453
+ next.push(el);
1454
+ const disconnects = this._disconnects;
1455
+ for (const node of next) node.parentNode?.removeChild(node);
1456
+ replaceChildrenSafely(this._root);
1457
+ if (this._disconnects !== disconnects) {
1458
+ for (const node of next) dispose(node);
1459
+ if (this._connected) this._dirty = true;
1460
+ return;
1267
1461
  }
1268
- const el = component(props, this);
1269
- this._root.appendChild(el);
1270
- this._rendered = el;
1462
+ this._root.replaceChildren(...next);
1271
1463
  }
1272
1464
  _getProps() {
1273
1465
  const props = {};
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEV,
3
3
  devWarn
4
- } from "./chunk-2WLZ6757.js";
4
+ } from "./chunk-NYNYSPK7.js";
5
5
 
6
6
  // src/patterns/hoc.ts
7
7
  function withWrapper(WrappedComponent, wrapper) {
@@ -122,7 +122,7 @@ function validateProps(props, schema) {
122
122
  const errors = [];
123
123
  for (const [key, def] of Object.entries(schema)) {
124
124
  const propDef = typeof def === "function" ? { type: def } : def;
125
- if (result[key] == null && propDef.default !== void 0) {
125
+ if (result[key] === void 0 && propDef.default !== void 0) {
126
126
  result[key] = typeof propDef.default === "function" ? propDef.default() : propDef.default;
127
127
  }
128
128
  if (propDef.required && result[key] == null) {
@@ -148,7 +148,7 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`);
148
148
  for (const [key, def] of Object.entries(schema)) {
149
149
  if (typeof def === "function") continue;
150
150
  const fallback = def.default;
151
- if (result[key] == null && fallback !== void 0) {
151
+ if (result[key] === void 0 && fallback !== void 0) {
152
152
  result[key] = typeof fallback === "function" ? fallback() : fallback;
153
153
  }
154
154
  }
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  isSSR
3
- } from "./chunk-5DXA2J44.js";
3
+ } from "./chunk-7XHATCIH.js";
4
4
  import {
5
5
  resumeTracking,
6
6
  suspendTracking,
7
7
  track
8
- } from "./chunk-3BTTCZ5J.js";
8
+ } from "./chunk-5Q4R7HCL.js";
9
9
  import {
10
10
  devAssert
11
- } from "./chunk-2WLZ6757.js";
11
+ } from "./chunk-NYNYSPK7.js";
12
12
 
13
13
  // src/core/signals/watch.ts
14
14
  function watch(getter, callback) {
@@ -1,25 +1,23 @@
1
1
  import {
2
2
  bindAttribute,
3
3
  setSafeAttribute
4
- } from "./chunk-VCTAEPSB.js";
4
+ } from "./chunk-47M47FOM.js";
5
5
  import {
6
6
  isEventHandlerAttr,
7
7
  sanitizeCSSValue,
8
8
  sanitizeStyleAttribute
9
- } from "./chunk-IXKSNWV5.js";
10
- import {
11
- dispose,
12
- registerDisposer
13
- } from "./chunk-QKRPLZ2V.js";
9
+ } from "./chunk-2INLLLMZ.js";
14
10
  import {
15
11
  reactiveBinding
16
- } from "./chunk-3BTTCZ5J.js";
12
+ } from "./chunk-5Q4R7HCL.js";
17
13
  import {
18
14
  DEV,
19
15
  devWarn,
20
16
  devWarnLazy,
17
+ dispose,
18
+ registerDisposer,
21
19
  reportError
22
- } from "./chunk-2WLZ6757.js";
20
+ } from "./chunk-NYNYSPK7.js";
23
21
 
24
22
  // src/core/rendering/focusPreservation.ts
25
23
  var IDENTITY_ATTRS = ["data-focus-key", "id", "name"];
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  isSSR
3
- } from "./chunk-5DXA2J44.js";
3
+ } from "./chunk-7XHATCIH.js";
4
4
  import {
5
5
  cleanup,
6
6
  retrack,
7
7
  untracked
8
- } from "./chunk-3BTTCZ5J.js";
8
+ } from "./chunk-5Q4R7HCL.js";
9
9
  import {
10
10
  devAssert,
11
11
  reportError
12
- } from "./chunk-2WLZ6757.js";
12
+ } from "./chunk-NYNYSPK7.js";
13
13
 
14
14
  // src/core/signals/effect.ts
15
15
  var _g = globalThis;