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/build.cjs CHANGED
@@ -161,15 +161,15 @@ function parseTemplateLiteral(code2, openBacktick) {
161
161
  return null;
162
162
  }
163
163
  function skipString(code2, start) {
164
- const quote = code2[start];
164
+ const quote2 = code2[start];
165
165
  let pos = start + 1;
166
166
  while (pos < code2.length) {
167
167
  if (code2[pos] === "\\") {
168
168
  pos += 2;
169
169
  continue;
170
170
  }
171
- if (code2[pos] === quote) return pos + 1;
172
- if (quote === "`" && code2[pos] === "$" && code2[pos + 1] === "{") {
171
+ if (code2[pos] === quote2) return pos + 1;
172
+ if (quote2 === "`" && code2[pos] === "$" && code2[pos + 1] === "{") {
173
173
  pos += 2;
174
174
  let depth = 1;
175
175
  while (pos < code2.length && depth > 0) {
@@ -225,12 +225,12 @@ function parseTemplateToAST(strings) {
225
225
  skipWs();
226
226
  const exprIdx = tryExprIdx();
227
227
  if (exprIdx >= 0) return { kind: "expr", index: exprIdx };
228
- const quote = template2[pos];
229
- if (quote === '"' || quote === "'") {
228
+ const quote2 = template2[pos];
229
+ if (quote2 === '"' || quote2 === "'") {
230
230
  pos++;
231
231
  const parts = [];
232
232
  let current = "";
233
- while (pos < len && template2[pos] !== quote) {
233
+ while (pos < len && template2[pos] !== quote2) {
234
234
  const innerIdx = tryExprIdx();
235
235
  if (innerIdx >= 0) {
236
236
  if (current) parts.push({ kind: "static", value: current });
@@ -1132,6 +1132,7 @@ __export(index_exports, {
1132
1132
  enhance: () => enhance,
1133
1133
  enhanceAll: () => enhanceAll,
1134
1134
  enqueueBatchedSignal: () => enqueueBatchedSignal,
1135
+ external: () => external,
1135
1136
  fieldset: () => fieldset,
1136
1137
  figcaption: () => figcaption,
1137
1138
  figure: () => figure,
@@ -1381,23 +1382,28 @@ function reportError(error, context2) {
1381
1382
  }
1382
1383
 
1383
1384
  // src/core/dev.ts
1385
+ var DEV = typeof __SIBU_DEV__ !== "undefined" ? !!__SIBU_DEV__ : typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
1386
+ // safe default: off in browser, on in test/dev Node
1387
+ typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
1388
+ );
1384
1389
  function isDev() {
1385
- return typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
1386
- // The bare `__SIBU_DEV__` is a bundler define that only exists in
1387
- // production builds; under the test runner it is always undefined, so
1388
- // this branch is unreachable here.
1389
- /* v8 ignore next 2 */
1390
- typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
1391
- );
1390
+ if (typeof globalThis.__SIBU_DEV__ !== "undefined") return !!globalThis.__SIBU_DEV__;
1391
+ if (typeof __SIBU_DEV__ !== "undefined") return !!__SIBU_DEV__;
1392
+ return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1392
1393
  }
1393
- var _isDev = isDev();
1394
1394
  function devAssert(condition, message) {
1395
- if (_isDev && !condition) {
1395
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
1396
1396
  throw new Error(`[SibuJS] ${message}`);
1397
1397
  }
1398
1398
  }
1399
+ function devWarnLazy(build) {
1400
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
1401
+ const message = build();
1402
+ if (message) console.warn(`[SibuJS] ${message}`);
1403
+ }
1404
+ }
1399
1405
  function devWarn(message) {
1400
- if (_isDev) {
1406
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
1401
1407
  console.warn(`[SibuJS] ${message}`);
1402
1408
  }
1403
1409
  }
@@ -1412,7 +1418,6 @@ function reportDrainRunaway(label2, executed, remaining) {
1412
1418
  { executed, remaining }
1413
1419
  );
1414
1420
  }
1415
- var _isDev2 = isDev();
1416
1421
  var activeBindingCount = 0;
1417
1422
  function registerDisposer(node, teardown) {
1418
1423
  let disposers = elementDisposers.get(node);
@@ -1421,7 +1426,7 @@ function registerDisposer(node, teardown) {
1421
1426
  elementDisposers.set(node, disposers);
1422
1427
  }
1423
1428
  disposers.push(teardown);
1424
- if (_isDev2) activeBindingCount++;
1429
+ if (DEV) activeBindingCount++;
1425
1430
  }
1426
1431
  function unregisterDisposer(node, teardown) {
1427
1432
  const disposers = elementDisposers.get(node);
@@ -1429,7 +1434,7 @@ function unregisterDisposer(node, teardown) {
1429
1434
  const index = disposers.indexOf(teardown);
1430
1435
  if (index === -1) return;
1431
1436
  disposers.splice(index, 1);
1432
- if (_isDev2) activeBindingCount--;
1437
+ if (DEV) activeBindingCount--;
1433
1438
  if (disposers.length === 0) elementDisposers.delete(node);
1434
1439
  }
1435
1440
  function dispose(node) {
@@ -1453,13 +1458,13 @@ function dispose(node) {
1453
1458
  if (!pending || pending.length === 0) break;
1454
1459
  const snapshot = pending.slice();
1455
1460
  elementDisposers.delete(current);
1456
- if (_isDev2) activeBindingCount -= snapshot.length;
1461
+ if (DEV) activeBindingCount -= snapshot.length;
1457
1462
  for (let i3 = 0; i3 < snapshot.length; i3++) {
1458
1463
  if (executed >= MAX_DRAIN_TEARDOWNS) {
1459
1464
  const rest = snapshot.slice(i3);
1460
1465
  const added = elementDisposers.get(current);
1461
1466
  elementDisposers.set(current, added ? rest.concat(added) : rest);
1462
- if (_isDev2) activeBindingCount += rest.length;
1467
+ if (DEV) activeBindingCount += rest.length;
1463
1468
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
1464
1469
  runaway = true;
1465
1470
  break;
@@ -1487,7 +1492,7 @@ function replaceChildrenSafely(parent, ...next) {
1487
1492
  parent.replaceChildren(...next);
1488
1493
  }
1489
1494
  function checkLeaks(warnThreshold = 0) {
1490
- if (!_isDev2) return 0;
1495
+ if (!DEV) return 0;
1491
1496
  if (warnThreshold > 0 && activeBindingCount > warnThreshold) {
1492
1497
  devWarn(
1493
1498
  `checkLeaks: ${activeBindingCount} active DOM bindings detected. Expected \u2264${warnThreshold}. This may indicate a component was removed from the DOM without calling dispose().`
@@ -1589,20 +1594,75 @@ function decodeCssEscapes(value) {
1589
1594
  }
1590
1595
  return out;
1591
1596
  }
1592
- function sanitizeCSSValue(value) {
1593
- if (!CSS_DANGER_GATE.test(value)) return value;
1597
+ var BLOCKED_CSS_NEEDLES = [
1598
+ "url(",
1599
+ "expression(",
1600
+ "javascript:",
1601
+ "vbscript:",
1602
+ "-moz-binding",
1603
+ "behavior:",
1604
+ "@import",
1605
+ "image-set(",
1606
+ "filter:progid"
1607
+ ];
1608
+ function describeElement(el) {
1609
+ if (!el) return "an element";
1610
+ let out = el.tagName.toLowerCase();
1611
+ const id = el.getAttribute("id");
1612
+ if (id) out += `#${id}`;
1613
+ const cls = el.getAttribute("class");
1614
+ if (cls) out += `.${cls.trim().split(/\s+/).slice(0, 3).join(".")}`;
1615
+ return `<${out}>`;
1616
+ }
1617
+ var warnedDeclarations = /* @__PURE__ */ new Set();
1618
+ var MAX_WARNED_DECLARATIONS = 100;
1619
+ var declarationCapAnnounced = false;
1620
+ function warnDroppedDeclaration(value, reason, ctx) {
1621
+ devWarnLazy(() => {
1622
+ const reasons = [
1623
+ "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",
1624
+ "expression() executes arbitrary script in legacy engines",
1625
+ "the javascript: scheme executes script",
1626
+ "the vbscript: scheme executes script",
1627
+ "-moz-binding loads and runs remote XBL",
1628
+ "behavior: loads and runs a remote HTC script",
1629
+ "@import pulls in a remote stylesheet",
1630
+ "image-set() fetches remote images, the same exfiltration channel as url()",
1631
+ "filter:progid activates legacy scriptable filters"
1632
+ ];
1633
+ const declaration = ctx?.property ? `${ctx.property}: ${value}` : value;
1634
+ const where = describeElement(ctx?.element);
1635
+ const key = `${where}|${declaration}`;
1636
+ if (warnedDeclarations.has(key)) return "";
1637
+ if (warnedDeclarations.size >= MAX_WARNED_DECLARATIONS) {
1638
+ if (declarationCapAnnounced) return "";
1639
+ declarationCapAnnounced = true;
1640
+ 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.`;
1641
+ }
1642
+ warnedDeclarations.add(key);
1643
+ 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.`;
1644
+ });
1645
+ }
1646
+ function blockedCssReason(value) {
1647
+ if (!CSS_DANGER_GATE.test(value)) return -1;
1594
1648
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
1595
1649
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
1596
- 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")) {
1597
- return "";
1650
+ for (let i2 = 0; i2 < BLOCKED_CSS_NEEDLES.length; i2++) {
1651
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i2])) return i2;
1598
1652
  }
1599
- return value;
1653
+ return -1;
1600
1654
  }
1601
- function sanitizeStyleAttribute(cssText) {
1655
+ function sanitizeCSSValue(value, context2) {
1656
+ const reason = blockedCssReason(value);
1657
+ if (reason === -1) return value;
1658
+ warnDroppedDeclaration(value, reason, context2);
1659
+ return "";
1660
+ }
1661
+ function sanitizeStyleAttribute(cssText, context2) {
1602
1662
  const input2 = String(cssText);
1603
1663
  if (input2.trim() === "") return "";
1604
1664
  if (typeof document === "undefined") {
1605
- return sanitizeCSSValue(input2) === "" ? "" : input2;
1665
+ return sanitizeCSSValue(input2, context2) === "" ? "" : input2;
1606
1666
  }
1607
1667
  const probe = document.createElement("div");
1608
1668
  try {
@@ -1614,8 +1674,12 @@ function sanitizeStyleAttribute(cssText) {
1614
1674
  for (let i2 = 0; i2 < probe.style.length; i2++) {
1615
1675
  const property = probe.style[i2];
1616
1676
  const value = probe.style.getPropertyValue(property);
1617
- if (sanitizeCSSValue(value) === "") continue;
1618
- if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
1677
+ let reason = blockedCssReason(value);
1678
+ if (reason === -1) reason = blockedCssReason(`${property}:${value}`);
1679
+ if (reason !== -1) {
1680
+ warnDroppedDeclaration(value, reason, { property, element: context2?.element });
1681
+ continue;
1682
+ }
1619
1683
  const priority = probe.style.getPropertyPriority(property);
1620
1684
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
1621
1685
  }
@@ -1643,16 +1707,15 @@ var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
1643
1707
  function isHtmlContentAttribute(name) {
1644
1708
  return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
1645
1709
  }
1646
- function sanitizeAttributeString(attr, value) {
1710
+ function sanitizeAttributeString(attr, value, context2) {
1647
1711
  const lower = attr.toLowerCase();
1648
1712
  if (lower === "srcset") return sanitizeSrcset(value);
1649
1713
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
1650
- if (lower === "style") return sanitizeStyleAttribute(value);
1714
+ if (lower === "style") return sanitizeStyleAttribute(value, context2);
1651
1715
  return value;
1652
1716
  }
1653
1717
 
1654
1718
  // src/utils/setSafeAttribute.ts
1655
- var _isDev3 = isDev();
1656
1719
  var HTML_NS = "http://www.w3.org/1999/xhtml";
1657
1720
  var SVG_NS = "http://www.w3.org/2000/svg";
1658
1721
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -1675,7 +1738,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1675
1738
  const ns = namespaceFor(el, name);
1676
1739
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
1677
1740
  if (isEventHandlerAttr(name)) {
1678
- if (_isDev3) {
1741
+ if (DEV) {
1679
1742
  devWarn(
1680
1743
  `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
1681
1744
  );
@@ -1690,7 +1753,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1690
1753
  return true;
1691
1754
  }
1692
1755
  if (isHtmlContentAttribute(name)) {
1693
- if (_isDev3) {
1756
+ if (DEV) {
1694
1757
  devWarn(
1695
1758
  `${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.`
1696
1759
  );
@@ -1719,7 +1782,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1719
1782
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
1720
1783
  return true;
1721
1784
  }
1722
- const safe = sanitizeAttributeString(name, str);
1785
+ const safe = sanitizeAttributeString(name, str, { element: el });
1723
1786
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
1724
1787
  if (current === safe) return true;
1725
1788
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -1945,7 +2008,14 @@ function reactiveBinding(commit, ownerNode) {
1945
2008
  subscriber._disposed = false;
1946
2009
  subscriber._errorPhase = "binding";
1947
2010
  subscriber._errorNode = ownerNode;
1948
- run();
2011
+ try {
2012
+ run();
2013
+ } catch (err) {
2014
+ subscriber._disposed = true;
2015
+ subscriber._errorNode = void 0;
2016
+ cleanup(subscriber);
2017
+ throw err;
2018
+ }
1949
2019
  return subscriber._dispose ?? (subscriber._dispose = () => {
1950
2020
  subscriber._disposed = true;
1951
2021
  subscriber._errorNode = void 0;
@@ -2176,17 +2246,16 @@ function forEachSubscriber(signal2, visit) {
2176
2246
  }
2177
2247
 
2178
2248
  // src/reactivity/track.ts
2179
- var _isDev4 = isDev();
2180
- var _runtimeVersion = true ? "4.0.1" : "dev";
2249
+ var _runtimeVersion = true ? "4.2.0" : "dev";
2181
2250
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
2182
2251
  function resolveReactiveApi() {
2183
2252
  const g2 = globalThis;
2184
2253
  const existing = g2[REGISTRY_KEY];
2185
2254
  if (existing) {
2186
- if (_isDev4 && !existing.__dupWarned) {
2255
+ if (DEV && !existing.__dupWarned) {
2187
2256
  existing.__dupWarned = true;
2188
2257
  devWarn(
2189
- `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']).`
2258
+ `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.`
2190
2259
  );
2191
2260
  }
2192
2261
  return existing;
@@ -2285,8 +2354,101 @@ function bindDynamic(el, nameGetter, valueGetter) {
2285
2354
  };
2286
2355
  }
2287
2356
 
2357
+ // src/core/rendering/focusPreservation.ts
2358
+ var IDENTITY_ATTRS = ["data-focus-key", "id", "name"];
2359
+ var SELECTABLE_TYPES = /* @__PURE__ */ new Set(["text", "search", "url", "tel", "password", ""]);
2360
+ function selectionOf(el) {
2361
+ const tag = el.tagName;
2362
+ const selectable = tag === "TEXTAREA" || tag === "INPUT" && SELECTABLE_TYPES.has(el.type.toLowerCase());
2363
+ if (!selectable) return { start: null, end: null, direction: null };
2364
+ const field = el;
2365
+ return { start: field.selectionStart, end: field.selectionEnd, direction: field.selectionDirection };
2366
+ }
2367
+ function captureFocusWithin(nodes) {
2368
+ if (nodes.length === 0 || typeof document === "undefined") return null;
2369
+ const active = document.activeElement;
2370
+ if (!active || active === document.body) return null;
2371
+ let inside = false;
2372
+ for (let i2 = 0; i2 < nodes.length; i2++) {
2373
+ const n = nodes[i2];
2374
+ if (n === active || n.nodeType === 1 && n.contains(active)) {
2375
+ inside = true;
2376
+ break;
2377
+ }
2378
+ }
2379
+ if (!inside) return null;
2380
+ let attr = null;
2381
+ let value = null;
2382
+ for (let i2 = 0; i2 < IDENTITY_ATTRS.length; i2++) {
2383
+ const candidate = active.getAttribute(IDENTITY_ATTRS[i2]);
2384
+ if (candidate) {
2385
+ attr = IDENTITY_ATTRS[i2];
2386
+ value = candidate;
2387
+ break;
2388
+ }
2389
+ }
2390
+ return { el: active, attr, value, tag: active.tagName, ...selectionOf(active) };
2391
+ }
2392
+ function quote(value) {
2393
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
2394
+ }
2395
+ function findUniqueByIdentity(nodes, selector) {
2396
+ let el = null;
2397
+ let count = 0;
2398
+ for (let i2 = 0; i2 < nodes.length; i2++) {
2399
+ const n = nodes[i2];
2400
+ if (n.nodeType !== 1) continue;
2401
+ const root = n;
2402
+ if (root.matches(selector)) {
2403
+ count++;
2404
+ el ?? (el = root);
2405
+ }
2406
+ const inner = root.querySelectorAll(selector);
2407
+ for (let j = 0; j < inner.length; j++) {
2408
+ count++;
2409
+ el ?? (el = inner[j]);
2410
+ }
2411
+ if (count > 1) return { el: null, count };
2412
+ }
2413
+ return count === 1 ? { el, count } : { el: null, count };
2414
+ }
2415
+ function refocus(el, snapshot) {
2416
+ el.focus();
2417
+ if (snapshot.start !== null && typeof el.setSelectionRange === "function") {
2418
+ const field = el;
2419
+ const max = field.value.length;
2420
+ const start = Math.min(snapshot.start, max);
2421
+ const end = Math.min(snapshot.end ?? start, max);
2422
+ field.setSelectionRange(start, end, snapshot.direction ?? "none");
2423
+ }
2424
+ }
2425
+ function restoreFocusWithin(snapshot, nodes, where) {
2426
+ if (!snapshot) return;
2427
+ if (typeof document === "undefined") return;
2428
+ const active = document.activeElement;
2429
+ if (active === snapshot.el) return;
2430
+ if (active && active !== document.body && active !== document.documentElement) return;
2431
+ if (snapshot.el.isConnected) {
2432
+ refocus(snapshot.el, snapshot);
2433
+ return;
2434
+ }
2435
+ let ambiguous = false;
2436
+ if (snapshot.attr && snapshot.value) {
2437
+ const { el: replacement, count } = findUniqueByIdentity(nodes, `[${snapshot.attr}=${quote(snapshot.value)}]`);
2438
+ if (replacement && replacement.tagName === snapshot.tag) {
2439
+ refocus(replacement, snapshot);
2440
+ return;
2441
+ }
2442
+ ambiguous = count > 1;
2443
+ }
2444
+ devWarnLazy(() => {
2445
+ const id = snapshot.attr ? ` (${snapshot.attr}="${snapshot.value}")` : "";
2446
+ 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";
2447
+ 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.`;
2448
+ });
2449
+ }
2450
+
2288
2451
  // src/reactivity/bindChildNode.ts
2289
- var _isDev5 = isDev();
2290
2452
  function bindChildNode(placeholder, getter) {
2291
2453
  let lastNodes = [];
2292
2454
  function commit() {
@@ -2297,6 +2459,7 @@ function bindChildNode(placeholder, getter) {
2297
2459
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
2298
2460
  return;
2299
2461
  }
2462
+ const focused = captureFocusWithin(lastNodes);
2300
2463
  if (result == null || typeof result === "boolean") {
2301
2464
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
2302
2465
  const node = lastNodes[i2];
@@ -2304,6 +2467,7 @@ function bindChildNode(placeholder, getter) {
2304
2467
  if (node.parentNode) node.parentNode.removeChild(node);
2305
2468
  }
2306
2469
  lastNodes.length = 0;
2470
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
2307
2471
  return;
2308
2472
  }
2309
2473
  const parent = placeholder.parentNode;
@@ -2320,8 +2484,7 @@ function bindChildNode(placeholder, getter) {
2320
2484
  if (item == null || typeof item === "boolean") continue;
2321
2485
  const node = item instanceof Node ? item : document.createTextNode(String(item));
2322
2486
  if (seen.has(node)) {
2323
- if (_isDev5)
2324
- devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
2487
+ if (DEV) devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
2325
2488
  continue;
2326
2489
  }
2327
2490
  seen.add(node);
@@ -2354,13 +2517,13 @@ function bindChildNode(placeholder, getter) {
2354
2517
  prev = node;
2355
2518
  }
2356
2519
  lastNodes = newNodes;
2520
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
2357
2521
  }
2358
2522
  return reactiveBinding2(commit, placeholder);
2359
2523
  }
2360
2524
 
2361
2525
  // src/core/rendering/tagFactory.ts
2362
2526
  var SVG_NS2 = "http://www.w3.org/2000/svg";
2363
- var _isDev6 = isDev();
2364
2527
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
2365
2528
  function isBlockedTag(tag) {
2366
2529
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -2378,18 +2541,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
2378
2541
  "self",
2379
2542
  "frames"
2380
2543
  ]);
2381
- function looksLikeClassList(s2) {
2382
- const t = s2.trim();
2383
- if (!t) return false;
2384
- const tokens = t.split(/\s+/);
2385
- let sawClassish = false;
2386
- for (let i2 = 0; i2 < tokens.length; i2++) {
2387
- const tok = tokens[i2];
2388
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
2389
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
2390
- }
2391
- return sawClassish;
2392
- }
2544
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
2545
+ var MAX_WARNED_LONE_STRINGS = 100;
2546
+ var loneStringCapAnnounced = false;
2393
2547
  var kebabCache = /* @__PURE__ */ new Map();
2394
2548
  function toKebab(prop) {
2395
2549
  let cached = kebabCache.get(prop);
@@ -2401,13 +2555,13 @@ function toKebab(prop) {
2401
2555
  function applyStyle(el, style2) {
2402
2556
  if (typeof style2 === "function") {
2403
2557
  const teardown = reactiveBinding2(() => {
2404
- el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
2558
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2()), { element: el }));
2405
2559
  }, el);
2406
2560
  registerDisposer(el, teardown);
2407
2561
  return;
2408
2562
  }
2409
2563
  if (typeof style2 === "string") {
2410
- el.setAttribute("style", sanitizeStyleAttribute(style2));
2564
+ el.setAttribute("style", sanitizeStyleAttribute(style2, { element: el }));
2411
2565
  return;
2412
2566
  }
2413
2567
  const htmlEl = el;
@@ -2417,11 +2571,11 @@ function applyStyle(el, style2) {
2417
2571
  if (typeof val === "function") {
2418
2572
  const getter = val;
2419
2573
  const teardown = reactiveBinding2(() => {
2420
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
2574
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
2421
2575
  }, el);
2422
2576
  registerDisposer(el, teardown);
2423
2577
  } else {
2424
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
2578
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
2425
2579
  }
2426
2580
  }
2427
2581
  }
@@ -2528,11 +2682,26 @@ var tagFactory = (tag, ns) => {
2528
2682
  appendChildren(el, second);
2529
2683
  return el;
2530
2684
  }
2531
- if (_isDev6 && looksLikeClassList(first)) {
2532
- devWarn(
2533
- `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.`
2534
- );
2535
- }
2685
+ devWarnLazy(() => {
2686
+ const tokens = first.trim().split(/\s+/);
2687
+ if (tokens.length < 2) return "";
2688
+ let utilityTokens = 0;
2689
+ for (let i2 = 0; i2 < tokens.length; i2++) {
2690
+ const tok = tokens[i2];
2691
+ if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return "";
2692
+ if (/[-:/0-9]/.test(tok)) utilityTokens++;
2693
+ }
2694
+ if (utilityTokens < 2) return "";
2695
+ const key = `${tag}|${first}`;
2696
+ if (warnedLoneStrings.has(key)) return "";
2697
+ if (warnedLoneStrings.size >= MAX_WARNED_LONE_STRINGS) {
2698
+ if (loneStringCapAnnounced) return "";
2699
+ loneStringCapAnnounced = true;
2700
+ 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.`;
2701
+ }
2702
+ warnedLoneStrings.add(key);
2703
+ 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.`;
2704
+ });
2536
2705
  el.textContent = first;
2537
2706
  return el;
2538
2707
  }
@@ -2549,7 +2718,7 @@ var tagFactory = (tag, ns) => {
2549
2718
  if (pClass != null) applyClass(el, pClass);
2550
2719
  const pId = props.id;
2551
2720
  if (pId != null) {
2552
- if (_isDev6 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2721
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2553
2722
  devWarn(
2554
2723
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
2555
2724
  );
@@ -2564,7 +2733,7 @@ var tagFactory = (tag, ns) => {
2564
2733
  const handler = pOn[ev];
2565
2734
  if (typeof handler === "function") {
2566
2735
  el.addEventListener(ev, handler);
2567
- } else if (_isDev6) {
2736
+ } else if (DEV) {
2568
2737
  devWarn(
2569
2738
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
2570
2739
  );
@@ -3363,7 +3532,6 @@ var isBatching = API2.isBatching;
3363
3532
 
3364
3533
  // src/core/signals/signal.ts
3365
3534
  var _g2 = globalThis;
3366
- var _isDev7 = isDev();
3367
3535
  function signal(initial, options) {
3368
3536
  const state2 = {
3369
3537
  value: initial,
@@ -3376,7 +3544,7 @@ function signal(initial, options) {
3376
3544
  _d: false,
3377
3545
  _validate: void 0
3378
3546
  };
3379
- const debugName = _isDev7 ? options?.name : void 0;
3547
+ const debugName = DEV ? options?.name : void 0;
3380
3548
  const equalsFn = options?.equals;
3381
3549
  if (debugName) state2.__name = debugName;
3382
3550
  function get() {
@@ -3393,7 +3561,7 @@ function signal(initial, options) {
3393
3561
  if (equalsFn(prev, newValue)) return;
3394
3562
  state2.value = newValue;
3395
3563
  state2.__v++;
3396
- if (_isDev7) {
3564
+ if (DEV) {
3397
3565
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
3398
3566
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
3399
3567
  }
@@ -3401,7 +3569,7 @@ function signal(initial, options) {
3401
3569
  notifySubscribers2(state2);
3402
3570
  }
3403
3571
  };
3404
- } else if (_isDev7) {
3572
+ } else if (DEV) {
3405
3573
  set = (next) => {
3406
3574
  const prev = state2.value;
3407
3575
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -3426,12 +3594,23 @@ function signal(initial, options) {
3426
3594
  }
3427
3595
  };
3428
3596
  }
3429
- if (_isDev7) {
3597
+ if (DEV) {
3430
3598
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
3431
3599
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
3432
3600
  }
3433
3601
  return [get, set];
3434
3602
  }
3603
+ function external(options) {
3604
+ const [version, bump] = signal(0, options?.name ? { name: options.name } : void 0);
3605
+ return {
3606
+ track() {
3607
+ version();
3608
+ },
3609
+ invalidate() {
3610
+ bump((n) => n + 1);
3611
+ }
3612
+ };
3613
+ }
3435
3614
 
3436
3615
  // src/components/ErrorDisplay.ts
3437
3616
  var STYLES = `
@@ -4407,11 +4586,10 @@ function setGlobalErrorHandler(handler) {
4407
4586
  }
4408
4587
 
4409
4588
  // src/core/rendering/context.ts
4410
- var _isDev8 = isDev();
4411
4589
  function context(defaultValue) {
4412
4590
  const [getValue, setValue] = signal(defaultValue);
4413
4591
  const warnIfSSR = (method) => {
4414
- if (!_isDev8 || !isSSR()) return;
4592
+ if (!DEV || !isSSR()) return;
4415
4593
  devWarn(
4416
4594
  `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.`
4417
4595
  );
@@ -4439,7 +4617,7 @@ function context(defaultValue) {
4439
4617
  setValue(value);
4440
4618
  try {
4441
4619
  const result = fn();
4442
- if (_isDev8 && result !== null && typeof result?.then === "function") {
4620
+ if (DEV && result !== null && typeof result?.then === "function") {
4443
4621
  devWarn(
4444
4622
  "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."
4445
4623
  );
@@ -4466,34 +4644,53 @@ function __resetIdCounter() {
4466
4644
 
4467
4645
  // src/core/rendering/directives.ts
4468
4646
  function show(condition, element) {
4647
+ const resolved = typeof element === "function" ? element() : element;
4469
4648
  const update = () => {
4470
- element.style.display = condition() ? "" : "none";
4649
+ resolved.style.display = condition() ? "" : "none";
4471
4650
  };
4472
- registerDisposer(element, track2(update));
4473
- return element;
4651
+ registerDisposer(resolved, track2(update));
4652
+ return resolved;
4653
+ }
4654
+ function attachBranch(parent, anchor, branch, attachedOnce, where) {
4655
+ const result = typeof branch === "function" ? branch() : branch;
4656
+ if (result == null || typeof result === "boolean") return { node: null, owned: false };
4657
+ const node = result instanceof Node ? result : document.createTextNode(String(result));
4658
+ const handedIn = node === branch;
4659
+ const seenBefore = attachedOnce.has(node);
4660
+ const owned = !handedIn && !seenBefore;
4661
+ attachedOnce.add(node);
4662
+ if (seenBefore) {
4663
+ devWarnLazy(
4664
+ () => `${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.`
4665
+ );
4666
+ }
4667
+ parent.insertBefore(node, anchor.nextSibling);
4668
+ return { node, owned };
4474
4669
  }
4475
4670
  function when(condition, thenBranch, elseBranch) {
4476
4671
  const anchor = document.createComment("when");
4477
4672
  let currentNode = null;
4478
4673
  let lastCondition;
4479
4674
  let initialized = false;
4675
+ const attachedOnce = /* @__PURE__ */ new WeakSet();
4676
+ let currentNodeOwned = false;
4480
4677
  const update = () => {
4481
4678
  const show2 = condition();
4482
4679
  const parent = anchor.parentNode;
4483
4680
  if (!parent) return;
4484
4681
  if (initialized && show2 === lastCondition) return;
4485
4682
  lastCondition = show2;
4683
+ const focused = currentNode ? captureFocusWithin([currentNode]) : null;
4486
4684
  if (currentNode?.parentNode) {
4487
- dispose(currentNode);
4685
+ if (currentNodeOwned) dispose(currentNode);
4488
4686
  currentNode.parentNode.removeChild(currentNode);
4489
4687
  currentNode = null;
4490
4688
  }
4491
- const result = show2 ? thenBranch() : elseBranch ? elseBranch() : null;
4492
- if (result != null) {
4493
- const node = result instanceof Node ? result : document.createTextNode(String(result));
4494
- parent.insertBefore(node, anchor.nextSibling);
4495
- currentNode = node;
4496
- }
4689
+ const branch = show2 ? thenBranch : elseBranch !== void 0 ? elseBranch : null;
4690
+ const attached = attachBranch(parent, anchor, branch, attachedOnce, "when");
4691
+ currentNode = attached.node;
4692
+ currentNodeOwned = attached.owned;
4693
+ restoreFocusWithin(focused, currentNode ? [currentNode] : [], "when");
4497
4694
  initialized = true;
4498
4695
  };
4499
4696
  registerDisposer(anchor, track2(update));
@@ -4509,26 +4706,25 @@ function match(value, cases, fallback) {
4509
4706
  let currentNode = null;
4510
4707
  let lastKey;
4511
4708
  let initialized = false;
4709
+ const attachedOnce = /* @__PURE__ */ new WeakSet();
4710
+ let currentNodeOwned = false;
4512
4711
  const update = () => {
4513
4712
  const key = String(value());
4514
4713
  const parent = anchor.parentNode;
4515
4714
  if (!parent) return;
4516
4715
  if (initialized && key === lastKey) return;
4517
4716
  lastKey = key;
4717
+ const focused = currentNode ? captureFocusWithin([currentNode]) : null;
4518
4718
  if (currentNode?.parentNode) {
4519
- dispose(currentNode);
4719
+ if (currentNodeOwned) dispose(currentNode);
4520
4720
  currentNode.parentNode.removeChild(currentNode);
4521
4721
  currentNode = null;
4522
4722
  }
4523
- const renderFn = cases[key] || fallback;
4524
- if (renderFn) {
4525
- const result = renderFn();
4526
- if (result != null) {
4527
- const node = result instanceof Node ? result : document.createTextNode(String(result));
4528
- parent.insertBefore(node, anchor.nextSibling);
4529
- currentNode = node;
4530
- }
4531
- }
4723
+ const branch = Object.hasOwn(cases, key) ? cases[key] : fallback;
4724
+ const attached = attachBranch(parent, anchor, branch ?? null, attachedOnce, "match");
4725
+ currentNode = attached.node;
4726
+ currentNodeOwned = attached.owned;
4727
+ restoreFocusWithin(focused, currentNode ? [currentNode] : [], "match");
4532
4728
  initialized = true;
4533
4729
  };
4534
4730
  registerDisposer(anchor, track2(update));
@@ -4573,7 +4769,6 @@ function DynamicComponent(is) {
4573
4769
  }
4574
4770
 
4575
4771
  // src/core/rendering/each.ts
4576
- var _isDev9 = isDev();
4577
4772
  function resolveNodeChild(child) {
4578
4773
  if (typeof child === "function") {
4579
4774
  return resolveNodeChild(child());
@@ -4664,7 +4859,7 @@ function each(getArray, render, options) {
4664
4859
  workMap.clear();
4665
4860
  keyIndexMap.clear();
4666
4861
  for (let i2 = 0; i2 < newLen; i2++) {
4667
- if (_isDev9 && keyIndexMap.has(newKeys[i2])) {
4862
+ if (DEV && keyIndexMap.has(newKeys[i2])) {
4668
4863
  devWarn(
4669
4864
  `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.`
4670
4865
  );
@@ -4848,7 +5043,6 @@ function resolveChild(child) {
4848
5043
  }
4849
5044
 
4850
5045
  // src/core/rendering/htm.ts
4851
- var _isDev10 = isDev();
4852
5046
  var RAW_TEXT_TAGS = /* @__PURE__ */ new Set(["script", "style"]);
4853
5047
  var VOID_ELEMENTS3 = /* @__PURE__ */ new Set([
4854
5048
  "area",
@@ -4935,13 +5129,13 @@ function parseTemplate(strings) {
4935
5129
  skipWs();
4936
5130
  const exprIdx = tryExprIdx();
4937
5131
  if (exprIdx >= 0) return { kind: "expr", idx: exprIdx };
4938
- const quote = template2[pos];
4939
- if (quote === '"' || quote === "'") {
5132
+ const quote2 = template2[pos];
5133
+ if (quote2 === '"' || quote2 === "'") {
4940
5134
  pos++;
4941
5135
  const statics = [];
4942
5136
  const exprs = [];
4943
5137
  let current = "";
4944
- while (pos < len && template2[pos] !== quote) {
5138
+ while (pos < len && template2[pos] !== quote2) {
4945
5139
  const innerIdx = tryExprIdx();
4946
5140
  if (innerIdx >= 0) {
4947
5141
  statics.push(current);
@@ -5128,7 +5322,7 @@ function executeElement(tmpl, values) {
5128
5322
  const fn = values[attr.idx];
5129
5323
  if (typeof fn === "function") {
5130
5324
  el.addEventListener(attr.name, fn);
5131
- } else if (_isDev10) {
5325
+ } else if (DEV) {
5132
5326
  devWarn(
5133
5327
  `html: on:${attr.name} handler is not a function (got ${typeof fn}). Event listener was not attached.`
5134
5328
  );
@@ -5996,6 +6190,7 @@ function drainTeardowns(teardowns, label2) {
5996
6190
  }
5997
6191
  }
5998
6192
  }
6193
+ var EACH_KEYS = ["text", "attr", "class", "show", "on", "cleanup"];
5999
6194
  function resolveTarget(root, target) {
6000
6195
  if (target == null) return root;
6001
6196
  if (typeof target !== "string") return target;
@@ -6006,6 +6201,11 @@ function resolveTarget(root, target) {
6006
6201
  return null;
6007
6202
  }
6008
6203
  }
6204
+ function bindNode(el, commit) {
6205
+ if (isSSR()) return () => {
6206
+ };
6207
+ return reactiveBinding2(commit, el);
6208
+ }
6009
6209
  function readControlValue(el) {
6010
6210
  if (el instanceof HTMLInputElement) {
6011
6211
  if (el.type === "checkbox") return el.checked;
@@ -6029,6 +6229,50 @@ function writeControlValue(el, value) {
6029
6229
  const next = value == null ? "" : String(value);
6030
6230
  if (el.value !== next) el.value = next;
6031
6231
  }
6232
+ function applyEachBindings(ctx, el, spec, index) {
6233
+ const where = `ctx.each[${index}]`;
6234
+ if (isDev()) {
6235
+ for (const key of Object.keys(spec)) {
6236
+ devAssert(
6237
+ EACH_KEYS.includes(key),
6238
+ `${where}: unknown binding "${key}". Expected one of: ${EACH_KEYS.join(", ")}.`
6239
+ );
6240
+ }
6241
+ }
6242
+ if (spec.text !== void 0) {
6243
+ devAssert(typeof spec.text === "function", `${where}: "text" must be a function returning the value.`);
6244
+ ctx.text(el, spec.text);
6245
+ }
6246
+ if (spec.attr !== void 0) {
6247
+ for (const name of Object.keys(spec.attr)) {
6248
+ const value = spec.attr[name];
6249
+ devAssert(typeof value === "function", `${where}: attr["${name}"] must be a function returning the value.`);
6250
+ ctx.attr(el, name, value);
6251
+ }
6252
+ }
6253
+ if (spec.class !== void 0) {
6254
+ for (const name of Object.keys(spec.class)) {
6255
+ const on2 = spec.class[name];
6256
+ devAssert(typeof on2 === "function", `${where}: class["${name}"] must be a function returning a boolean.`);
6257
+ ctx.classed(el, name, on2);
6258
+ }
6259
+ }
6260
+ if (spec.show !== void 0) {
6261
+ devAssert(typeof spec.show === "function", `${where}: "show" must be a function returning a boolean.`);
6262
+ ctx.show(el, spec.show);
6263
+ }
6264
+ if (spec.on !== void 0) {
6265
+ for (const event of Object.keys(spec.on)) {
6266
+ const handler = spec.on[event];
6267
+ devAssert(typeof handler === "function", `${where}: on["${String(event)}"] must be a function.`);
6268
+ ctx.on(el, event, handler);
6269
+ }
6270
+ }
6271
+ if (spec.cleanup !== void 0) {
6272
+ devAssert(typeof spec.cleanup === "function", `${where}: "cleanup" must be a function.`);
6273
+ ctx.cleanup(spec.cleanup);
6274
+ }
6275
+ }
6032
6276
  function enhance(target, setup) {
6033
6277
  const root = typeof target === "string" ? typeof document !== "undefined" ? document.querySelector(target) : null : target;
6034
6278
  if (!root) {
@@ -6079,7 +6323,7 @@ function enhance(target, setup) {
6079
6323
  text: (t, value) => {
6080
6324
  bind(t, (el) => {
6081
6325
  teardowns.push(
6082
- effect(() => {
6326
+ bindNode(el, () => {
6083
6327
  const v = value();
6084
6328
  const next = v == null ? "" : String(v);
6085
6329
  if (el.textContent !== next) el.textContent = next;
@@ -6090,7 +6334,7 @@ function enhance(target, setup) {
6090
6334
  attr: (t, name, value) => {
6091
6335
  bind(t, (el) => {
6092
6336
  teardowns.push(
6093
- effect(() => {
6337
+ bindNode(el, () => {
6094
6338
  const v = value();
6095
6339
  const next = v == null ? null : String(v);
6096
6340
  setSafeAttribute(el, name, next, { syncValueProperty: false, label: "enhance attr()" });
@@ -6101,7 +6345,7 @@ function enhance(target, setup) {
6101
6345
  classed: (t, name, on2) => {
6102
6346
  bind(t, (el) => {
6103
6347
  teardowns.push(
6104
- effect(() => {
6348
+ bindNode(el, () => {
6105
6349
  el.classList.toggle(name, Boolean(on2()));
6106
6350
  })
6107
6351
  );
@@ -6111,7 +6355,7 @@ function enhance(target, setup) {
6111
6355
  bind(t, (el) => {
6112
6356
  const prevHidden = el.hidden;
6113
6357
  teardowns.push(
6114
- effect(() => {
6358
+ bindNode(el, () => {
6115
6359
  el.hidden = !when2();
6116
6360
  })
6117
6361
  );
@@ -6126,7 +6370,7 @@ function enhance(target, setup) {
6126
6370
  const [get, set] = state2;
6127
6371
  const evt = options?.event ?? (control instanceof HTMLSelectElement || control instanceof HTMLInputElement && (control.type === "checkbox" || control.type === "radio") ? "change" : "input");
6128
6372
  teardowns.push(
6129
- effect(() => {
6373
+ bindNode(el, () => {
6130
6374
  writeControlValue(control, get());
6131
6375
  })
6132
6376
  );
@@ -6135,6 +6379,16 @@ function enhance(target, setup) {
6135
6379
  teardowns.push(() => control.removeEventListener(evt, onInput));
6136
6380
  });
6137
6381
  },
6382
+ each: (target_, describe2) => {
6383
+ devAssert(typeof describe2 === "function", "ctx.each: second argument must be a function.");
6384
+ const elements = typeof target_ === "string" ? ctx.refs(target_) : Array.from(target_);
6385
+ for (let i2 = 0; i2 < elements.length; i2++) {
6386
+ const el = elements[i2];
6387
+ const spec = describe2(el, i2);
6388
+ if (spec == null) continue;
6389
+ applyEachBindings(ctx, el, spec, i2);
6390
+ }
6391
+ },
6138
6392
  cleanup: (fn) => {
6139
6393
  teardowns.push(fn);
6140
6394
  }
@@ -6343,7 +6597,6 @@ function schedule(strategy, el, activate, options) {
6343
6597
  }
6344
6598
 
6345
6599
  // src/platform/ssr.ts
6346
- var _isDev11 = isDev();
6347
6600
  function trustHTML(html3) {
6348
6601
  return html3;
6349
6602
  }