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/index.cjs CHANGED
@@ -346,23 +346,28 @@ function reportError(error, context2) {
346
346
  }
347
347
 
348
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
+ );
349
353
  function isDev() {
350
- return typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
351
- // The bare `__SIBU_DEV__` is a bundler define that only exists in
352
- // production builds; under the test runner it is always undefined, so
353
- // this branch is unreachable here.
354
- /* v8 ignore next 2 */
355
- typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
356
- );
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";
357
357
  }
358
- var _isDev = isDev();
359
358
  function devAssert(condition, message) {
360
- if (_isDev && !condition) {
359
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
361
360
  throw new Error(`[SibuJS] ${message}`);
362
361
  }
363
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
+ }
364
369
  function devWarn(message) {
365
- if (_isDev) {
370
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
366
371
  console.warn(`[SibuJS] ${message}`);
367
372
  }
368
373
  }
@@ -377,7 +382,6 @@ function reportDrainRunaway(label2, executed, remaining) {
377
382
  { executed, remaining }
378
383
  );
379
384
  }
380
- var _isDev2 = isDev();
381
385
  var activeBindingCount = 0;
382
386
  function registerDisposer(node, teardown) {
383
387
  let disposers = elementDisposers.get(node);
@@ -386,7 +390,7 @@ function registerDisposer(node, teardown) {
386
390
  elementDisposers.set(node, disposers);
387
391
  }
388
392
  disposers.push(teardown);
389
- if (_isDev2) activeBindingCount++;
393
+ if (DEV) activeBindingCount++;
390
394
  }
391
395
  function unregisterDisposer(node, teardown) {
392
396
  const disposers = elementDisposers.get(node);
@@ -394,7 +398,7 @@ function unregisterDisposer(node, teardown) {
394
398
  const index = disposers.indexOf(teardown);
395
399
  if (index === -1) return;
396
400
  disposers.splice(index, 1);
397
- if (_isDev2) activeBindingCount--;
401
+ if (DEV) activeBindingCount--;
398
402
  if (disposers.length === 0) elementDisposers.delete(node);
399
403
  }
400
404
  function dispose(node) {
@@ -418,13 +422,13 @@ function dispose(node) {
418
422
  if (!pending || pending.length === 0) break;
419
423
  const snapshot = pending.slice();
420
424
  elementDisposers.delete(current);
421
- if (_isDev2) activeBindingCount -= snapshot.length;
425
+ if (DEV) activeBindingCount -= snapshot.length;
422
426
  for (let i3 = 0; i3 < snapshot.length; i3++) {
423
427
  if (executed >= MAX_DRAIN_TEARDOWNS) {
424
428
  const rest = snapshot.slice(i3);
425
429
  const added = elementDisposers.get(current);
426
430
  elementDisposers.set(current, added ? rest.concat(added) : rest);
427
- if (_isDev2) activeBindingCount += rest.length;
431
+ if (DEV) activeBindingCount += rest.length;
428
432
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
429
433
  runaway = true;
430
434
  break;
@@ -452,7 +456,7 @@ function replaceChildrenSafely(parent, ...next) {
452
456
  parent.replaceChildren(...next);
453
457
  }
454
458
  function checkLeaks(warnThreshold = 0) {
455
- if (!_isDev2) return 0;
459
+ if (!DEV) return 0;
456
460
  if (warnThreshold > 0 && activeBindingCount > warnThreshold) {
457
461
  devWarn(
458
462
  `checkLeaks: ${activeBindingCount} active DOM bindings detected. Expected \u2264${warnThreshold}. This may indicate a component was removed from the DOM without calling dispose().`
@@ -554,20 +558,75 @@ function decodeCssEscapes(value) {
554
558
  }
555
559
  return out;
556
560
  }
557
- function sanitizeCSSValue(value) {
558
- 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;
559
612
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
560
613
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
561
- 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")) {
562
- return "";
614
+ for (let i2 = 0; i2 < BLOCKED_CSS_NEEDLES.length; i2++) {
615
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i2])) return i2;
563
616
  }
564
- 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 "";
565
624
  }
566
- function sanitizeStyleAttribute(cssText) {
625
+ function sanitizeStyleAttribute(cssText, context2) {
567
626
  const input2 = String(cssText);
568
627
  if (input2.trim() === "") return "";
569
628
  if (typeof document === "undefined") {
570
- return sanitizeCSSValue(input2) === "" ? "" : input2;
629
+ return sanitizeCSSValue(input2, context2) === "" ? "" : input2;
571
630
  }
572
631
  const probe = document.createElement("div");
573
632
  try {
@@ -579,8 +638,12 @@ function sanitizeStyleAttribute(cssText) {
579
638
  for (let i2 = 0; i2 < probe.style.length; i2++) {
580
639
  const property = probe.style[i2];
581
640
  const value = probe.style.getPropertyValue(property);
582
- if (sanitizeCSSValue(value) === "") continue;
583
- 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
+ }
584
647
  const priority = probe.style.getPropertyPriority(property);
585
648
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
586
649
  }
@@ -608,16 +671,15 @@ var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
608
671
  function isHtmlContentAttribute(name) {
609
672
  return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
610
673
  }
611
- function sanitizeAttributeString(attr, value) {
674
+ function sanitizeAttributeString(attr, value, context2) {
612
675
  const lower = attr.toLowerCase();
613
676
  if (lower === "srcset") return sanitizeSrcset(value);
614
677
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
615
- if (lower === "style") return sanitizeStyleAttribute(value);
678
+ if (lower === "style") return sanitizeStyleAttribute(value, context2);
616
679
  return value;
617
680
  }
618
681
 
619
682
  // src/utils/setSafeAttribute.ts
620
- var _isDev3 = isDev();
621
683
  var HTML_NS = "http://www.w3.org/1999/xhtml";
622
684
  var SVG_NS = "http://www.w3.org/2000/svg";
623
685
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -640,7 +702,7 @@ function setSafeAttribute(el, name, value, options = {}) {
640
702
  const ns = namespaceFor(el, name);
641
703
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
642
704
  if (isEventHandlerAttr(name)) {
643
- if (_isDev3) {
705
+ if (DEV) {
644
706
  devWarn(
645
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.`
646
708
  );
@@ -655,7 +717,7 @@ function setSafeAttribute(el, name, value, options = {}) {
655
717
  return true;
656
718
  }
657
719
  if (isHtmlContentAttribute(name)) {
658
- if (_isDev3) {
720
+ if (DEV) {
659
721
  devWarn(
660
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.`
661
723
  );
@@ -684,7 +746,7 @@ function setSafeAttribute(el, name, value, options = {}) {
684
746
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
685
747
  return true;
686
748
  }
687
- const safe = sanitizeAttributeString(name, str);
749
+ const safe = sanitizeAttributeString(name, str, { element: el });
688
750
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
689
751
  if (current === safe) return true;
690
752
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -1148,17 +1210,16 @@ function forEachSubscriber(signal2, visit) {
1148
1210
  }
1149
1211
 
1150
1212
  // src/reactivity/track.ts
1151
- var _isDev4 = isDev();
1152
- var _runtimeVersion = true ? "4.1.0" : "dev";
1213
+ var _runtimeVersion = true ? "4.2.0" : "dev";
1153
1214
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
1154
1215
  function resolveReactiveApi() {
1155
1216
  const g2 = globalThis;
1156
1217
  const existing = g2[REGISTRY_KEY];
1157
1218
  if (existing) {
1158
- if (_isDev4 && !existing.__dupWarned) {
1219
+ if (DEV && !existing.__dupWarned) {
1159
1220
  existing.__dupWarned = true;
1160
1221
  devWarn(
1161
- `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.`
1162
1223
  );
1163
1224
  }
1164
1225
  return existing;
@@ -1257,8 +1318,101 @@ function bindDynamic(el, nameGetter, valueGetter) {
1257
1318
  };
1258
1319
  }
1259
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
+
1260
1415
  // src/reactivity/bindChildNode.ts
1261
- var _isDev5 = isDev();
1262
1416
  function bindChildNode(placeholder, getter) {
1263
1417
  let lastNodes = [];
1264
1418
  function commit() {
@@ -1269,6 +1423,7 @@ function bindChildNode(placeholder, getter) {
1269
1423
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
1270
1424
  return;
1271
1425
  }
1426
+ const focused = captureFocusWithin(lastNodes);
1272
1427
  if (result == null || typeof result === "boolean") {
1273
1428
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
1274
1429
  const node = lastNodes[i2];
@@ -1276,6 +1431,7 @@ function bindChildNode(placeholder, getter) {
1276
1431
  if (node.parentNode) node.parentNode.removeChild(node);
1277
1432
  }
1278
1433
  lastNodes.length = 0;
1434
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
1279
1435
  return;
1280
1436
  }
1281
1437
  const parent = placeholder.parentNode;
@@ -1292,8 +1448,7 @@ function bindChildNode(placeholder, getter) {
1292
1448
  if (item == null || typeof item === "boolean") continue;
1293
1449
  const node = item instanceof Node ? item : document.createTextNode(String(item));
1294
1450
  if (seen.has(node)) {
1295
- if (_isDev5)
1296
- 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.");
1297
1452
  continue;
1298
1453
  }
1299
1454
  seen.add(node);
@@ -1326,13 +1481,13 @@ function bindChildNode(placeholder, getter) {
1326
1481
  prev = node;
1327
1482
  }
1328
1483
  lastNodes = newNodes;
1484
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
1329
1485
  }
1330
1486
  return reactiveBinding2(commit, placeholder);
1331
1487
  }
1332
1488
 
1333
1489
  // src/core/rendering/tagFactory.ts
1334
1490
  var SVG_NS2 = "http://www.w3.org/2000/svg";
1335
- var _isDev6 = isDev();
1336
1491
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
1337
1492
  function isBlockedTag(tag) {
1338
1493
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -1350,18 +1505,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
1350
1505
  "self",
1351
1506
  "frames"
1352
1507
  ]);
1353
- function looksLikeClassList(s2) {
1354
- const t = s2.trim();
1355
- if (!t) return false;
1356
- const tokens = t.split(/\s+/);
1357
- let sawClassish = false;
1358
- for (let i2 = 0; i2 < tokens.length; i2++) {
1359
- const tok = tokens[i2];
1360
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
1361
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
1362
- }
1363
- return sawClassish;
1364
- }
1508
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
1509
+ var MAX_WARNED_LONE_STRINGS = 100;
1510
+ var loneStringCapAnnounced = false;
1365
1511
  var kebabCache = /* @__PURE__ */ new Map();
1366
1512
  function toKebab(prop) {
1367
1513
  let cached = kebabCache.get(prop);
@@ -1373,13 +1519,13 @@ function toKebab(prop) {
1373
1519
  function applyStyle(el, style2) {
1374
1520
  if (typeof style2 === "function") {
1375
1521
  const teardown = reactiveBinding2(() => {
1376
- el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
1522
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2()), { element: el }));
1377
1523
  }, el);
1378
1524
  registerDisposer(el, teardown);
1379
1525
  return;
1380
1526
  }
1381
1527
  if (typeof style2 === "string") {
1382
- el.setAttribute("style", sanitizeStyleAttribute(style2));
1528
+ el.setAttribute("style", sanitizeStyleAttribute(style2, { element: el }));
1383
1529
  return;
1384
1530
  }
1385
1531
  const htmlEl = el;
@@ -1389,11 +1535,11 @@ function applyStyle(el, style2) {
1389
1535
  if (typeof val === "function") {
1390
1536
  const getter = val;
1391
1537
  const teardown = reactiveBinding2(() => {
1392
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
1538
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
1393
1539
  }, el);
1394
1540
  registerDisposer(el, teardown);
1395
1541
  } else {
1396
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
1542
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
1397
1543
  }
1398
1544
  }
1399
1545
  }
@@ -1500,11 +1646,26 @@ var tagFactory = (tag, ns) => {
1500
1646
  appendChildren(el, second);
1501
1647
  return el;
1502
1648
  }
1503
- if (_isDev6 && looksLikeClassList(first)) {
1504
- devWarn(
1505
- `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.`
1506
- );
1507
- }
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
+ });
1508
1669
  el.textContent = first;
1509
1670
  return el;
1510
1671
  }
@@ -1521,7 +1682,7 @@ var tagFactory = (tag, ns) => {
1521
1682
  if (pClass != null) applyClass(el, pClass);
1522
1683
  const pId = props.id;
1523
1684
  if (pId != null) {
1524
- if (_isDev6 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
1685
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
1525
1686
  devWarn(
1526
1687
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
1527
1688
  );
@@ -1536,7 +1697,7 @@ var tagFactory = (tag, ns) => {
1536
1697
  const handler = pOn[ev];
1537
1698
  if (typeof handler === "function") {
1538
1699
  el.addEventListener(ev, handler);
1539
- } else if (_isDev6) {
1700
+ } else if (DEV) {
1540
1701
  devWarn(
1541
1702
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
1542
1703
  );
@@ -2335,7 +2496,6 @@ var isBatching = API2.isBatching;
2335
2496
 
2336
2497
  // src/core/signals/signal.ts
2337
2498
  var _g2 = globalThis;
2338
- var _isDev7 = isDev();
2339
2499
  function signal(initial, options) {
2340
2500
  const state2 = {
2341
2501
  value: initial,
@@ -2348,7 +2508,7 @@ function signal(initial, options) {
2348
2508
  _d: false,
2349
2509
  _validate: void 0
2350
2510
  };
2351
- const debugName = _isDev7 ? options?.name : void 0;
2511
+ const debugName = DEV ? options?.name : void 0;
2352
2512
  const equalsFn = options?.equals;
2353
2513
  if (debugName) state2.__name = debugName;
2354
2514
  function get() {
@@ -2365,7 +2525,7 @@ function signal(initial, options) {
2365
2525
  if (equalsFn(prev, newValue)) return;
2366
2526
  state2.value = newValue;
2367
2527
  state2.__v++;
2368
- if (_isDev7) {
2528
+ if (DEV) {
2369
2529
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
2370
2530
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
2371
2531
  }
@@ -2373,7 +2533,7 @@ function signal(initial, options) {
2373
2533
  notifySubscribers2(state2);
2374
2534
  }
2375
2535
  };
2376
- } else if (_isDev7) {
2536
+ } else if (DEV) {
2377
2537
  set = (next) => {
2378
2538
  const prev = state2.value;
2379
2539
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -2398,7 +2558,7 @@ function signal(initial, options) {
2398
2558
  }
2399
2559
  };
2400
2560
  }
2401
- if (_isDev7) {
2561
+ if (DEV) {
2402
2562
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
2403
2563
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
2404
2564
  }
@@ -3390,11 +3550,10 @@ function setGlobalErrorHandler(handler) {
3390
3550
  }
3391
3551
 
3392
3552
  // src/core/rendering/context.ts
3393
- var _isDev8 = isDev();
3394
3553
  function context(defaultValue) {
3395
3554
  const [getValue, setValue] = signal(defaultValue);
3396
3555
  const warnIfSSR = (method) => {
3397
- if (!_isDev8 || !isSSR()) return;
3556
+ if (!DEV || !isSSR()) return;
3398
3557
  devWarn(
3399
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.`
3400
3559
  );
@@ -3422,7 +3581,7 @@ function context(defaultValue) {
3422
3581
  setValue(value);
3423
3582
  try {
3424
3583
  const result = fn();
3425
- if (_isDev8 && result !== null && typeof result?.then === "function") {
3584
+ if (DEV && result !== null && typeof result?.then === "function") {
3426
3585
  devWarn(
3427
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."
3428
3587
  );
@@ -3449,34 +3608,53 @@ function __resetIdCounter() {
3449
3608
 
3450
3609
  // src/core/rendering/directives.ts
3451
3610
  function show(condition, element) {
3611
+ const resolved = typeof element === "function" ? element() : element;
3452
3612
  const update = () => {
3453
- element.style.display = condition() ? "" : "none";
3613
+ resolved.style.display = condition() ? "" : "none";
3454
3614
  };
3455
- registerDisposer(element, track2(update));
3456
- 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 };
3457
3633
  }
3458
3634
  function when(condition, thenBranch, elseBranch) {
3459
3635
  const anchor = document.createComment("when");
3460
3636
  let currentNode = null;
3461
3637
  let lastCondition;
3462
3638
  let initialized = false;
3639
+ const attachedOnce = /* @__PURE__ */ new WeakSet();
3640
+ let currentNodeOwned = false;
3463
3641
  const update = () => {
3464
3642
  const show2 = condition();
3465
3643
  const parent = anchor.parentNode;
3466
3644
  if (!parent) return;
3467
3645
  if (initialized && show2 === lastCondition) return;
3468
3646
  lastCondition = show2;
3647
+ const focused = currentNode ? captureFocusWithin([currentNode]) : null;
3469
3648
  if (currentNode?.parentNode) {
3470
- dispose(currentNode);
3649
+ if (currentNodeOwned) dispose(currentNode);
3471
3650
  currentNode.parentNode.removeChild(currentNode);
3472
3651
  currentNode = null;
3473
3652
  }
3474
- const result = show2 ? thenBranch() : elseBranch ? elseBranch() : null;
3475
- if (result != null) {
3476
- const node = result instanceof Node ? result : document.createTextNode(String(result));
3477
- parent.insertBefore(node, anchor.nextSibling);
3478
- currentNode = node;
3479
- }
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");
3480
3658
  initialized = true;
3481
3659
  };
3482
3660
  registerDisposer(anchor, track2(update));
@@ -3492,26 +3670,25 @@ function match(value, cases, fallback) {
3492
3670
  let currentNode = null;
3493
3671
  let lastKey;
3494
3672
  let initialized = false;
3673
+ const attachedOnce = /* @__PURE__ */ new WeakSet();
3674
+ let currentNodeOwned = false;
3495
3675
  const update = () => {
3496
3676
  const key = String(value());
3497
3677
  const parent = anchor.parentNode;
3498
3678
  if (!parent) return;
3499
3679
  if (initialized && key === lastKey) return;
3500
3680
  lastKey = key;
3681
+ const focused = currentNode ? captureFocusWithin([currentNode]) : null;
3501
3682
  if (currentNode?.parentNode) {
3502
- dispose(currentNode);
3683
+ if (currentNodeOwned) dispose(currentNode);
3503
3684
  currentNode.parentNode.removeChild(currentNode);
3504
3685
  currentNode = null;
3505
3686
  }
3506
- const renderFn = cases[key] || fallback;
3507
- if (renderFn) {
3508
- const result = renderFn();
3509
- if (result != null) {
3510
- const node = result instanceof Node ? result : document.createTextNode(String(result));
3511
- parent.insertBefore(node, anchor.nextSibling);
3512
- currentNode = node;
3513
- }
3514
- }
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");
3515
3692
  initialized = true;
3516
3693
  };
3517
3694
  registerDisposer(anchor, track2(update));
@@ -3556,7 +3733,6 @@ function DynamicComponent(is) {
3556
3733
  }
3557
3734
 
3558
3735
  // src/core/rendering/each.ts
3559
- var _isDev9 = isDev();
3560
3736
  function resolveNodeChild(child) {
3561
3737
  if (typeof child === "function") {
3562
3738
  return resolveNodeChild(child());
@@ -3647,7 +3823,7 @@ function each(getArray, render, options) {
3647
3823
  workMap.clear();
3648
3824
  keyIndexMap.clear();
3649
3825
  for (let i2 = 0; i2 < newLen; i2++) {
3650
- if (_isDev9 && keyIndexMap.has(newKeys[i2])) {
3826
+ if (DEV && keyIndexMap.has(newKeys[i2])) {
3651
3827
  devWarn(
3652
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.`
3653
3829
  );
@@ -3831,7 +4007,6 @@ function resolveChild(child) {
3831
4007
  }
3832
4008
 
3833
4009
  // src/core/rendering/htm.ts
3834
- var _isDev10 = isDev();
3835
4010
  var RAW_TEXT_TAGS = /* @__PURE__ */ new Set(["script", "style"]);
3836
4011
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
3837
4012
  "area",
@@ -3918,13 +4093,13 @@ function parseTemplate(strings) {
3918
4093
  skipWs();
3919
4094
  const exprIdx = tryExprIdx();
3920
4095
  if (exprIdx >= 0) return { kind: "expr", idx: exprIdx };
3921
- const quote = template2[pos];
3922
- if (quote === '"' || quote === "'") {
4096
+ const quote2 = template2[pos];
4097
+ if (quote2 === '"' || quote2 === "'") {
3923
4098
  pos++;
3924
4099
  const statics = [];
3925
4100
  const exprs = [];
3926
4101
  let current = "";
3927
- while (pos < len && template2[pos] !== quote) {
4102
+ while (pos < len && template2[pos] !== quote2) {
3928
4103
  const innerIdx = tryExprIdx();
3929
4104
  if (innerIdx >= 0) {
3930
4105
  statics.push(current);
@@ -4111,7 +4286,7 @@ function executeElement(tmpl, values) {
4111
4286
  const fn = values[attr.idx];
4112
4287
  if (typeof fn === "function") {
4113
4288
  el.addEventListener(attr.name, fn);
4114
- } else if (_isDev10) {
4289
+ } else if (DEV) {
4115
4290
  devWarn(
4116
4291
  `html: on:${attr.name} handler is not a function (got ${typeof fn}). Event listener was not attached.`
4117
4292
  );
@@ -5386,7 +5561,6 @@ function schedule(strategy, el, activate, options) {
5386
5561
  }
5387
5562
 
5388
5563
  // src/platform/ssr.ts
5389
- var _isDev11 = isDev();
5390
5564
  function trustHTML(html3) {
5391
5565
  return html3;
5392
5566
  }