sibujs 4.1.0 → 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 +24 -0
  2. package/dist/browser.cjs +13 -21
  3. package/dist/browser.js +6 -6
  4. package/dist/build.cjs +278 -104
  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-DCDS2PQ6.js → chunk-2LOYNGBY.js} +2 -2
  9. package/dist/{chunk-VPP2FONR.js → chunk-2WLZ6757.js} +17 -10
  10. package/dist/{chunk-IM7N5WR4.js → chunk-3VTSPNC5.js} +2 -2
  11. package/dist/{chunk-TIRZCERI.js → chunk-5DXA2J44.js} +1 -1
  12. package/dist/{chunk-I3VI34DP.js → chunk-6ZGI3RBE.js} +149 -33
  13. package/dist/{chunk-D3SZ4LVS.js → chunk-7ALC7KGE.js} +5 -6
  14. package/dist/{chunk-PFTDGEPZ.js → chunk-7VEC3UD2.js} +3 -3
  15. package/dist/{chunk-KSOEJGFO.js → chunk-BMYYZWHX.js} +3 -3
  16. package/dist/{chunk-LOFZHP6V.js → chunk-BNKV4REK.js} +7 -8
  17. package/dist/{chunk-5GJ5RTHA.js → chunk-FOWBJZDL.js} +7 -8
  18. package/dist/{chunk-FB7KQXZI.js → chunk-GDUNQCCW.js} +4 -4
  19. package/dist/{chunk-UFSQEO7P.js → chunk-HECVW343.js} +7 -8
  20. package/dist/{chunk-SHDVWATN.js → chunk-HJEPKZ2O.js} +7 -8
  21. package/dist/{chunk-ANGSYNPX.js → chunk-HZEBO22J.js} +57 -37
  22. package/dist/{chunk-5INI7D2L.js → chunk-IXKSNWV5.js} +74 -11
  23. package/dist/{chunk-DAKLOMKS.js → chunk-KE4KWRDY.js} +3 -3
  24. package/dist/{chunk-AW6HPJ3L.js → chunk-KKLW7YWL.js} +1 -1
  25. package/dist/{chunk-BMSN6NWQ.js → chunk-NEYWS4EX.js} +6 -6
  26. package/dist/{chunk-HYCCIYNS.js → chunk-NIOYEGBQ.js} +13 -14
  27. package/dist/{chunk-4U7KD3RY.js → chunk-OUWAIXIK.js} +3 -3
  28. package/dist/{chunk-MRAMKDHQ.js → chunk-P3DHXBR6.js} +1 -1
  29. package/dist/{chunk-FMHIQNMP.js → chunk-QCFLDD7C.js} +5 -5
  30. package/dist/{chunk-DHDZ7IVN.js → chunk-QKRPLZ2V.js} +7 -8
  31. package/dist/{chunk-4RH6D7GR.js → chunk-QL4UREZO.js} +6 -6
  32. package/dist/{chunk-XIHMS2Y6.js → chunk-SF3TB4K6.js} +8 -8
  33. package/dist/{chunk-XSZRJW76.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 +15 -24
  37. package/dist/data.js +9 -9
  38. package/dist/devtools.cjs +18 -22
  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 +222 -67
  44. package/dist/ecosystem.js +11 -11
  45. package/dist/extras.cjs +243 -86
  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 +272 -98
  50. package/dist/index.d.cts +154 -33
  51. package/dist/index.d.ts +154 -33
  52. package/dist/index.js +15 -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 +13 -22
  56. package/dist/motion.js +5 -5
  57. package/dist/patterns.cjs +16 -24
  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 +16 -27
  62. package/dist/performance.js +9 -9
  63. package/dist/plugins.cjs +260 -86
  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 +233 -79
  69. package/dist/ssr.js +12 -12
  70. package/dist/testing.cjs +11 -19
  71. package/dist/testing.js +5 -5
  72. package/dist/ui.cjs +98 -43
  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 +13 -21
  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 +5 -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 });
@@ -1382,23 +1382,28 @@ function reportError(error, context2) {
1382
1382
  }
1383
1383
 
1384
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
+ );
1385
1389
  function isDev() {
1386
- return typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
1387
- // The bare `__SIBU_DEV__` is a bundler define that only exists in
1388
- // production builds; under the test runner it is always undefined, so
1389
- // this branch is unreachable here.
1390
- /* v8 ignore next 2 */
1391
- typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
1392
- );
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";
1393
1393
  }
1394
- var _isDev = isDev();
1395
1394
  function devAssert(condition, message) {
1396
- if (_isDev && !condition) {
1395
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
1397
1396
  throw new Error(`[SibuJS] ${message}`);
1398
1397
  }
1399
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
+ }
1400
1405
  function devWarn(message) {
1401
- if (_isDev) {
1406
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
1402
1407
  console.warn(`[SibuJS] ${message}`);
1403
1408
  }
1404
1409
  }
@@ -1413,7 +1418,6 @@ function reportDrainRunaway(label2, executed, remaining) {
1413
1418
  { executed, remaining }
1414
1419
  );
1415
1420
  }
1416
- var _isDev2 = isDev();
1417
1421
  var activeBindingCount = 0;
1418
1422
  function registerDisposer(node, teardown) {
1419
1423
  let disposers = elementDisposers.get(node);
@@ -1422,7 +1426,7 @@ function registerDisposer(node, teardown) {
1422
1426
  elementDisposers.set(node, disposers);
1423
1427
  }
1424
1428
  disposers.push(teardown);
1425
- if (_isDev2) activeBindingCount++;
1429
+ if (DEV) activeBindingCount++;
1426
1430
  }
1427
1431
  function unregisterDisposer(node, teardown) {
1428
1432
  const disposers = elementDisposers.get(node);
@@ -1430,7 +1434,7 @@ function unregisterDisposer(node, teardown) {
1430
1434
  const index = disposers.indexOf(teardown);
1431
1435
  if (index === -1) return;
1432
1436
  disposers.splice(index, 1);
1433
- if (_isDev2) activeBindingCount--;
1437
+ if (DEV) activeBindingCount--;
1434
1438
  if (disposers.length === 0) elementDisposers.delete(node);
1435
1439
  }
1436
1440
  function dispose(node) {
@@ -1454,13 +1458,13 @@ function dispose(node) {
1454
1458
  if (!pending || pending.length === 0) break;
1455
1459
  const snapshot = pending.slice();
1456
1460
  elementDisposers.delete(current);
1457
- if (_isDev2) activeBindingCount -= snapshot.length;
1461
+ if (DEV) activeBindingCount -= snapshot.length;
1458
1462
  for (let i3 = 0; i3 < snapshot.length; i3++) {
1459
1463
  if (executed >= MAX_DRAIN_TEARDOWNS) {
1460
1464
  const rest = snapshot.slice(i3);
1461
1465
  const added = elementDisposers.get(current);
1462
1466
  elementDisposers.set(current, added ? rest.concat(added) : rest);
1463
- if (_isDev2) activeBindingCount += rest.length;
1467
+ if (DEV) activeBindingCount += rest.length;
1464
1468
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
1465
1469
  runaway = true;
1466
1470
  break;
@@ -1488,7 +1492,7 @@ function replaceChildrenSafely(parent, ...next) {
1488
1492
  parent.replaceChildren(...next);
1489
1493
  }
1490
1494
  function checkLeaks(warnThreshold = 0) {
1491
- if (!_isDev2) return 0;
1495
+ if (!DEV) return 0;
1492
1496
  if (warnThreshold > 0 && activeBindingCount > warnThreshold) {
1493
1497
  devWarn(
1494
1498
  `checkLeaks: ${activeBindingCount} active DOM bindings detected. Expected \u2264${warnThreshold}. This may indicate a component was removed from the DOM without calling dispose().`
@@ -1590,20 +1594,75 @@ function decodeCssEscapes(value) {
1590
1594
  }
1591
1595
  return out;
1592
1596
  }
1593
- function sanitizeCSSValue(value) {
1594
- 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;
1595
1648
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
1596
1649
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
1597
- 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")) {
1598
- return "";
1650
+ for (let i2 = 0; i2 < BLOCKED_CSS_NEEDLES.length; i2++) {
1651
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i2])) return i2;
1599
1652
  }
1600
- return value;
1653
+ return -1;
1654
+ }
1655
+ function sanitizeCSSValue(value, context2) {
1656
+ const reason = blockedCssReason(value);
1657
+ if (reason === -1) return value;
1658
+ warnDroppedDeclaration(value, reason, context2);
1659
+ return "";
1601
1660
  }
1602
- function sanitizeStyleAttribute(cssText) {
1661
+ function sanitizeStyleAttribute(cssText, context2) {
1603
1662
  const input2 = String(cssText);
1604
1663
  if (input2.trim() === "") return "";
1605
1664
  if (typeof document === "undefined") {
1606
- return sanitizeCSSValue(input2) === "" ? "" : input2;
1665
+ return sanitizeCSSValue(input2, context2) === "" ? "" : input2;
1607
1666
  }
1608
1667
  const probe = document.createElement("div");
1609
1668
  try {
@@ -1615,8 +1674,12 @@ function sanitizeStyleAttribute(cssText) {
1615
1674
  for (let i2 = 0; i2 < probe.style.length; i2++) {
1616
1675
  const property = probe.style[i2];
1617
1676
  const value = probe.style.getPropertyValue(property);
1618
- if (sanitizeCSSValue(value) === "") continue;
1619
- 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
+ }
1620
1683
  const priority = probe.style.getPropertyPriority(property);
1621
1684
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
1622
1685
  }
@@ -1644,16 +1707,15 @@ var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
1644
1707
  function isHtmlContentAttribute(name) {
1645
1708
  return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
1646
1709
  }
1647
- function sanitizeAttributeString(attr, value) {
1710
+ function sanitizeAttributeString(attr, value, context2) {
1648
1711
  const lower = attr.toLowerCase();
1649
1712
  if (lower === "srcset") return sanitizeSrcset(value);
1650
1713
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
1651
- if (lower === "style") return sanitizeStyleAttribute(value);
1714
+ if (lower === "style") return sanitizeStyleAttribute(value, context2);
1652
1715
  return value;
1653
1716
  }
1654
1717
 
1655
1718
  // src/utils/setSafeAttribute.ts
1656
- var _isDev3 = isDev();
1657
1719
  var HTML_NS = "http://www.w3.org/1999/xhtml";
1658
1720
  var SVG_NS = "http://www.w3.org/2000/svg";
1659
1721
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -1676,7 +1738,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1676
1738
  const ns = namespaceFor(el, name);
1677
1739
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
1678
1740
  if (isEventHandlerAttr(name)) {
1679
- if (_isDev3) {
1741
+ if (DEV) {
1680
1742
  devWarn(
1681
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.`
1682
1744
  );
@@ -1691,7 +1753,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1691
1753
  return true;
1692
1754
  }
1693
1755
  if (isHtmlContentAttribute(name)) {
1694
- if (_isDev3) {
1756
+ if (DEV) {
1695
1757
  devWarn(
1696
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.`
1697
1759
  );
@@ -1720,7 +1782,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1720
1782
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
1721
1783
  return true;
1722
1784
  }
1723
- const safe = sanitizeAttributeString(name, str);
1785
+ const safe = sanitizeAttributeString(name, str, { element: el });
1724
1786
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
1725
1787
  if (current === safe) return true;
1726
1788
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -2184,17 +2246,16 @@ function forEachSubscriber(signal2, visit) {
2184
2246
  }
2185
2247
 
2186
2248
  // src/reactivity/track.ts
2187
- var _isDev4 = isDev();
2188
- var _runtimeVersion = true ? "4.1.0" : "dev";
2249
+ var _runtimeVersion = true ? "4.2.0" : "dev";
2189
2250
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
2190
2251
  function resolveReactiveApi() {
2191
2252
  const g2 = globalThis;
2192
2253
  const existing = g2[REGISTRY_KEY];
2193
2254
  if (existing) {
2194
- if (_isDev4 && !existing.__dupWarned) {
2255
+ if (DEV && !existing.__dupWarned) {
2195
2256
  existing.__dupWarned = true;
2196
2257
  devWarn(
2197
- `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.`
2198
2259
  );
2199
2260
  }
2200
2261
  return existing;
@@ -2293,8 +2354,101 @@ function bindDynamic(el, nameGetter, valueGetter) {
2293
2354
  };
2294
2355
  }
2295
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
+
2296
2451
  // src/reactivity/bindChildNode.ts
2297
- var _isDev5 = isDev();
2298
2452
  function bindChildNode(placeholder, getter) {
2299
2453
  let lastNodes = [];
2300
2454
  function commit() {
@@ -2305,6 +2459,7 @@ function bindChildNode(placeholder, getter) {
2305
2459
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
2306
2460
  return;
2307
2461
  }
2462
+ const focused = captureFocusWithin(lastNodes);
2308
2463
  if (result == null || typeof result === "boolean") {
2309
2464
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
2310
2465
  const node = lastNodes[i2];
@@ -2312,6 +2467,7 @@ function bindChildNode(placeholder, getter) {
2312
2467
  if (node.parentNode) node.parentNode.removeChild(node);
2313
2468
  }
2314
2469
  lastNodes.length = 0;
2470
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
2315
2471
  return;
2316
2472
  }
2317
2473
  const parent = placeholder.parentNode;
@@ -2328,8 +2484,7 @@ function bindChildNode(placeholder, getter) {
2328
2484
  if (item == null || typeof item === "boolean") continue;
2329
2485
  const node = item instanceof Node ? item : document.createTextNode(String(item));
2330
2486
  if (seen.has(node)) {
2331
- if (_isDev5)
2332
- 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.");
2333
2488
  continue;
2334
2489
  }
2335
2490
  seen.add(node);
@@ -2362,13 +2517,13 @@ function bindChildNode(placeholder, getter) {
2362
2517
  prev = node;
2363
2518
  }
2364
2519
  lastNodes = newNodes;
2520
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
2365
2521
  }
2366
2522
  return reactiveBinding2(commit, placeholder);
2367
2523
  }
2368
2524
 
2369
2525
  // src/core/rendering/tagFactory.ts
2370
2526
  var SVG_NS2 = "http://www.w3.org/2000/svg";
2371
- var _isDev6 = isDev();
2372
2527
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
2373
2528
  function isBlockedTag(tag) {
2374
2529
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -2386,18 +2541,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
2386
2541
  "self",
2387
2542
  "frames"
2388
2543
  ]);
2389
- function looksLikeClassList(s2) {
2390
- const t = s2.trim();
2391
- if (!t) return false;
2392
- const tokens = t.split(/\s+/);
2393
- let sawClassish = false;
2394
- for (let i2 = 0; i2 < tokens.length; i2++) {
2395
- const tok = tokens[i2];
2396
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
2397
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
2398
- }
2399
- return sawClassish;
2400
- }
2544
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
2545
+ var MAX_WARNED_LONE_STRINGS = 100;
2546
+ var loneStringCapAnnounced = false;
2401
2547
  var kebabCache = /* @__PURE__ */ new Map();
2402
2548
  function toKebab(prop) {
2403
2549
  let cached = kebabCache.get(prop);
@@ -2409,13 +2555,13 @@ function toKebab(prop) {
2409
2555
  function applyStyle(el, style2) {
2410
2556
  if (typeof style2 === "function") {
2411
2557
  const teardown = reactiveBinding2(() => {
2412
- el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
2558
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2()), { element: el }));
2413
2559
  }, el);
2414
2560
  registerDisposer(el, teardown);
2415
2561
  return;
2416
2562
  }
2417
2563
  if (typeof style2 === "string") {
2418
- el.setAttribute("style", sanitizeStyleAttribute(style2));
2564
+ el.setAttribute("style", sanitizeStyleAttribute(style2, { element: el }));
2419
2565
  return;
2420
2566
  }
2421
2567
  const htmlEl = el;
@@ -2425,11 +2571,11 @@ function applyStyle(el, style2) {
2425
2571
  if (typeof val === "function") {
2426
2572
  const getter = val;
2427
2573
  const teardown = reactiveBinding2(() => {
2428
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
2574
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
2429
2575
  }, el);
2430
2576
  registerDisposer(el, teardown);
2431
2577
  } else {
2432
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
2578
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
2433
2579
  }
2434
2580
  }
2435
2581
  }
@@ -2536,11 +2682,26 @@ var tagFactory = (tag, ns) => {
2536
2682
  appendChildren(el, second);
2537
2683
  return el;
2538
2684
  }
2539
- if (_isDev6 && looksLikeClassList(first)) {
2540
- devWarn(
2541
- `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.`
2542
- );
2543
- }
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
+ });
2544
2705
  el.textContent = first;
2545
2706
  return el;
2546
2707
  }
@@ -2557,7 +2718,7 @@ var tagFactory = (tag, ns) => {
2557
2718
  if (pClass != null) applyClass(el, pClass);
2558
2719
  const pId = props.id;
2559
2720
  if (pId != null) {
2560
- if (_isDev6 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2721
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2561
2722
  devWarn(
2562
2723
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
2563
2724
  );
@@ -2572,7 +2733,7 @@ var tagFactory = (tag, ns) => {
2572
2733
  const handler = pOn[ev];
2573
2734
  if (typeof handler === "function") {
2574
2735
  el.addEventListener(ev, handler);
2575
- } else if (_isDev6) {
2736
+ } else if (DEV) {
2576
2737
  devWarn(
2577
2738
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
2578
2739
  );
@@ -3371,7 +3532,6 @@ var isBatching = API2.isBatching;
3371
3532
 
3372
3533
  // src/core/signals/signal.ts
3373
3534
  var _g2 = globalThis;
3374
- var _isDev7 = isDev();
3375
3535
  function signal(initial, options) {
3376
3536
  const state2 = {
3377
3537
  value: initial,
@@ -3384,7 +3544,7 @@ function signal(initial, options) {
3384
3544
  _d: false,
3385
3545
  _validate: void 0
3386
3546
  };
3387
- const debugName = _isDev7 ? options?.name : void 0;
3547
+ const debugName = DEV ? options?.name : void 0;
3388
3548
  const equalsFn = options?.equals;
3389
3549
  if (debugName) state2.__name = debugName;
3390
3550
  function get() {
@@ -3401,7 +3561,7 @@ function signal(initial, options) {
3401
3561
  if (equalsFn(prev, newValue)) return;
3402
3562
  state2.value = newValue;
3403
3563
  state2.__v++;
3404
- if (_isDev7) {
3564
+ if (DEV) {
3405
3565
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
3406
3566
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
3407
3567
  }
@@ -3409,7 +3569,7 @@ function signal(initial, options) {
3409
3569
  notifySubscribers2(state2);
3410
3570
  }
3411
3571
  };
3412
- } else if (_isDev7) {
3572
+ } else if (DEV) {
3413
3573
  set = (next) => {
3414
3574
  const prev = state2.value;
3415
3575
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -3434,7 +3594,7 @@ function signal(initial, options) {
3434
3594
  }
3435
3595
  };
3436
3596
  }
3437
- if (_isDev7) {
3597
+ if (DEV) {
3438
3598
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
3439
3599
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
3440
3600
  }
@@ -4426,11 +4586,10 @@ function setGlobalErrorHandler(handler) {
4426
4586
  }
4427
4587
 
4428
4588
  // src/core/rendering/context.ts
4429
- var _isDev8 = isDev();
4430
4589
  function context(defaultValue) {
4431
4590
  const [getValue, setValue] = signal(defaultValue);
4432
4591
  const warnIfSSR = (method) => {
4433
- if (!_isDev8 || !isSSR()) return;
4592
+ if (!DEV || !isSSR()) return;
4434
4593
  devWarn(
4435
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.`
4436
4595
  );
@@ -4458,7 +4617,7 @@ function context(defaultValue) {
4458
4617
  setValue(value);
4459
4618
  try {
4460
4619
  const result = fn();
4461
- if (_isDev8 && result !== null && typeof result?.then === "function") {
4620
+ if (DEV && result !== null && typeof result?.then === "function") {
4462
4621
  devWarn(
4463
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."
4464
4623
  );
@@ -4485,34 +4644,53 @@ function __resetIdCounter() {
4485
4644
 
4486
4645
  // src/core/rendering/directives.ts
4487
4646
  function show(condition, element) {
4647
+ const resolved = typeof element === "function" ? element() : element;
4488
4648
  const update = () => {
4489
- element.style.display = condition() ? "" : "none";
4649
+ resolved.style.display = condition() ? "" : "none";
4490
4650
  };
4491
- registerDisposer(element, track2(update));
4492
- 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 };
4493
4669
  }
4494
4670
  function when(condition, thenBranch, elseBranch) {
4495
4671
  const anchor = document.createComment("when");
4496
4672
  let currentNode = null;
4497
4673
  let lastCondition;
4498
4674
  let initialized = false;
4675
+ const attachedOnce = /* @__PURE__ */ new WeakSet();
4676
+ let currentNodeOwned = false;
4499
4677
  const update = () => {
4500
4678
  const show2 = condition();
4501
4679
  const parent = anchor.parentNode;
4502
4680
  if (!parent) return;
4503
4681
  if (initialized && show2 === lastCondition) return;
4504
4682
  lastCondition = show2;
4683
+ const focused = currentNode ? captureFocusWithin([currentNode]) : null;
4505
4684
  if (currentNode?.parentNode) {
4506
- dispose(currentNode);
4685
+ if (currentNodeOwned) dispose(currentNode);
4507
4686
  currentNode.parentNode.removeChild(currentNode);
4508
4687
  currentNode = null;
4509
4688
  }
4510
- const result = show2 ? thenBranch() : elseBranch ? elseBranch() : null;
4511
- if (result != null) {
4512
- const node = result instanceof Node ? result : document.createTextNode(String(result));
4513
- parent.insertBefore(node, anchor.nextSibling);
4514
- currentNode = node;
4515
- }
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");
4516
4694
  initialized = true;
4517
4695
  };
4518
4696
  registerDisposer(anchor, track2(update));
@@ -4528,26 +4706,25 @@ function match(value, cases, fallback) {
4528
4706
  let currentNode = null;
4529
4707
  let lastKey;
4530
4708
  let initialized = false;
4709
+ const attachedOnce = /* @__PURE__ */ new WeakSet();
4710
+ let currentNodeOwned = false;
4531
4711
  const update = () => {
4532
4712
  const key = String(value());
4533
4713
  const parent = anchor.parentNode;
4534
4714
  if (!parent) return;
4535
4715
  if (initialized && key === lastKey) return;
4536
4716
  lastKey = key;
4717
+ const focused = currentNode ? captureFocusWithin([currentNode]) : null;
4537
4718
  if (currentNode?.parentNode) {
4538
- dispose(currentNode);
4719
+ if (currentNodeOwned) dispose(currentNode);
4539
4720
  currentNode.parentNode.removeChild(currentNode);
4540
4721
  currentNode = null;
4541
4722
  }
4542
- const renderFn = cases[key] || fallback;
4543
- if (renderFn) {
4544
- const result = renderFn();
4545
- if (result != null) {
4546
- const node = result instanceof Node ? result : document.createTextNode(String(result));
4547
- parent.insertBefore(node, anchor.nextSibling);
4548
- currentNode = node;
4549
- }
4550
- }
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");
4551
4728
  initialized = true;
4552
4729
  };
4553
4730
  registerDisposer(anchor, track2(update));
@@ -4592,7 +4769,6 @@ function DynamicComponent(is) {
4592
4769
  }
4593
4770
 
4594
4771
  // src/core/rendering/each.ts
4595
- var _isDev9 = isDev();
4596
4772
  function resolveNodeChild(child) {
4597
4773
  if (typeof child === "function") {
4598
4774
  return resolveNodeChild(child());
@@ -4683,7 +4859,7 @@ function each(getArray, render, options) {
4683
4859
  workMap.clear();
4684
4860
  keyIndexMap.clear();
4685
4861
  for (let i2 = 0; i2 < newLen; i2++) {
4686
- if (_isDev9 && keyIndexMap.has(newKeys[i2])) {
4862
+ if (DEV && keyIndexMap.has(newKeys[i2])) {
4687
4863
  devWarn(
4688
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.`
4689
4865
  );
@@ -4867,7 +5043,6 @@ function resolveChild(child) {
4867
5043
  }
4868
5044
 
4869
5045
  // src/core/rendering/htm.ts
4870
- var _isDev10 = isDev();
4871
5046
  var RAW_TEXT_TAGS = /* @__PURE__ */ new Set(["script", "style"]);
4872
5047
  var VOID_ELEMENTS3 = /* @__PURE__ */ new Set([
4873
5048
  "area",
@@ -4954,13 +5129,13 @@ function parseTemplate(strings) {
4954
5129
  skipWs();
4955
5130
  const exprIdx = tryExprIdx();
4956
5131
  if (exprIdx >= 0) return { kind: "expr", idx: exprIdx };
4957
- const quote = template2[pos];
4958
- if (quote === '"' || quote === "'") {
5132
+ const quote2 = template2[pos];
5133
+ if (quote2 === '"' || quote2 === "'") {
4959
5134
  pos++;
4960
5135
  const statics = [];
4961
5136
  const exprs = [];
4962
5137
  let current = "";
4963
- while (pos < len && template2[pos] !== quote) {
5138
+ while (pos < len && template2[pos] !== quote2) {
4964
5139
  const innerIdx = tryExprIdx();
4965
5140
  if (innerIdx >= 0) {
4966
5141
  statics.push(current);
@@ -5147,7 +5322,7 @@ function executeElement(tmpl, values) {
5147
5322
  const fn = values[attr.idx];
5148
5323
  if (typeof fn === "function") {
5149
5324
  el.addEventListener(attr.name, fn);
5150
- } else if (_isDev10) {
5325
+ } else if (DEV) {
5151
5326
  devWarn(
5152
5327
  `html: on:${attr.name} handler is not a function (got ${typeof fn}). Event listener was not attached.`
5153
5328
  );
@@ -6422,7 +6597,6 @@ function schedule(strategy, el, activate, options) {
6422
6597
  }
6423
6598
 
6424
6599
  // src/platform/ssr.ts
6425
- var _isDev11 = isDev();
6426
6600
  function trustHTML(html3) {
6427
6601
  return html3;
6428
6602
  }