sibujs 4.5.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 (109) hide show
  1. package/dist/browser.cjs +309 -129
  2. package/dist/browser.d.cts +46 -14
  3. package/dist/browser.d.ts +46 -14
  4. package/dist/browser.js +8 -6
  5. package/dist/build.cjs +184 -72
  6. package/dist/build.js +15 -16
  7. package/dist/cdn.dev.global.js +12 -12
  8. package/dist/cdn.full.dev.global.js +11 -11
  9. package/dist/cdn.full.global.js +10 -10
  10. package/dist/cdn.global.js +12 -12
  11. package/dist/{chunk-IXKSNWV5.js → chunk-2INLLLMZ.js} +1 -1
  12. package/dist/chunk-3ISI6ACU.js +44 -0
  13. package/dist/{chunk-ONOHFDLG.js → chunk-47M47FOM.js} +3 -3
  14. package/dist/chunk-5HZXGZ6T.js +24 -0
  15. package/dist/{chunk-KKLW7YWL.js → chunk-5MT6SJ3P.js} +275 -144
  16. package/dist/{chunk-UGRX3S57.js → chunk-5Q4R7HCL.js} +12 -3
  17. package/dist/{chunk-S373NSMK.js → chunk-7GQCWFOE.js} +150 -59
  18. package/dist/{chunk-FTIR4QW2.js → chunk-7NCARGJW.js} +6 -8
  19. package/dist/{chunk-5DXA2J44.js → chunk-7XHATCIH.js} +5 -2
  20. package/dist/{chunk-VZKNK2V7.js → chunk-AIF3Z2T7.js} +296 -38
  21. package/dist/{chunk-4PMLNECI.js → chunk-BAAG6ZTI.js} +98 -29
  22. package/dist/{chunk-XZZOBQAY.js → chunk-D33YTSX5.js} +2 -2
  23. package/dist/{chunk-TBYTO6BS.js → chunk-DKGBBKOF.js} +136 -80
  24. package/dist/{chunk-NIOYEGBQ.js → chunk-HURREPU2.js} +27 -13
  25. package/dist/{chunk-GW3SCCZG.js → chunk-HYXDKS4N.js} +153 -110
  26. package/dist/chunk-J6FW5TV6.js +233 -0
  27. package/dist/{chunk-PCT43HW3.js → chunk-JSXPZCET.js} +1 -1
  28. package/dist/chunk-NYNYSPK7.js +318 -0
  29. package/dist/{chunk-RJE2BNI4.js → chunk-ORMZXBKQ.js} +108 -45
  30. package/dist/{chunk-OMJJM3KM.js → chunk-OXUY2A6L.js} +287 -130
  31. package/dist/{chunk-3JZ4L5TJ.js → chunk-PK6FK2G2.js} +181 -66
  32. package/dist/{chunk-RBTPLM32.js → chunk-QE4TTDU3.js} +2 -2
  33. package/dist/{chunk-2DCGACUU.js → chunk-SXXVZMKZ.js} +251 -66
  34. package/dist/{chunk-R25EFXXC.js → chunk-TUCPL2HB.js} +3 -3
  35. package/dist/{chunk-7LN645I6.js → chunk-UZQ6ALFS.js} +3 -3
  36. package/dist/{chunk-KEISJXBU.js → chunk-VVWPJ543.js} +6 -8
  37. package/dist/{chunk-RIXRAYIU.js → chunk-XC4MEKGA.js} +3 -3
  38. package/dist/{chunk-B3WHI2QA.js → chunk-XRRZKZYX.js} +55 -31
  39. package/dist/{contracts-DBdg9J_a.d.cts → contracts-DRIuclVT.d.cts} +10 -2
  40. package/dist/{contracts-DBdg9J_a.d.ts → contracts-DRIuclVT.d.ts} +10 -2
  41. package/dist/{customElement-OB9CIsc5.d.cts → customElement-MmInOW1U.d.cts} +21 -0
  42. package/dist/{customElement-OB9CIsc5.d.ts → customElement-MmInOW1U.d.ts} +21 -0
  43. package/dist/data.cjs +213 -117
  44. package/dist/data.d.cts +47 -9
  45. package/dist/data.d.ts +47 -9
  46. package/dist/data.js +12 -9
  47. package/dist/devtools.cjs +81 -57
  48. package/dist/devtools.js +7 -8
  49. package/dist/dispose-GEIG2KOF.js +28 -0
  50. package/dist/ecosystem.cjs +309 -104
  51. package/dist/ecosystem.d.cts +19 -3
  52. package/dist/ecosystem.d.ts +19 -3
  53. package/dist/ecosystem.js +12 -12
  54. package/dist/extras.cjs +2133 -863
  55. package/dist/extras.d.cts +10 -9
  56. package/dist/extras.d.ts +10 -9
  57. package/dist/extras.js +39 -28
  58. package/dist/index.cjs +184 -72
  59. package/dist/index.d.cts +22 -5
  60. package/dist/index.d.ts +22 -5
  61. package/dist/index.js +24 -27
  62. package/dist/motion.cjs +107 -41
  63. package/dist/motion.js +5 -5
  64. package/dist/patterns.cjs +317 -39
  65. package/dist/patterns.d.cts +27 -9
  66. package/dist/patterns.d.ts +27 -9
  67. package/dist/patterns.js +8 -8
  68. package/dist/performance.cjs +308 -216
  69. package/dist/performance.d.cts +2 -2
  70. package/dist/performance.d.ts +2 -2
  71. package/dist/performance.js +8 -9
  72. package/dist/plugin-DVgSnTfK.d.cts +112 -0
  73. package/dist/plugin-DVgSnTfK.d.ts +112 -0
  74. package/dist/plugins.cjs +549 -202
  75. package/dist/plugins.d.cts +3 -3
  76. package/dist/plugins.d.ts +3 -3
  77. package/dist/plugins.js +35 -23
  78. package/dist/signal-EotCj4hS.d.cts +110 -0
  79. package/dist/signal-EotCj4hS.d.ts +110 -0
  80. package/dist/{ssr-BiPRdZ6n.d.cts → ssr-Bli9XRW5.d.cts} +5 -0
  81. package/dist/{ssr-BiPRdZ6n.d.ts → ssr-Bli9XRW5.d.ts} +5 -0
  82. package/dist/{ssr-Y7XOEPEN.js → ssr-XOTUASDO.js} +4 -5
  83. package/dist/ssr.cjs +176 -72
  84. package/dist/ssr.d.cts +9 -3
  85. package/dist/ssr.d.ts +9 -3
  86. package/dist/ssr.js +11 -12
  87. package/dist/{startup-BMpaiMhP.d.ts → startup-BLfSeL15.d.cts} +73 -22
  88. package/dist/{startup-BMpaiMhP.d.cts → startup-BLfSeL15.d.ts} +73 -22
  89. package/dist/{tagFactory-DVoDpHye.d.ts → tagFactory-DFstCLQV.d.cts} +2 -100
  90. package/dist/{tagFactory-DVoDpHye.d.cts → tagFactory-DkaNVUNV.d.ts} +2 -100
  91. package/dist/testing.cjs +2484 -2185
  92. package/dist/testing.d.cts +56 -5
  93. package/dist/testing.d.ts +56 -5
  94. package/dist/testing.js +580 -307
  95. package/dist/ui.cjs +707 -317
  96. package/dist/ui.d.cts +40 -7
  97. package/dist/ui.d.ts +40 -7
  98. package/dist/ui.js +151 -56
  99. package/dist/widgets.cjs +246 -278
  100. package/dist/widgets.js +9 -10
  101. package/package.json +1 -1
  102. package/dist/chunk-2WLZ6757.js +0 -149
  103. package/dist/chunk-CCSJMTRN.js +0 -15
  104. package/dist/chunk-QKRPLZ2V.js +0 -108
  105. package/dist/chunk-VUF4ALSW.js +0 -60
  106. package/dist/chunk-WWV3SJ3L.js +0 -131
  107. package/dist/dispose-46BOMMQJ.js +0 -19
  108. package/dist/plugin-D30wlGW5.d.cts +0 -71
  109. package/dist/plugin-D30wlGW5.d.ts +0 -71
@@ -1,34 +1,38 @@
1
+ import {
2
+ domBinding
3
+ } from "./chunk-3ISI6ACU.js";
1
4
  import {
2
5
  bindAttribute,
3
6
  setSafeAttribute
4
- } from "./chunk-ONOHFDLG.js";
7
+ } from "./chunk-47M47FOM.js";
5
8
  import {
6
9
  derived
7
- } from "./chunk-RBTPLM32.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-RIXRAYIU.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-XZZOBQAY.js";
22
+ } from "./chunk-D33YTSX5.js";
25
23
  import {
26
24
  reactiveBinding,
27
25
  track
28
- } from "./chunk-UGRX3S57.js";
26
+ } from "./chunk-5Q4R7HCL.js";
29
27
  import {
30
- reportError
31
- } from "./chunk-2WLZ6757.js";
28
+ dispose,
29
+ disposeNodeOwn,
30
+ registerDisposer,
31
+ replaceChildrenSafely,
32
+ reportError,
33
+ unregisterDisposer,
34
+ withDisposerRollback
35
+ } from "./chunk-NYNYSPK7.js";
32
36
 
33
37
  // src/ui/form.ts
34
38
  function required(message = "This field is required") {
@@ -57,10 +61,14 @@ function maxLength(max2, message) {
57
61
  }
58
62
  function matchesPattern(regex, message = "Invalid format") {
59
63
  return (value) => {
60
- if (value && !regex.test(value)) {
61
- 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;
62
71
  }
63
- return null;
64
72
  };
65
73
  }
66
74
  function email(message = "Invalid email address") {
@@ -109,13 +117,13 @@ function bindField(field, extras) {
109
117
  const mergedOn = extraOn && typeof extraOn === "object" ? { ...extraOn, ...fieldOn } : fieldOn;
110
118
  const onElement = (el) => {
111
119
  if (el instanceof HTMLSelectElement && el.multiple) {
112
- const stop = effect(() => {
120
+ const stop = domBinding(() => {
113
121
  const v = field.value();
114
122
  const selected = Array.isArray(v) ? v.map(String) : [];
115
123
  for (const opt of Array.from(el.options)) {
116
124
  opt.selected = selected.includes(opt.value);
117
125
  }
118
- });
126
+ }, el);
119
127
  registerDisposer(el, stop);
120
128
  }
121
129
  if (typeof extraOnElement === "function") {
@@ -135,6 +143,7 @@ function form(config) {
135
143
  const fieldEntries = Object.entries(config);
136
144
  const fieldMap = {};
137
145
  const [manualErrors, setManualErrors] = signal({});
146
+ const fieldErrors = [];
138
147
  for (const [name, cfg] of fieldEntries) {
139
148
  const [value, setValue] = signal(cfg.initial);
140
149
  const [isTouched, setTouched] = signal(false);
@@ -149,6 +158,7 @@ function form(config) {
149
158
  }
150
159
  return null;
151
160
  });
161
+ fieldErrors.push(error);
152
162
  const wrappedSet = (next) => {
153
163
  setValue(next);
154
164
  setManualErrors((prev) => {
@@ -225,12 +235,22 @@ function form(config) {
225
235
  field.touch();
226
236
  }
227
237
  if (isValid()) {
228
- const result = onSubmit(values());
229
- 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) {
230
247
  setSubmitting(true);
231
- result.then(
248
+ pending.then(
232
249
  () => setSubmitting(false),
233
- () => setSubmitting(false)
250
+ (error) => {
251
+ setSubmitting(false);
252
+ report(error);
253
+ }
234
254
  );
235
255
  }
236
256
  }
@@ -244,6 +264,17 @@ function form(config) {
244
264
  function setError(field, message) {
245
265
  setManualErrors((prev) => ({ ...prev, [field]: message }));
246
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
+ }
247
278
  return {
248
279
  fields: fieldMap,
249
280
  errors,
@@ -254,7 +285,8 @@ function form(config) {
254
285
  values,
255
286
  handleSubmit,
256
287
  reset,
257
- setError
288
+ setError,
289
+ dispose: dispose2
258
290
  };
259
291
  }
260
292
 
@@ -298,7 +330,7 @@ function VirtualList(props) {
298
330
  content.appendChild(itemEl);
299
331
  }
300
332
  };
301
- registerDisposer(container, effect(update));
333
+ registerDisposer(container, domBinding(update, container));
302
334
  return container;
303
335
  }
304
336
 
@@ -308,11 +340,14 @@ function intersection(options) {
308
340
  const [ratio, setRatio] = signal(0);
309
341
  let observer = null;
310
342
  let currentElement = null;
343
+ let generation = 0;
311
344
  function observe(element) {
312
345
  if (typeof IntersectionObserver === "undefined") return;
313
346
  unobserve();
314
347
  currentElement = element;
348
+ const observation = ++generation;
315
349
  observer = new IntersectionObserver((entries) => {
350
+ if (observation !== generation) return;
316
351
  const entry = entries[0];
317
352
  if (entry) {
318
353
  setIsIntersecting(entry.isIntersecting);
@@ -322,8 +357,10 @@ function intersection(options) {
322
357
  observer.observe(element);
323
358
  }
324
359
  function unobserve() {
360
+ generation++;
325
361
  if (observer) {
326
362
  if (currentElement) observer.unobserve(currentElement);
363
+ observer.takeRecords();
327
364
  observer.disconnect();
328
365
  observer = null;
329
366
  currentElement = null;
@@ -342,17 +379,26 @@ function lazyLoad(element, loader, options) {
342
379
  return () => {
343
380
  };
344
381
  }
382
+ let done = false;
383
+ const stop = () => {
384
+ done = true;
385
+ observer.takeRecords();
386
+ observer.disconnect();
387
+ };
345
388
  const observer = new IntersectionObserver((entries) => {
389
+ if (done) return;
346
390
  for (const entry of entries) {
347
391
  if (entry.isIntersecting) {
392
+ stop();
348
393
  loader();
349
- observer.disconnect();
350
394
  break;
351
395
  }
352
396
  }
353
397
  }, options);
354
398
  observer.observe(element);
355
- return () => observer.disconnect();
399
+ return () => {
400
+ if (!done) stop();
401
+ };
356
402
  }
357
403
 
358
404
  // src/ui/inputMask.ts
@@ -501,6 +547,7 @@ function focus() {
501
547
  const dispose2 = () => {
502
548
  if (disposed) return;
503
549
  disposed = true;
550
+ unregisterDisposer(element, dispose2);
504
551
  element.removeEventListener("focus", onFocus);
505
552
  element.removeEventListener("blur", onBlur);
506
553
  if (currentElement === element) currentElement = null;
@@ -569,14 +616,19 @@ function FocusTrap(nodes, options = {}) {
569
616
  }
570
617
  };
571
618
  container.addEventListener("keydown", onTrapKeydown);
619
+ let disposed = false;
572
620
  if (options.autoFocus !== false) {
573
621
  queueMicrotask(() => {
622
+ if (disposed || !container.isConnected) return;
574
623
  const first = getFocusable()[0];
575
624
  first?.focus();
576
625
  });
577
626
  }
578
627
  let trapObserver = null;
579
628
  function restoreFocusAndCleanup() {
629
+ if (disposed) return;
630
+ disposed = true;
631
+ unregisterDisposer(container, restoreFocusAndCleanup);
580
632
  if (options.restoreFocus !== false) previouslyFocused?.focus();
581
633
  container.removeEventListener("keydown", onTrapKeydown);
582
634
  if (trapObserver) {
@@ -591,29 +643,45 @@ function FocusTrap(nodes, options = {}) {
591
643
  }
592
644
  });
593
645
  queueMicrotask(() => {
594
- if (container.isConnected) {
595
- trapObserver.observe(container, { childList: true, subtree: true });
596
- }
646
+ const observer = trapObserver;
647
+ if (disposed || !container.isConnected || !observer) return;
648
+ observer.observe(container, { childList: true, subtree: true });
597
649
  });
598
650
  }
599
651
  registerDisposer(container, restoreFocusAndCleanup);
600
652
  return container;
601
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
+ }
602
659
  function hotkey(combo, handler, options = {}) {
603
660
  let key = combo;
604
661
  let needCtrl = options.ctrl ?? false;
605
662
  let needShift = options.shift ?? false;
606
663
  let needAlt = options.alt ?? false;
607
664
  let needMeta = options.meta ?? false;
608
- if (combo.includes("+")) {
609
- 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("+");
610
671
  key = parts[parts.length - 1];
672
+ if (key === "") throw new Error(`hotkey("${combo}"): missing key`);
611
673
  for (let i = 0; i < parts.length - 1; i++) {
612
674
  const mod = parts[i];
613
675
  if (mod === "ctrl" || mod === "control") needCtrl = true;
614
676
  else if (mod === "shift") needShift = true;
615
- else if (mod === "alt") needAlt = true;
677
+ else if (mod === "alt" || mod === "option") needAlt = true;
616
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
+ }
617
685
  }
618
686
  }
619
687
  const listener = (e) => {
@@ -968,6 +1036,8 @@ function removeGlobalListenerIfIdle() {
968
1036
  function dialog() {
969
1037
  const [isOpen, setIsOpen] = signal(false);
970
1038
  const entry = { close: () => close() };
1039
+ let opened = false;
1040
+ let disposed = false;
971
1041
  function pushOnStack() {
972
1042
  if (dialogStack.indexOf(entry) !== -1) return;
973
1043
  dialogStack.push(entry);
@@ -979,23 +1049,27 @@ function dialog() {
979
1049
  removeGlobalListenerIfIdle();
980
1050
  }
981
1051
  function open() {
982
- if (isOpen()) return;
983
- setIsOpen(true);
1052
+ if (disposed || opened) return;
1053
+ opened = true;
984
1054
  pushOnStack();
1055
+ setIsOpen(true);
985
1056
  }
986
1057
  function close() {
987
- if (!isOpen()) {
1058
+ if (!opened) {
988
1059
  removeFromStack();
989
1060
  return;
990
1061
  }
991
- setIsOpen(false);
1062
+ opened = false;
992
1063
  removeFromStack();
1064
+ setIsOpen(false);
993
1065
  }
994
1066
  function toggle() {
995
- if (isOpen()) close();
1067
+ if (opened) close();
996
1068
  else open();
997
1069
  }
998
1070
  function dispose2() {
1071
+ disposed = true;
1072
+ opened = false;
999
1073
  removeFromStack();
1000
1074
  setIsOpen(false);
1001
1075
  }
@@ -1200,32 +1274,49 @@ function pagination(options) {
1200
1274
  function eventBus() {
1201
1275
  const listeners = /* @__PURE__ */ new Map();
1202
1276
  function on(event, handler) {
1203
- let set = listeners.get(event);
1204
- if (!set) {
1205
- set = /* @__PURE__ */ new Set();
1206
- 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);
1207
1286
  }
1208
- set.add(handler);
1209
- return () => off(event, handler);
1287
+ const own = subscription;
1288
+ return () => {
1289
+ if (listeners.get(event)?.get(handler) !== own) return;
1290
+ off(event, handler);
1291
+ };
1210
1292
  }
1211
1293
  function emit(event, data) {
1212
- const set = listeners.get(event);
1213
- if (set) {
1214
- for (const handler of set) {
1215
- 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)})` });
1216
1302
  }
1217
1303
  }
1218
1304
  }
1219
1305
  function off(event, handler) {
1220
- const set = listeners.get(event);
1221
- if (set) {
1222
- set.delete(handler);
1223
- if (set.size === 0) {
1224
- listeners.delete(event);
1225
- }
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);
1226
1314
  }
1227
1315
  }
1228
1316
  function clear() {
1317
+ for (const subs of listeners.values()) {
1318
+ for (const subscription of subs.values()) subscription.active = false;
1319
+ }
1229
1320
  listeners.clear();
1230
1321
  }
1231
1322
  return { on, emit, off, clear };
@@ -1233,13 +1324,33 @@ function eventBus() {
1233
1324
 
1234
1325
  // src/platform/customElement.ts
1235
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");
1236
1329
  function defineElement(name, component, options = {}) {
1237
1330
  if (customElements.get(name)) return;
1238
1331
  const observed = options.observedAttributes || [];
1239
1332
  class SibuElement extends HTMLElement {
1240
1333
  constructor() {
1241
1334
  super();
1242
- 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;
1243
1354
  if (options.shadow !== false) {
1244
1355
  this._root = this.attachShadow({ mode: options.mode || "open" });
1245
1356
  } else {
@@ -1249,32 +1360,106 @@ function defineElement(name, component, options = {}) {
1249
1360
  static get observedAttributes() {
1250
1361
  return observed;
1251
1362
  }
1363
+ /** Whether rendering must wait for work already in progress. */
1364
+ get _busy() {
1365
+ return this._rendering || this._teardownDepth > 0;
1366
+ }
1252
1367
  connectedCallback() {
1368
+ this._connected = true;
1369
+ if (this._busy) {
1370
+ this._dirty = true;
1371
+ return;
1372
+ }
1253
1373
  this._render();
1254
1374
  }
1255
1375
  disconnectedCallback() {
1256
- 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
+ }
1257
1388
  }
1258
- attributeChangedCallback() {
1259
- if (this._rendered) {
1260
- this._render();
1389
+ attributeChangedCallback(_name, oldValue, newValue) {
1390
+ if (oldValue === newValue || !this._connected) return;
1391
+ if (this._busy) {
1392
+ this._dirty = true;
1393
+ return;
1261
1394
  }
1395
+ this._render();
1262
1396
  }
1263
1397
  _teardown() {
1264
- this._rendered = null;
1265
1398
  replaceChildrenSafely(this._root);
1266
- }
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
+ */
1267
1410
  _render() {
1268
- 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() {
1269
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 = [];
1270
1448
  if (options.styles && this._root instanceof ShadowRoot) {
1271
1449
  const styleEl = document.createElement("style");
1272
1450
  styleEl.textContent = options.styles;
1273
- 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;
1274
1461
  }
1275
- const el = component(props, this);
1276
- this._root.appendChild(el);
1277
- this._rendered = el;
1462
+ this._root.replaceChildren(...next);
1278
1463
  }
1279
1464
  _getProps() {
1280
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-UGRX3S57.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-ONOHFDLG.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-UGRX3S57.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-UGRX3S57.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;