sibujs 4.0.1 → 4.2.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 (80) hide show
  1. package/README.md +29 -1
  2. package/dist/browser.cjs +21 -22
  3. package/dist/browser.js +6 -6
  4. package/dist/build.cjs +363 -110
  5. package/dist/build.js +15 -15
  6. package/dist/cdn.dev.global.js +398 -0
  7. package/dist/cdn.global.js +10 -10
  8. package/dist/{chunk-AD5YSZAN.js → chunk-2LOYNGBY.js} +2 -2
  9. package/dist/{chunk-VPP2FONR.js → chunk-2WLZ6757.js} +17 -10
  10. package/dist/{chunk-UNAYBND2.js → chunk-3VTSPNC5.js} +2 -2
  11. package/dist/{chunk-TIRZCERI.js → chunk-5DXA2J44.js} +1 -1
  12. package/dist/{chunk-QD4I7ZGZ.js → chunk-6ZGI3RBE.js} +149 -33
  13. package/dist/{chunk-JYXQCELN.js → chunk-7ALC7KGE.js} +13 -7
  14. package/dist/{chunk-XDOMTQHM.js → chunk-7VEC3UD2.js} +3 -3
  15. package/dist/{chunk-PTLJ7NQX.js → chunk-BMYYZWHX.js} +3 -3
  16. package/dist/{chunk-PHB3MCK4.js → chunk-BNKV4REK.js} +7 -8
  17. package/dist/{chunk-XET2RUJF.js → chunk-FOWBJZDL.js} +7 -8
  18. package/dist/{chunk-5BVKQX2F.js → chunk-GDUNQCCW.js} +4 -4
  19. package/dist/{chunk-TVURNWDK.js → chunk-HECVW343.js} +20 -9
  20. package/dist/{chunk-PHRH66Y7.js → chunk-HJEPKZ2O.js} +7 -8
  21. package/dist/{chunk-PJDSI2K7.js → chunk-HZEBO22J.js} +125 -42
  22. package/dist/{chunk-5INI7D2L.js → chunk-IXKSNWV5.js} +74 -11
  23. package/dist/{chunk-IKVUUE7D.js → chunk-KE4KWRDY.js} +3 -3
  24. package/dist/{chunk-AW6HPJ3L.js → chunk-KKLW7YWL.js} +1 -1
  25. package/dist/{chunk-XZMCVZW6.js → chunk-NEYWS4EX.js} +6 -6
  26. package/dist/{chunk-HYCCIYNS.js → chunk-NIOYEGBQ.js} +13 -14
  27. package/dist/{chunk-YLV26GAJ.js → chunk-OUWAIXIK.js} +3 -3
  28. package/dist/{chunk-E72G5JSK.js → chunk-P3DHXBR6.js} +1 -1
  29. package/dist/{chunk-J4WZMCIW.js → chunk-QCFLDD7C.js} +5 -5
  30. package/dist/{chunk-DHDZ7IVN.js → chunk-QKRPLZ2V.js} +7 -8
  31. package/dist/{chunk-52ERIVFD.js → chunk-QL4UREZO.js} +6 -6
  32. package/dist/{chunk-TNEHT3NG.js → chunk-SF3TB4K6.js} +8 -8
  33. package/dist/{chunk-HPXTUPJD.js → chunk-XZ2N525U.js} +6 -6
  34. package/dist/{customElement-CKLoO63w.d.ts → customElement-CNZxEB9G.d.cts} +77 -0
  35. package/dist/{customElement-CKLoO63w.d.cts → customElement-CNZxEB9G.d.ts} +77 -0
  36. package/dist/data.cjs +23 -25
  37. package/dist/data.js +9 -9
  38. package/dist/devtools.cjs +26 -23
  39. package/dist/devtools.d.cts +1 -1
  40. package/dist/devtools.d.ts +1 -1
  41. package/dist/devtools.js +8 -8
  42. package/dist/{dispose-4FZ6OFXR.js → dispose-46BOMMQJ.js} +2 -2
  43. package/dist/ecosystem.cjs +230 -68
  44. package/dist/ecosystem.js +11 -11
  45. package/dist/extras.cjs +251 -87
  46. package/dist/extras.d.cts +2 -2
  47. package/dist/extras.d.ts +2 -2
  48. package/dist/extras.js +25 -25
  49. package/dist/index.cjs +358 -104
  50. package/dist/index.d.cts +287 -34
  51. package/dist/index.d.ts +287 -34
  52. package/dist/index.js +17 -15
  53. package/dist/{introspect-RjLfIFpL.d.cts → introspect-D5xF-0Es.d.cts} +7 -0
  54. package/dist/{introspect-DOZfmC-4.d.ts → introspect-rSBIxEQi.d.ts} +7 -0
  55. package/dist/motion.cjs +21 -23
  56. package/dist/motion.js +5 -5
  57. package/dist/patterns.cjs +24 -25
  58. package/dist/patterns.d.cts +8 -0
  59. package/dist/patterns.d.ts +8 -0
  60. package/dist/patterns.js +7 -7
  61. package/dist/performance.cjs +24 -28
  62. package/dist/performance.js +9 -9
  63. package/dist/plugins.cjs +268 -87
  64. package/dist/plugins.d.cts +92 -0
  65. package/dist/plugins.d.ts +92 -0
  66. package/dist/plugins.js +18 -17
  67. package/dist/{ssr-JFP5535P.js → ssr-Y7XOEPEN.js} +5 -5
  68. package/dist/ssr.cjs +241 -80
  69. package/dist/ssr.js +12 -12
  70. package/dist/testing.cjs +19 -20
  71. package/dist/testing.js +5 -5
  72. package/dist/ui.cjs +106 -44
  73. package/dist/ui.d.cts +7 -1
  74. package/dist/ui.d.ts +7 -1
  75. package/dist/ui.js +10 -10
  76. package/dist/widgets.cjs +21 -22
  77. package/dist/widgets.d.cts +8 -0
  78. package/dist/widgets.d.ts +8 -0
  79. package/dist/widgets.js +9 -9
  80. package/package.json +11 -2
package/dist/index.cjs CHANGED
@@ -95,6 +95,7 @@ __export(index_exports, {
95
95
  enhance: () => enhance,
96
96
  enhanceAll: () => enhanceAll,
97
97
  enqueueBatchedSignal: () => enqueueBatchedSignal,
98
+ external: () => external,
98
99
  fieldset: () => fieldset,
99
100
  figcaption: () => figcaption,
100
101
  figure: () => figure,
@@ -345,23 +346,28 @@ function reportError(error, context2) {
345
346
  }
346
347
 
347
348
  // src/core/dev.ts
349
+ var DEV = typeof __SIBU_DEV__ !== "undefined" ? !!__SIBU_DEV__ : typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
350
+ // safe default: off in browser, on in test/dev Node
351
+ typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
352
+ );
348
353
  function isDev() {
349
- return typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
350
- // The bare `__SIBU_DEV__` is a bundler define that only exists in
351
- // production builds; under the test runner it is always undefined, so
352
- // this branch is unreachable here.
353
- /* v8 ignore next 2 */
354
- typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
355
- );
354
+ if (typeof globalThis.__SIBU_DEV__ !== "undefined") return !!globalThis.__SIBU_DEV__;
355
+ if (typeof __SIBU_DEV__ !== "undefined") return !!__SIBU_DEV__;
356
+ return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
356
357
  }
357
- var _isDev = isDev();
358
358
  function devAssert(condition, message) {
359
- if (_isDev && !condition) {
359
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
360
360
  throw new Error(`[SibuJS] ${message}`);
361
361
  }
362
362
  }
363
+ function devWarnLazy(build) {
364
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
365
+ const message = build();
366
+ if (message) console.warn(`[SibuJS] ${message}`);
367
+ }
368
+ }
363
369
  function devWarn(message) {
364
- if (_isDev) {
370
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
365
371
  console.warn(`[SibuJS] ${message}`);
366
372
  }
367
373
  }
@@ -376,7 +382,6 @@ function reportDrainRunaway(label2, executed, remaining) {
376
382
  { executed, remaining }
377
383
  );
378
384
  }
379
- var _isDev2 = isDev();
380
385
  var activeBindingCount = 0;
381
386
  function registerDisposer(node, teardown) {
382
387
  let disposers = elementDisposers.get(node);
@@ -385,7 +390,7 @@ function registerDisposer(node, teardown) {
385
390
  elementDisposers.set(node, disposers);
386
391
  }
387
392
  disposers.push(teardown);
388
- if (_isDev2) activeBindingCount++;
393
+ if (DEV) activeBindingCount++;
389
394
  }
390
395
  function unregisterDisposer(node, teardown) {
391
396
  const disposers = elementDisposers.get(node);
@@ -393,7 +398,7 @@ function unregisterDisposer(node, teardown) {
393
398
  const index = disposers.indexOf(teardown);
394
399
  if (index === -1) return;
395
400
  disposers.splice(index, 1);
396
- if (_isDev2) activeBindingCount--;
401
+ if (DEV) activeBindingCount--;
397
402
  if (disposers.length === 0) elementDisposers.delete(node);
398
403
  }
399
404
  function dispose(node) {
@@ -417,13 +422,13 @@ function dispose(node) {
417
422
  if (!pending || pending.length === 0) break;
418
423
  const snapshot = pending.slice();
419
424
  elementDisposers.delete(current);
420
- if (_isDev2) activeBindingCount -= snapshot.length;
425
+ if (DEV) activeBindingCount -= snapshot.length;
421
426
  for (let i3 = 0; i3 < snapshot.length; i3++) {
422
427
  if (executed >= MAX_DRAIN_TEARDOWNS) {
423
428
  const rest = snapshot.slice(i3);
424
429
  const added = elementDisposers.get(current);
425
430
  elementDisposers.set(current, added ? rest.concat(added) : rest);
426
- if (_isDev2) activeBindingCount += rest.length;
431
+ if (DEV) activeBindingCount += rest.length;
427
432
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
428
433
  runaway = true;
429
434
  break;
@@ -451,7 +456,7 @@ function replaceChildrenSafely(parent, ...next) {
451
456
  parent.replaceChildren(...next);
452
457
  }
453
458
  function checkLeaks(warnThreshold = 0) {
454
- if (!_isDev2) return 0;
459
+ if (!DEV) return 0;
455
460
  if (warnThreshold > 0 && activeBindingCount > warnThreshold) {
456
461
  devWarn(
457
462
  `checkLeaks: ${activeBindingCount} active DOM bindings detected. Expected \u2264${warnThreshold}. This may indicate a component was removed from the DOM without calling dispose().`
@@ -553,20 +558,75 @@ function decodeCssEscapes(value) {
553
558
  }
554
559
  return out;
555
560
  }
556
- function sanitizeCSSValue(value) {
557
- if (!CSS_DANGER_GATE.test(value)) return value;
561
+ var BLOCKED_CSS_NEEDLES = [
562
+ "url(",
563
+ "expression(",
564
+ "javascript:",
565
+ "vbscript:",
566
+ "-moz-binding",
567
+ "behavior:",
568
+ "@import",
569
+ "image-set(",
570
+ "filter:progid"
571
+ ];
572
+ function describeElement(el) {
573
+ if (!el) return "an element";
574
+ let out = el.tagName.toLowerCase();
575
+ const id = el.getAttribute("id");
576
+ if (id) out += `#${id}`;
577
+ const cls = el.getAttribute("class");
578
+ if (cls) out += `.${cls.trim().split(/\s+/).slice(0, 3).join(".")}`;
579
+ return `<${out}>`;
580
+ }
581
+ var warnedDeclarations = /* @__PURE__ */ new Set();
582
+ var MAX_WARNED_DECLARATIONS = 100;
583
+ var declarationCapAnnounced = false;
584
+ function warnDroppedDeclaration(value, reason, ctx) {
585
+ devWarnLazy(() => {
586
+ const reasons = [
587
+ "url() in an inline style is an exfiltration channel: the browser fetches it immediately and the URL's path or query can carry page data off-origin",
588
+ "expression() executes arbitrary script in legacy engines",
589
+ "the javascript: scheme executes script",
590
+ "the vbscript: scheme executes script",
591
+ "-moz-binding loads and runs remote XBL",
592
+ "behavior: loads and runs a remote HTC script",
593
+ "@import pulls in a remote stylesheet",
594
+ "image-set() fetches remote images, the same exfiltration channel as url()",
595
+ "filter:progid activates legacy scriptable filters"
596
+ ];
597
+ const declaration = ctx?.property ? `${ctx.property}: ${value}` : value;
598
+ const where = describeElement(ctx?.element);
599
+ const key = `${where}|${declaration}`;
600
+ if (warnedDeclarations.has(key)) return "";
601
+ if (warnedDeclarations.size >= MAX_WARNED_DECLARATIONS) {
602
+ if (declarationCapAnnounced) return "";
603
+ declarationCapAnnounced = true;
604
+ return `${MAX_WARNED_DECLARATIONS} distinct dropped style declarations have been reported; suppressing further ones for the rest of this session so they cannot flood the console. The declarations are still dropped \u2014 only the reporting stops. Fix the reported ones and reload to see any that remain.`;
605
+ }
606
+ warnedDeclarations.add(key);
607
+ return `style declaration "${declaration}" was dropped by the style sanitizer on ${where} \u2014 ${reasons[reason]}. The element renders without it, which usually looks like a missing image rather than an error. Sanctioned alternatives: for a content image use an <img> element (or a <picture>); for decoration, put the rule in a stylesheet and set a class instead \u2014 stylesheet CSS is not sanitized, only inline style attributes are. If this declaration is not yours, it came from untrusted data and the drop just prevented an exfiltration.`;
608
+ });
609
+ }
610
+ function blockedCssReason(value) {
611
+ if (!CSS_DANGER_GATE.test(value)) return -1;
558
612
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
559
613
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
560
- if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("vbscript:") || lower.includes("-moz-binding") || lower.includes("behavior:") || lower.includes("@import") || lower.includes("image-set(") || lower.includes("filter:progid")) {
561
- return "";
614
+ for (let i2 = 0; i2 < BLOCKED_CSS_NEEDLES.length; i2++) {
615
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i2])) return i2;
562
616
  }
563
- return value;
617
+ return -1;
618
+ }
619
+ function sanitizeCSSValue(value, context2) {
620
+ const reason = blockedCssReason(value);
621
+ if (reason === -1) return value;
622
+ warnDroppedDeclaration(value, reason, context2);
623
+ return "";
564
624
  }
565
- function sanitizeStyleAttribute(cssText) {
625
+ function sanitizeStyleAttribute(cssText, context2) {
566
626
  const input2 = String(cssText);
567
627
  if (input2.trim() === "") return "";
568
628
  if (typeof document === "undefined") {
569
- return sanitizeCSSValue(input2) === "" ? "" : input2;
629
+ return sanitizeCSSValue(input2, context2) === "" ? "" : input2;
570
630
  }
571
631
  const probe = document.createElement("div");
572
632
  try {
@@ -578,8 +638,12 @@ function sanitizeStyleAttribute(cssText) {
578
638
  for (let i2 = 0; i2 < probe.style.length; i2++) {
579
639
  const property = probe.style[i2];
580
640
  const value = probe.style.getPropertyValue(property);
581
- if (sanitizeCSSValue(value) === "") continue;
582
- if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
641
+ let reason = blockedCssReason(value);
642
+ if (reason === -1) reason = blockedCssReason(`${property}:${value}`);
643
+ if (reason !== -1) {
644
+ warnDroppedDeclaration(value, reason, { property, element: context2?.element });
645
+ continue;
646
+ }
583
647
  const priority = probe.style.getPropertyPriority(property);
584
648
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
585
649
  }
@@ -607,16 +671,15 @@ var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
607
671
  function isHtmlContentAttribute(name) {
608
672
  return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
609
673
  }
610
- function sanitizeAttributeString(attr, value) {
674
+ function sanitizeAttributeString(attr, value, context2) {
611
675
  const lower = attr.toLowerCase();
612
676
  if (lower === "srcset") return sanitizeSrcset(value);
613
677
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
614
- if (lower === "style") return sanitizeStyleAttribute(value);
678
+ if (lower === "style") return sanitizeStyleAttribute(value, context2);
615
679
  return value;
616
680
  }
617
681
 
618
682
  // src/utils/setSafeAttribute.ts
619
- var _isDev3 = isDev();
620
683
  var HTML_NS = "http://www.w3.org/1999/xhtml";
621
684
  var SVG_NS = "http://www.w3.org/2000/svg";
622
685
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -639,7 +702,7 @@ function setSafeAttribute(el, name, value, options = {}) {
639
702
  const ns = namespaceFor(el, name);
640
703
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
641
704
  if (isEventHandlerAttr(name)) {
642
- if (_isDev3) {
705
+ if (DEV) {
643
706
  devWarn(
644
707
  `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
645
708
  );
@@ -654,7 +717,7 @@ function setSafeAttribute(el, name, value, options = {}) {
654
717
  return true;
655
718
  }
656
719
  if (isHtmlContentAttribute(name)) {
657
- if (_isDev3) {
720
+ if (DEV) {
658
721
  devWarn(
659
722
  `${options.label ?? "setSafeAttribute"}: refusing to set "${name}". The browser parses this attribute as a nested HTML document, so a generic string value cannot be made safe \u2014 attribute escaping is undone before the parse. Build the frame's content another way.`
660
723
  );
@@ -683,7 +746,7 @@ function setSafeAttribute(el, name, value, options = {}) {
683
746
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
684
747
  return true;
685
748
  }
686
- const safe = sanitizeAttributeString(name, str);
749
+ const safe = sanitizeAttributeString(name, str, { element: el });
687
750
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
688
751
  if (current === safe) return true;
689
752
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -909,7 +972,14 @@ function reactiveBinding(commit, ownerNode) {
909
972
  subscriber._disposed = false;
910
973
  subscriber._errorPhase = "binding";
911
974
  subscriber._errorNode = ownerNode;
912
- run();
975
+ try {
976
+ run();
977
+ } catch (err) {
978
+ subscriber._disposed = true;
979
+ subscriber._errorNode = void 0;
980
+ cleanup(subscriber);
981
+ throw err;
982
+ }
913
983
  return subscriber._dispose ?? (subscriber._dispose = () => {
914
984
  subscriber._disposed = true;
915
985
  subscriber._errorNode = void 0;
@@ -1140,17 +1210,16 @@ function forEachSubscriber(signal2, visit) {
1140
1210
  }
1141
1211
 
1142
1212
  // src/reactivity/track.ts
1143
- var _isDev4 = isDev();
1144
- var _runtimeVersion = true ? "4.0.1" : "dev";
1213
+ var _runtimeVersion = true ? "4.2.0" : "dev";
1145
1214
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
1146
1215
  function resolveReactiveApi() {
1147
1216
  const g2 = globalThis;
1148
1217
  const existing = g2[REGISTRY_KEY];
1149
1218
  if (existing) {
1150
- if (_isDev4 && !existing.__dupWarned) {
1219
+ if (DEV && !existing.__dupWarned) {
1151
1220
  existing.__dupWarned = true;
1152
1221
  devWarn(
1153
- `Multiple instances of the reactive runtime detected on this page (active: ${existing.version}, duplicate: ${_runtimeVersion}). Reactivity still works \u2014 all copies share the first one \u2014 but de-duplicate sibujs in your bundler (e.g. Vite optimizeDeps.exclude: ['sibujs'] or resolve.dedupe: ['sibujs']).`
1222
+ `Multiple instances of the reactive runtime detected on this page (active: ${existing.version}, duplicate: ${_runtimeVersion}). Reactivity is NOT broken and no bundler configuration is required: every copy routes through the first one via a global registry, so signal writes still reach subscribers registered by any copy. This is only a SIZE issue \u2014 the duplicate copy's bytes are downloaded and parsed for nothing. If you want them back, de-duplicate sibujs in your bundler (Vite: resolve.dedupe: ['sibujs']). Mismatched VERSIONS above are worth fixing regardless, since only the first one loaded is used.`
1154
1223
  );
1155
1224
  }
1156
1225
  return existing;
@@ -1249,8 +1318,101 @@ function bindDynamic(el, nameGetter, valueGetter) {
1249
1318
  };
1250
1319
  }
1251
1320
 
1321
+ // src/core/rendering/focusPreservation.ts
1322
+ var IDENTITY_ATTRS = ["data-focus-key", "id", "name"];
1323
+ var SELECTABLE_TYPES = /* @__PURE__ */ new Set(["text", "search", "url", "tel", "password", ""]);
1324
+ function selectionOf(el) {
1325
+ const tag = el.tagName;
1326
+ const selectable = tag === "TEXTAREA" || tag === "INPUT" && SELECTABLE_TYPES.has(el.type.toLowerCase());
1327
+ if (!selectable) return { start: null, end: null, direction: null };
1328
+ const field = el;
1329
+ return { start: field.selectionStart, end: field.selectionEnd, direction: field.selectionDirection };
1330
+ }
1331
+ function captureFocusWithin(nodes) {
1332
+ if (nodes.length === 0 || typeof document === "undefined") return null;
1333
+ const active = document.activeElement;
1334
+ if (!active || active === document.body) return null;
1335
+ let inside = false;
1336
+ for (let i2 = 0; i2 < nodes.length; i2++) {
1337
+ const n = nodes[i2];
1338
+ if (n === active || n.nodeType === 1 && n.contains(active)) {
1339
+ inside = true;
1340
+ break;
1341
+ }
1342
+ }
1343
+ if (!inside) return null;
1344
+ let attr = null;
1345
+ let value = null;
1346
+ for (let i2 = 0; i2 < IDENTITY_ATTRS.length; i2++) {
1347
+ const candidate = active.getAttribute(IDENTITY_ATTRS[i2]);
1348
+ if (candidate) {
1349
+ attr = IDENTITY_ATTRS[i2];
1350
+ value = candidate;
1351
+ break;
1352
+ }
1353
+ }
1354
+ return { el: active, attr, value, tag: active.tagName, ...selectionOf(active) };
1355
+ }
1356
+ function quote(value) {
1357
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1358
+ }
1359
+ function findUniqueByIdentity(nodes, selector) {
1360
+ let el = null;
1361
+ let count = 0;
1362
+ for (let i2 = 0; i2 < nodes.length; i2++) {
1363
+ const n = nodes[i2];
1364
+ if (n.nodeType !== 1) continue;
1365
+ const root = n;
1366
+ if (root.matches(selector)) {
1367
+ count++;
1368
+ el ?? (el = root);
1369
+ }
1370
+ const inner = root.querySelectorAll(selector);
1371
+ for (let j = 0; j < inner.length; j++) {
1372
+ count++;
1373
+ el ?? (el = inner[j]);
1374
+ }
1375
+ if (count > 1) return { el: null, count };
1376
+ }
1377
+ return count === 1 ? { el, count } : { el: null, count };
1378
+ }
1379
+ function refocus(el, snapshot) {
1380
+ el.focus();
1381
+ if (snapshot.start !== null && typeof el.setSelectionRange === "function") {
1382
+ const field = el;
1383
+ const max = field.value.length;
1384
+ const start = Math.min(snapshot.start, max);
1385
+ const end = Math.min(snapshot.end ?? start, max);
1386
+ field.setSelectionRange(start, end, snapshot.direction ?? "none");
1387
+ }
1388
+ }
1389
+ function restoreFocusWithin(snapshot, nodes, where) {
1390
+ if (!snapshot) return;
1391
+ if (typeof document === "undefined") return;
1392
+ const active = document.activeElement;
1393
+ if (active === snapshot.el) return;
1394
+ if (active && active !== document.body && active !== document.documentElement) return;
1395
+ if (snapshot.el.isConnected) {
1396
+ refocus(snapshot.el, snapshot);
1397
+ return;
1398
+ }
1399
+ let ambiguous = false;
1400
+ if (snapshot.attr && snapshot.value) {
1401
+ const { el: replacement, count } = findUniqueByIdentity(nodes, `[${snapshot.attr}=${quote(snapshot.value)}]`);
1402
+ if (replacement && replacement.tagName === snapshot.tag) {
1403
+ refocus(replacement, snapshot);
1404
+ return;
1405
+ }
1406
+ ambiguous = count > 1;
1407
+ }
1408
+ devWarnLazy(() => {
1409
+ const id = snapshot.attr ? ` (${snapshot.attr}="${snapshot.value}")` : "";
1410
+ const why = ambiguous ? `several elements in the rebuilt subtree share ${snapshot.attr}="${snapshot.value}", so there is no way to tell which one the user was in \u2014 a shared name (a radio group) identifies a GROUP, not a control. Add a unique id or data-focus-key to each` : "no element in the rebuilt subtree carries the same identity. Give it a stable id, name, or data-focus-key that the rebuild reproduces";
1411
+ return `${where}: a reactive rebuild discarded the focused element <${snapshot.tag.toLowerCase()}>${id} and could not re-establish it, so focus fell back to <body>. An in-progress edit ends here, and an in-progress IME composition is lost outright \u2014 a composition belongs to the DOM node, so it cannot survive the node being destroyed. ${why}, or stop the rebuild from happening: key the subtree with match(() => shapeKey(), { \u2026 }) so it is re-created only when the shape actually changes, and drive the rest with reactive attributes and text children, which mutate nodes in place instead of replacing them.`;
1412
+ });
1413
+ }
1414
+
1252
1415
  // src/reactivity/bindChildNode.ts
1253
- var _isDev5 = isDev();
1254
1416
  function bindChildNode(placeholder, getter) {
1255
1417
  let lastNodes = [];
1256
1418
  function commit() {
@@ -1261,6 +1423,7 @@ function bindChildNode(placeholder, getter) {
1261
1423
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
1262
1424
  return;
1263
1425
  }
1426
+ const focused = captureFocusWithin(lastNodes);
1264
1427
  if (result == null || typeof result === "boolean") {
1265
1428
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
1266
1429
  const node = lastNodes[i2];
@@ -1268,6 +1431,7 @@ function bindChildNode(placeholder, getter) {
1268
1431
  if (node.parentNode) node.parentNode.removeChild(node);
1269
1432
  }
1270
1433
  lastNodes.length = 0;
1434
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
1271
1435
  return;
1272
1436
  }
1273
1437
  const parent = placeholder.parentNode;
@@ -1284,8 +1448,7 @@ function bindChildNode(placeholder, getter) {
1284
1448
  if (item == null || typeof item === "boolean") continue;
1285
1449
  const node = item instanceof Node ? item : document.createTextNode(String(item));
1286
1450
  if (seen.has(node)) {
1287
- if (_isDev5)
1288
- devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1451
+ if (DEV) devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1289
1452
  continue;
1290
1453
  }
1291
1454
  seen.add(node);
@@ -1318,13 +1481,13 @@ function bindChildNode(placeholder, getter) {
1318
1481
  prev = node;
1319
1482
  }
1320
1483
  lastNodes = newNodes;
1484
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
1321
1485
  }
1322
1486
  return reactiveBinding2(commit, placeholder);
1323
1487
  }
1324
1488
 
1325
1489
  // src/core/rendering/tagFactory.ts
1326
1490
  var SVG_NS2 = "http://www.w3.org/2000/svg";
1327
- var _isDev6 = isDev();
1328
1491
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
1329
1492
  function isBlockedTag(tag) {
1330
1493
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -1342,18 +1505,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
1342
1505
  "self",
1343
1506
  "frames"
1344
1507
  ]);
1345
- function looksLikeClassList(s2) {
1346
- const t = s2.trim();
1347
- if (!t) return false;
1348
- const tokens = t.split(/\s+/);
1349
- let sawClassish = false;
1350
- for (let i2 = 0; i2 < tokens.length; i2++) {
1351
- const tok = tokens[i2];
1352
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
1353
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
1354
- }
1355
- return sawClassish;
1356
- }
1508
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
1509
+ var MAX_WARNED_LONE_STRINGS = 100;
1510
+ var loneStringCapAnnounced = false;
1357
1511
  var kebabCache = /* @__PURE__ */ new Map();
1358
1512
  function toKebab(prop) {
1359
1513
  let cached = kebabCache.get(prop);
@@ -1365,13 +1519,13 @@ function toKebab(prop) {
1365
1519
  function applyStyle(el, style2) {
1366
1520
  if (typeof style2 === "function") {
1367
1521
  const teardown = reactiveBinding2(() => {
1368
- el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
1522
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2()), { element: el }));
1369
1523
  }, el);
1370
1524
  registerDisposer(el, teardown);
1371
1525
  return;
1372
1526
  }
1373
1527
  if (typeof style2 === "string") {
1374
- el.setAttribute("style", sanitizeStyleAttribute(style2));
1528
+ el.setAttribute("style", sanitizeStyleAttribute(style2, { element: el }));
1375
1529
  return;
1376
1530
  }
1377
1531
  const htmlEl = el;
@@ -1381,11 +1535,11 @@ function applyStyle(el, style2) {
1381
1535
  if (typeof val === "function") {
1382
1536
  const getter = val;
1383
1537
  const teardown = reactiveBinding2(() => {
1384
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
1538
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
1385
1539
  }, el);
1386
1540
  registerDisposer(el, teardown);
1387
1541
  } else {
1388
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
1542
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
1389
1543
  }
1390
1544
  }
1391
1545
  }
@@ -1492,11 +1646,26 @@ var tagFactory = (tag, ns) => {
1492
1646
  appendChildren(el, second);
1493
1647
  return el;
1494
1648
  }
1495
- if (_isDev6 && looksLikeClassList(first)) {
1496
- devWarn(
1497
- `tagFactory: lone string "${first}" looks like a class list but is being rendered as TEXT. For a class, use ${tag}({ class: "${first}" }) \u2014 or ${tag}("${first}", children) to set the class AND add children.`
1498
- );
1499
- }
1649
+ devWarnLazy(() => {
1650
+ const tokens = first.trim().split(/\s+/);
1651
+ if (tokens.length < 2) return "";
1652
+ let utilityTokens = 0;
1653
+ for (let i2 = 0; i2 < tokens.length; i2++) {
1654
+ const tok = tokens[i2];
1655
+ if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return "";
1656
+ if (/[-:/0-9]/.test(tok)) utilityTokens++;
1657
+ }
1658
+ if (utilityTokens < 2) return "";
1659
+ const key = `${tag}|${first}`;
1660
+ if (warnedLoneStrings.has(key)) return "";
1661
+ if (warnedLoneStrings.size >= MAX_WARNED_LONE_STRINGS) {
1662
+ if (loneStringCapAnnounced) return "";
1663
+ loneStringCapAnnounced = true;
1664
+ return `tagFactory: ${MAX_WARNED_LONE_STRINGS} distinct lone-string class warnings have been reported; suppressing further ones for the rest of this session so they cannot flood the console. Fix the reported ones and reload to see any that remain.`;
1665
+ }
1666
+ warnedLoneStrings.add(key);
1667
+ return `tagFactory: lone string "${first}" looks like a class list but is being rendered as TEXT. For a class, use ${tag}({ class: "${first}" }) \u2014 or ${tag}("${first}", children) to set the class AND add children.`;
1668
+ });
1500
1669
  el.textContent = first;
1501
1670
  return el;
1502
1671
  }
@@ -1513,7 +1682,7 @@ var tagFactory = (tag, ns) => {
1513
1682
  if (pClass != null) applyClass(el, pClass);
1514
1683
  const pId = props.id;
1515
1684
  if (pId != null) {
1516
- if (_isDev6 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
1685
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
1517
1686
  devWarn(
1518
1687
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
1519
1688
  );
@@ -1528,7 +1697,7 @@ var tagFactory = (tag, ns) => {
1528
1697
  const handler = pOn[ev];
1529
1698
  if (typeof handler === "function") {
1530
1699
  el.addEventListener(ev, handler);
1531
- } else if (_isDev6) {
1700
+ } else if (DEV) {
1532
1701
  devWarn(
1533
1702
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
1534
1703
  );
@@ -2327,7 +2496,6 @@ var isBatching = API2.isBatching;
2327
2496
 
2328
2497
  // src/core/signals/signal.ts
2329
2498
  var _g2 = globalThis;
2330
- var _isDev7 = isDev();
2331
2499
  function signal(initial, options) {
2332
2500
  const state2 = {
2333
2501
  value: initial,
@@ -2340,7 +2508,7 @@ function signal(initial, options) {
2340
2508
  _d: false,
2341
2509
  _validate: void 0
2342
2510
  };
2343
- const debugName = _isDev7 ? options?.name : void 0;
2511
+ const debugName = DEV ? options?.name : void 0;
2344
2512
  const equalsFn = options?.equals;
2345
2513
  if (debugName) state2.__name = debugName;
2346
2514
  function get() {
@@ -2357,7 +2525,7 @@ function signal(initial, options) {
2357
2525
  if (equalsFn(prev, newValue)) return;
2358
2526
  state2.value = newValue;
2359
2527
  state2.__v++;
2360
- if (_isDev7) {
2528
+ if (DEV) {
2361
2529
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
2362
2530
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
2363
2531
  }
@@ -2365,7 +2533,7 @@ function signal(initial, options) {
2365
2533
  notifySubscribers2(state2);
2366
2534
  }
2367
2535
  };
2368
- } else if (_isDev7) {
2536
+ } else if (DEV) {
2369
2537
  set = (next) => {
2370
2538
  const prev = state2.value;
2371
2539
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -2390,12 +2558,23 @@ function signal(initial, options) {
2390
2558
  }
2391
2559
  };
2392
2560
  }
2393
- if (_isDev7) {
2561
+ if (DEV) {
2394
2562
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
2395
2563
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
2396
2564
  }
2397
2565
  return [get, set];
2398
2566
  }
2567
+ function external(options) {
2568
+ const [version, bump] = signal(0, options?.name ? { name: options.name } : void 0);
2569
+ return {
2570
+ track() {
2571
+ version();
2572
+ },
2573
+ invalidate() {
2574
+ bump((n) => n + 1);
2575
+ }
2576
+ };
2577
+ }
2399
2578
 
2400
2579
  // src/components/ErrorDisplay.ts
2401
2580
  var STYLES = `
@@ -3371,11 +3550,10 @@ function setGlobalErrorHandler(handler) {
3371
3550
  }
3372
3551
 
3373
3552
  // src/core/rendering/context.ts
3374
- var _isDev8 = isDev();
3375
3553
  function context(defaultValue) {
3376
3554
  const [getValue, setValue] = signal(defaultValue);
3377
3555
  const warnIfSSR = (method) => {
3378
- if (!_isDev8 || !isSSR()) return;
3556
+ if (!DEV || !isSSR()) return;
3379
3557
  devWarn(
3380
3558
  `context.${method}() called during SSR. SibuJS context() is application-global \u2014 it is NOT isolated per request, so a concurrent request can observe this value. Do not use context() for request-specific SSR data; pass it explicitly or use request-scoped storage instead.`
3381
3559
  );
@@ -3403,7 +3581,7 @@ function context(defaultValue) {
3403
3581
  setValue(value);
3404
3582
  try {
3405
3583
  const result = fn();
3406
- if (_isDev8 && result !== null && typeof result?.then === "function") {
3584
+ if (DEV && result !== null && typeof result?.then === "function") {
3407
3585
  devWarn(
3408
3586
  "context.withContext() received an async callback. Scoping is synchronous only \u2014 the previous value is restored as soon as the callback returns its promise, so anything after an `await` runs outside the scope."
3409
3587
  );
@@ -3430,34 +3608,53 @@ function __resetIdCounter() {
3430
3608
 
3431
3609
  // src/core/rendering/directives.ts
3432
3610
  function show(condition, element) {
3611
+ const resolved = typeof element === "function" ? element() : element;
3433
3612
  const update = () => {
3434
- element.style.display = condition() ? "" : "none";
3613
+ resolved.style.display = condition() ? "" : "none";
3435
3614
  };
3436
- registerDisposer(element, track2(update));
3437
- return element;
3615
+ registerDisposer(resolved, track2(update));
3616
+ return resolved;
3617
+ }
3618
+ function attachBranch(parent, anchor, branch, attachedOnce, where) {
3619
+ const result = typeof branch === "function" ? branch() : branch;
3620
+ if (result == null || typeof result === "boolean") return { node: null, owned: false };
3621
+ const node = result instanceof Node ? result : document.createTextNode(String(result));
3622
+ const handedIn = node === branch;
3623
+ const seenBefore = attachedOnce.has(node);
3624
+ const owned = !handedIn && !seenBefore;
3625
+ attachedOnce.add(node);
3626
+ if (seenBefore) {
3627
+ devWarnLazy(
3628
+ () => `${where}: ${handedIn ? "a branch was given as an element rather than a function" : "a branch factory returned a node it had already returned"}, so the SAME node is being re-attached \u2014 any state it accumulated while detached (input value, scroll position, classes set imperatively) comes back with it. Its reactive bindings are left intact rather than disposed, because the node is not this directive's to tear down. Return a FRESH node per call (\`() => div(\u2026)\`) for rebuild semantics, or keep this form deliberately when the reuse is what you want.`
3629
+ );
3630
+ }
3631
+ parent.insertBefore(node, anchor.nextSibling);
3632
+ return { node, owned };
3438
3633
  }
3439
3634
  function when(condition, thenBranch, elseBranch) {
3440
3635
  const anchor = document.createComment("when");
3441
3636
  let currentNode = null;
3442
3637
  let lastCondition;
3443
3638
  let initialized = false;
3639
+ const attachedOnce = /* @__PURE__ */ new WeakSet();
3640
+ let currentNodeOwned = false;
3444
3641
  const update = () => {
3445
3642
  const show2 = condition();
3446
3643
  const parent = anchor.parentNode;
3447
3644
  if (!parent) return;
3448
3645
  if (initialized && show2 === lastCondition) return;
3449
3646
  lastCondition = show2;
3647
+ const focused = currentNode ? captureFocusWithin([currentNode]) : null;
3450
3648
  if (currentNode?.parentNode) {
3451
- dispose(currentNode);
3649
+ if (currentNodeOwned) dispose(currentNode);
3452
3650
  currentNode.parentNode.removeChild(currentNode);
3453
3651
  currentNode = null;
3454
3652
  }
3455
- const result = show2 ? thenBranch() : elseBranch ? elseBranch() : null;
3456
- if (result != null) {
3457
- const node = result instanceof Node ? result : document.createTextNode(String(result));
3458
- parent.insertBefore(node, anchor.nextSibling);
3459
- currentNode = node;
3460
- }
3653
+ const branch = show2 ? thenBranch : elseBranch !== void 0 ? elseBranch : null;
3654
+ const attached = attachBranch(parent, anchor, branch, attachedOnce, "when");
3655
+ currentNode = attached.node;
3656
+ currentNodeOwned = attached.owned;
3657
+ restoreFocusWithin(focused, currentNode ? [currentNode] : [], "when");
3461
3658
  initialized = true;
3462
3659
  };
3463
3660
  registerDisposer(anchor, track2(update));
@@ -3473,26 +3670,25 @@ function match(value, cases, fallback) {
3473
3670
  let currentNode = null;
3474
3671
  let lastKey;
3475
3672
  let initialized = false;
3673
+ const attachedOnce = /* @__PURE__ */ new WeakSet();
3674
+ let currentNodeOwned = false;
3476
3675
  const update = () => {
3477
3676
  const key = String(value());
3478
3677
  const parent = anchor.parentNode;
3479
3678
  if (!parent) return;
3480
3679
  if (initialized && key === lastKey) return;
3481
3680
  lastKey = key;
3681
+ const focused = currentNode ? captureFocusWithin([currentNode]) : null;
3482
3682
  if (currentNode?.parentNode) {
3483
- dispose(currentNode);
3683
+ if (currentNodeOwned) dispose(currentNode);
3484
3684
  currentNode.parentNode.removeChild(currentNode);
3485
3685
  currentNode = null;
3486
3686
  }
3487
- const renderFn = cases[key] || fallback;
3488
- if (renderFn) {
3489
- const result = renderFn();
3490
- if (result != null) {
3491
- const node = result instanceof Node ? result : document.createTextNode(String(result));
3492
- parent.insertBefore(node, anchor.nextSibling);
3493
- currentNode = node;
3494
- }
3495
- }
3687
+ const branch = Object.hasOwn(cases, key) ? cases[key] : fallback;
3688
+ const attached = attachBranch(parent, anchor, branch ?? null, attachedOnce, "match");
3689
+ currentNode = attached.node;
3690
+ currentNodeOwned = attached.owned;
3691
+ restoreFocusWithin(focused, currentNode ? [currentNode] : [], "match");
3496
3692
  initialized = true;
3497
3693
  };
3498
3694
  registerDisposer(anchor, track2(update));
@@ -3537,7 +3733,6 @@ function DynamicComponent(is) {
3537
3733
  }
3538
3734
 
3539
3735
  // src/core/rendering/each.ts
3540
- var _isDev9 = isDev();
3541
3736
  function resolveNodeChild(child) {
3542
3737
  if (typeof child === "function") {
3543
3738
  return resolveNodeChild(child());
@@ -3628,7 +3823,7 @@ function each(getArray, render, options) {
3628
3823
  workMap.clear();
3629
3824
  keyIndexMap.clear();
3630
3825
  for (let i2 = 0; i2 < newLen; i2++) {
3631
- if (_isDev9 && keyIndexMap.has(newKeys[i2])) {
3826
+ if (DEV && keyIndexMap.has(newKeys[i2])) {
3632
3827
  devWarn(
3633
3828
  `each: duplicate key "${String(newKeys[i2])}" at index ${i2} (first seen at ${keyIndexMap.get(newKeys[i2])}). Keys must be unique \u2014 duplicates cause rows to be dropped or mis-ordered.`
3634
3829
  );
@@ -3812,7 +4007,6 @@ function resolveChild(child) {
3812
4007
  }
3813
4008
 
3814
4009
  // src/core/rendering/htm.ts
3815
- var _isDev10 = isDev();
3816
4010
  var RAW_TEXT_TAGS = /* @__PURE__ */ new Set(["script", "style"]);
3817
4011
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
3818
4012
  "area",
@@ -3899,13 +4093,13 @@ function parseTemplate(strings) {
3899
4093
  skipWs();
3900
4094
  const exprIdx = tryExprIdx();
3901
4095
  if (exprIdx >= 0) return { kind: "expr", idx: exprIdx };
3902
- const quote = template2[pos];
3903
- if (quote === '"' || quote === "'") {
4096
+ const quote2 = template2[pos];
4097
+ if (quote2 === '"' || quote2 === "'") {
3904
4098
  pos++;
3905
4099
  const statics = [];
3906
4100
  const exprs = [];
3907
4101
  let current = "";
3908
- while (pos < len && template2[pos] !== quote) {
4102
+ while (pos < len && template2[pos] !== quote2) {
3909
4103
  const innerIdx = tryExprIdx();
3910
4104
  if (innerIdx >= 0) {
3911
4105
  statics.push(current);
@@ -4092,7 +4286,7 @@ function executeElement(tmpl, values) {
4092
4286
  const fn = values[attr.idx];
4093
4287
  if (typeof fn === "function") {
4094
4288
  el.addEventListener(attr.name, fn);
4095
- } else if (_isDev10) {
4289
+ } else if (DEV) {
4096
4290
  devWarn(
4097
4291
  `html: on:${attr.name} handler is not a function (got ${typeof fn}). Event listener was not attached.`
4098
4292
  );
@@ -4960,6 +5154,7 @@ function drainTeardowns(teardowns, label2) {
4960
5154
  }
4961
5155
  }
4962
5156
  }
5157
+ var EACH_KEYS = ["text", "attr", "class", "show", "on", "cleanup"];
4963
5158
  function resolveTarget(root, target) {
4964
5159
  if (target == null) return root;
4965
5160
  if (typeof target !== "string") return target;
@@ -4970,6 +5165,11 @@ function resolveTarget(root, target) {
4970
5165
  return null;
4971
5166
  }
4972
5167
  }
5168
+ function bindNode(el, commit) {
5169
+ if (isSSR()) return () => {
5170
+ };
5171
+ return reactiveBinding2(commit, el);
5172
+ }
4973
5173
  function readControlValue(el) {
4974
5174
  if (el instanceof HTMLInputElement) {
4975
5175
  if (el.type === "checkbox") return el.checked;
@@ -4993,6 +5193,50 @@ function writeControlValue(el, value) {
4993
5193
  const next = value == null ? "" : String(value);
4994
5194
  if (el.value !== next) el.value = next;
4995
5195
  }
5196
+ function applyEachBindings(ctx, el, spec, index) {
5197
+ const where = `ctx.each[${index}]`;
5198
+ if (isDev()) {
5199
+ for (const key of Object.keys(spec)) {
5200
+ devAssert(
5201
+ EACH_KEYS.includes(key),
5202
+ `${where}: unknown binding "${key}". Expected one of: ${EACH_KEYS.join(", ")}.`
5203
+ );
5204
+ }
5205
+ }
5206
+ if (spec.text !== void 0) {
5207
+ devAssert(typeof spec.text === "function", `${where}: "text" must be a function returning the value.`);
5208
+ ctx.text(el, spec.text);
5209
+ }
5210
+ if (spec.attr !== void 0) {
5211
+ for (const name of Object.keys(spec.attr)) {
5212
+ const value = spec.attr[name];
5213
+ devAssert(typeof value === "function", `${where}: attr["${name}"] must be a function returning the value.`);
5214
+ ctx.attr(el, name, value);
5215
+ }
5216
+ }
5217
+ if (spec.class !== void 0) {
5218
+ for (const name of Object.keys(spec.class)) {
5219
+ const on2 = spec.class[name];
5220
+ devAssert(typeof on2 === "function", `${where}: class["${name}"] must be a function returning a boolean.`);
5221
+ ctx.classed(el, name, on2);
5222
+ }
5223
+ }
5224
+ if (spec.show !== void 0) {
5225
+ devAssert(typeof spec.show === "function", `${where}: "show" must be a function returning a boolean.`);
5226
+ ctx.show(el, spec.show);
5227
+ }
5228
+ if (spec.on !== void 0) {
5229
+ for (const event of Object.keys(spec.on)) {
5230
+ const handler = spec.on[event];
5231
+ devAssert(typeof handler === "function", `${where}: on["${String(event)}"] must be a function.`);
5232
+ ctx.on(el, event, handler);
5233
+ }
5234
+ }
5235
+ if (spec.cleanup !== void 0) {
5236
+ devAssert(typeof spec.cleanup === "function", `${where}: "cleanup" must be a function.`);
5237
+ ctx.cleanup(spec.cleanup);
5238
+ }
5239
+ }
4996
5240
  function enhance(target, setup) {
4997
5241
  const root = typeof target === "string" ? typeof document !== "undefined" ? document.querySelector(target) : null : target;
4998
5242
  if (!root) {
@@ -5043,7 +5287,7 @@ function enhance(target, setup) {
5043
5287
  text: (t, value) => {
5044
5288
  bind(t, (el) => {
5045
5289
  teardowns.push(
5046
- effect(() => {
5290
+ bindNode(el, () => {
5047
5291
  const v = value();
5048
5292
  const next = v == null ? "" : String(v);
5049
5293
  if (el.textContent !== next) el.textContent = next;
@@ -5054,7 +5298,7 @@ function enhance(target, setup) {
5054
5298
  attr: (t, name, value) => {
5055
5299
  bind(t, (el) => {
5056
5300
  teardowns.push(
5057
- effect(() => {
5301
+ bindNode(el, () => {
5058
5302
  const v = value();
5059
5303
  const next = v == null ? null : String(v);
5060
5304
  setSafeAttribute(el, name, next, { syncValueProperty: false, label: "enhance attr()" });
@@ -5065,7 +5309,7 @@ function enhance(target, setup) {
5065
5309
  classed: (t, name, on2) => {
5066
5310
  bind(t, (el) => {
5067
5311
  teardowns.push(
5068
- effect(() => {
5312
+ bindNode(el, () => {
5069
5313
  el.classList.toggle(name, Boolean(on2()));
5070
5314
  })
5071
5315
  );
@@ -5075,7 +5319,7 @@ function enhance(target, setup) {
5075
5319
  bind(t, (el) => {
5076
5320
  const prevHidden = el.hidden;
5077
5321
  teardowns.push(
5078
- effect(() => {
5322
+ bindNode(el, () => {
5079
5323
  el.hidden = !when2();
5080
5324
  })
5081
5325
  );
@@ -5090,7 +5334,7 @@ function enhance(target, setup) {
5090
5334
  const [get, set] = state2;
5091
5335
  const evt = options?.event ?? (control instanceof HTMLSelectElement || control instanceof HTMLInputElement && (control.type === "checkbox" || control.type === "radio") ? "change" : "input");
5092
5336
  teardowns.push(
5093
- effect(() => {
5337
+ bindNode(el, () => {
5094
5338
  writeControlValue(control, get());
5095
5339
  })
5096
5340
  );
@@ -5099,6 +5343,16 @@ function enhance(target, setup) {
5099
5343
  teardowns.push(() => control.removeEventListener(evt, onInput));
5100
5344
  });
5101
5345
  },
5346
+ each: (target_, describe2) => {
5347
+ devAssert(typeof describe2 === "function", "ctx.each: second argument must be a function.");
5348
+ const elements = typeof target_ === "string" ? ctx.refs(target_) : Array.from(target_);
5349
+ for (let i2 = 0; i2 < elements.length; i2++) {
5350
+ const el = elements[i2];
5351
+ const spec = describe2(el, i2);
5352
+ if (spec == null) continue;
5353
+ applyEachBindings(ctx, el, spec, i2);
5354
+ }
5355
+ },
5102
5356
  cleanup: (fn) => {
5103
5357
  teardowns.push(fn);
5104
5358
  }
@@ -5307,7 +5561,6 @@ function schedule(strategy, el, activate, options) {
5307
5561
  }
5308
5562
 
5309
5563
  // src/platform/ssr.ts
5310
- var _isDev11 = isDev();
5311
5564
  function trustHTML(html3) {
5312
5565
  return html3;
5313
5566
  }
@@ -5474,6 +5727,7 @@ function nextTick() {
5474
5727
  enhance,
5475
5728
  enhanceAll,
5476
5729
  enqueueBatchedSignal,
5730
+ external,
5477
5731
  fieldset,
5478
5732
  figcaption,
5479
5733
  figure,