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
@@ -32,23 +32,23 @@ __export(ecosystem_exports, {
32
32
  module.exports = __toCommonJS(ecosystem_exports);
33
33
 
34
34
  // src/core/dev.ts
35
- function isDev() {
36
- return typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
37
- // The bare `__SIBU_DEV__` is a bundler define that only exists in
38
- // production builds; under the test runner it is always undefined, so
39
- // this branch is unreachable here.
40
- /* v8 ignore next 2 */
41
- typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
42
- );
43
- }
44
- var _isDev = isDev();
35
+ var DEV = typeof __SIBU_DEV__ !== "undefined" ? !!__SIBU_DEV__ : typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
36
+ // safe default: off in browser, on in test/dev Node
37
+ typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
38
+ );
45
39
  function devAssert(condition, message) {
46
- if (_isDev && !condition) {
40
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
47
41
  throw new Error(`[SibuJS] ${message}`);
48
42
  }
49
43
  }
44
+ function devWarnLazy(build) {
45
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
46
+ const message = build();
47
+ if (message) console.warn(`[SibuJS] ${message}`);
48
+ }
49
+ }
50
50
  function devWarn(message) {
51
- if (_isDev) {
51
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
52
52
  console.warn(`[SibuJS] ${message}`);
53
53
  }
54
54
  }
@@ -359,7 +359,14 @@ function reactiveBinding(commit, ownerNode) {
359
359
  subscriber._disposed = false;
360
360
  subscriber._errorPhase = "binding";
361
361
  subscriber._errorNode = ownerNode;
362
- run();
362
+ try {
363
+ run();
364
+ } catch (err) {
365
+ subscriber._disposed = true;
366
+ subscriber._errorNode = void 0;
367
+ cleanup(subscriber);
368
+ throw err;
369
+ }
363
370
  return subscriber._dispose ?? (subscriber._dispose = () => {
364
371
  subscriber._disposed = true;
365
372
  subscriber._errorNode = void 0;
@@ -590,17 +597,16 @@ function forEachSubscriber(signal2, visit) {
590
597
  }
591
598
 
592
599
  // src/reactivity/track.ts
593
- var _isDev2 = isDev();
594
- var _runtimeVersion = true ? "4.0.1" : "dev";
600
+ var _runtimeVersion = true ? "4.2.0" : "dev";
595
601
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
596
602
  function resolveReactiveApi() {
597
603
  const g = globalThis;
598
604
  const existing = g[REGISTRY_KEY];
599
605
  if (existing) {
600
- if (_isDev2 && !existing.__dupWarned) {
606
+ if (DEV && !existing.__dupWarned) {
601
607
  existing.__dupWarned = true;
602
608
  devWarn(
603
- `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']).`
609
+ `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.`
604
610
  );
605
611
  }
606
612
  return existing;
@@ -862,7 +868,6 @@ var isBatching = API2.isBatching;
862
868
 
863
869
  // src/core/signals/signal.ts
864
870
  var _g2 = globalThis;
865
- var _isDev3 = isDev();
866
871
  function signal(initial, options) {
867
872
  const state2 = {
868
873
  value: initial,
@@ -875,7 +880,7 @@ function signal(initial, options) {
875
880
  _d: false,
876
881
  _validate: void 0
877
882
  };
878
- const debugName = _isDev3 ? options?.name : void 0;
883
+ const debugName = DEV ? options?.name : void 0;
879
884
  const equalsFn = options?.equals;
880
885
  if (debugName) state2.__name = debugName;
881
886
  function get() {
@@ -892,7 +897,7 @@ function signal(initial, options) {
892
897
  if (equalsFn(prev, newValue)) return;
893
898
  state2.value = newValue;
894
899
  state2.__v++;
895
- if (_isDev3) {
900
+ if (DEV) {
896
901
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
897
902
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
898
903
  }
@@ -900,7 +905,7 @@ function signal(initial, options) {
900
905
  notifySubscribers2(state2);
901
906
  }
902
907
  };
903
- } else if (_isDev3) {
908
+ } else if (DEV) {
904
909
  set = (next) => {
905
910
  const prev = state2.value;
906
911
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -925,7 +930,7 @@ function signal(initial, options) {
925
930
  }
926
931
  };
927
932
  }
928
- if (_isDev3) {
933
+ if (DEV) {
929
934
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
930
935
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
931
936
  }
@@ -1286,20 +1291,75 @@ function decodeCssEscapes(value) {
1286
1291
  }
1287
1292
  return out;
1288
1293
  }
1289
- function sanitizeCSSValue(value) {
1290
- if (!CSS_DANGER_GATE.test(value)) return value;
1294
+ var BLOCKED_CSS_NEEDLES = [
1295
+ "url(",
1296
+ "expression(",
1297
+ "javascript:",
1298
+ "vbscript:",
1299
+ "-moz-binding",
1300
+ "behavior:",
1301
+ "@import",
1302
+ "image-set(",
1303
+ "filter:progid"
1304
+ ];
1305
+ function describeElement(el) {
1306
+ if (!el) return "an element";
1307
+ let out = el.tagName.toLowerCase();
1308
+ const id = el.getAttribute("id");
1309
+ if (id) out += `#${id}`;
1310
+ const cls = el.getAttribute("class");
1311
+ if (cls) out += `.${cls.trim().split(/\s+/).slice(0, 3).join(".")}`;
1312
+ return `<${out}>`;
1313
+ }
1314
+ var warnedDeclarations = /* @__PURE__ */ new Set();
1315
+ var MAX_WARNED_DECLARATIONS = 100;
1316
+ var declarationCapAnnounced = false;
1317
+ function warnDroppedDeclaration(value, reason, ctx) {
1318
+ devWarnLazy(() => {
1319
+ const reasons = [
1320
+ "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",
1321
+ "expression() executes arbitrary script in legacy engines",
1322
+ "the javascript: scheme executes script",
1323
+ "the vbscript: scheme executes script",
1324
+ "-moz-binding loads and runs remote XBL",
1325
+ "behavior: loads and runs a remote HTC script",
1326
+ "@import pulls in a remote stylesheet",
1327
+ "image-set() fetches remote images, the same exfiltration channel as url()",
1328
+ "filter:progid activates legacy scriptable filters"
1329
+ ];
1330
+ const declaration = ctx?.property ? `${ctx.property}: ${value}` : value;
1331
+ const where = describeElement(ctx?.element);
1332
+ const key = `${where}|${declaration}`;
1333
+ if (warnedDeclarations.has(key)) return "";
1334
+ if (warnedDeclarations.size >= MAX_WARNED_DECLARATIONS) {
1335
+ if (declarationCapAnnounced) return "";
1336
+ declarationCapAnnounced = true;
1337
+ 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.`;
1338
+ }
1339
+ warnedDeclarations.add(key);
1340
+ 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.`;
1341
+ });
1342
+ }
1343
+ function blockedCssReason(value) {
1344
+ if (!CSS_DANGER_GATE.test(value)) return -1;
1291
1345
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
1292
1346
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
1293
- 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")) {
1294
- return "";
1347
+ for (let i = 0; i < BLOCKED_CSS_NEEDLES.length; i++) {
1348
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i])) return i;
1295
1349
  }
1296
- return value;
1350
+ return -1;
1351
+ }
1352
+ function sanitizeCSSValue(value, context) {
1353
+ const reason = blockedCssReason(value);
1354
+ if (reason === -1) return value;
1355
+ warnDroppedDeclaration(value, reason, context);
1356
+ return "";
1297
1357
  }
1298
- function sanitizeStyleAttribute(cssText) {
1358
+ function sanitizeStyleAttribute(cssText, context) {
1299
1359
  const input = String(cssText);
1300
1360
  if (input.trim() === "") return "";
1301
1361
  if (typeof document === "undefined") {
1302
- return sanitizeCSSValue(input) === "" ? "" : input;
1362
+ return sanitizeCSSValue(input, context) === "" ? "" : input;
1303
1363
  }
1304
1364
  const probe = document.createElement("div");
1305
1365
  try {
@@ -1311,8 +1371,12 @@ function sanitizeStyleAttribute(cssText) {
1311
1371
  for (let i = 0; i < probe.style.length; i++) {
1312
1372
  const property = probe.style[i];
1313
1373
  const value = probe.style.getPropertyValue(property);
1314
- if (sanitizeCSSValue(value) === "") continue;
1315
- if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
1374
+ let reason = blockedCssReason(value);
1375
+ if (reason === -1) reason = blockedCssReason(`${property}:${value}`);
1376
+ if (reason !== -1) {
1377
+ warnDroppedDeclaration(value, reason, { property, element: context?.element });
1378
+ continue;
1379
+ }
1316
1380
  const priority = probe.style.getPropertyPriority(property);
1317
1381
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
1318
1382
  }
@@ -1340,16 +1404,15 @@ var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
1340
1404
  function isHtmlContentAttribute(name) {
1341
1405
  return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
1342
1406
  }
1343
- function sanitizeAttributeString(attr, value) {
1407
+ function sanitizeAttributeString(attr, value, context) {
1344
1408
  const lower = attr.toLowerCase();
1345
1409
  if (lower === "srcset") return sanitizeSrcset(value);
1346
1410
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
1347
- if (lower === "style") return sanitizeStyleAttribute(value);
1411
+ if (lower === "style") return sanitizeStyleAttribute(value, context);
1348
1412
  return value;
1349
1413
  }
1350
1414
 
1351
1415
  // src/utils/setSafeAttribute.ts
1352
- var _isDev4 = isDev();
1353
1416
  var HTML_NS = "http://www.w3.org/1999/xhtml";
1354
1417
  var SVG_NS = "http://www.w3.org/2000/svg";
1355
1418
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -1372,7 +1435,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1372
1435
  const ns = namespaceFor(el, name);
1373
1436
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
1374
1437
  if (isEventHandlerAttr(name)) {
1375
- if (_isDev4) {
1438
+ if (DEV) {
1376
1439
  devWarn(
1377
1440
  `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
1378
1441
  );
@@ -1387,7 +1450,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1387
1450
  return true;
1388
1451
  }
1389
1452
  if (isHtmlContentAttribute(name)) {
1390
- if (_isDev4) {
1453
+ if (DEV) {
1391
1454
  devWarn(
1392
1455
  `${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.`
1393
1456
  );
@@ -1416,7 +1479,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1416
1479
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
1417
1480
  return true;
1418
1481
  }
1419
- const safe = sanitizeAttributeString(name, str);
1482
+ const safe = sanitizeAttributeString(name, str, { element: el });
1420
1483
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
1421
1484
  if (current === safe) return true;
1422
1485
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -1454,7 +1517,6 @@ function reportDrainRunaway(label, executed, remaining) {
1454
1517
  { executed, remaining }
1455
1518
  );
1456
1519
  }
1457
- var _isDev5 = isDev();
1458
1520
  var activeBindingCount = 0;
1459
1521
  function registerDisposer(node, teardown) {
1460
1522
  let disposers = elementDisposers.get(node);
@@ -1463,7 +1525,7 @@ function registerDisposer(node, teardown) {
1463
1525
  elementDisposers.set(node, disposers);
1464
1526
  }
1465
1527
  disposers.push(teardown);
1466
- if (_isDev5) activeBindingCount++;
1528
+ if (DEV) activeBindingCount++;
1467
1529
  }
1468
1530
  function dispose(node) {
1469
1531
  const stack = [node];
@@ -1486,13 +1548,13 @@ function dispose(node) {
1486
1548
  if (!pending || pending.length === 0) break;
1487
1549
  const snapshot = pending.slice();
1488
1550
  elementDisposers.delete(current);
1489
- if (_isDev5) activeBindingCount -= snapshot.length;
1551
+ if (DEV) activeBindingCount -= snapshot.length;
1490
1552
  for (let i2 = 0; i2 < snapshot.length; i2++) {
1491
1553
  if (executed >= MAX_DRAIN_TEARDOWNS) {
1492
1554
  const rest = snapshot.slice(i2);
1493
1555
  const added = elementDisposers.get(current);
1494
1556
  elementDisposers.set(current, added ? rest.concat(added) : rest);
1495
- if (_isDev5) activeBindingCount += rest.length;
1557
+ if (DEV) activeBindingCount += rest.length;
1496
1558
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
1497
1559
  runaway = true;
1498
1560
  break;
@@ -1509,8 +1571,101 @@ function dispose(node) {
1509
1571
  }
1510
1572
  }
1511
1573
 
1574
+ // src/core/rendering/focusPreservation.ts
1575
+ var IDENTITY_ATTRS = ["data-focus-key", "id", "name"];
1576
+ var SELECTABLE_TYPES = /* @__PURE__ */ new Set(["text", "search", "url", "tel", "password", ""]);
1577
+ function selectionOf(el) {
1578
+ const tag = el.tagName;
1579
+ const selectable = tag === "TEXTAREA" || tag === "INPUT" && SELECTABLE_TYPES.has(el.type.toLowerCase());
1580
+ if (!selectable) return { start: null, end: null, direction: null };
1581
+ const field = el;
1582
+ return { start: field.selectionStart, end: field.selectionEnd, direction: field.selectionDirection };
1583
+ }
1584
+ function captureFocusWithin(nodes) {
1585
+ if (nodes.length === 0 || typeof document === "undefined") return null;
1586
+ const active = document.activeElement;
1587
+ if (!active || active === document.body) return null;
1588
+ let inside = false;
1589
+ for (let i = 0; i < nodes.length; i++) {
1590
+ const n = nodes[i];
1591
+ if (n === active || n.nodeType === 1 && n.contains(active)) {
1592
+ inside = true;
1593
+ break;
1594
+ }
1595
+ }
1596
+ if (!inside) return null;
1597
+ let attr = null;
1598
+ let value = null;
1599
+ for (let i = 0; i < IDENTITY_ATTRS.length; i++) {
1600
+ const candidate = active.getAttribute(IDENTITY_ATTRS[i]);
1601
+ if (candidate) {
1602
+ attr = IDENTITY_ATTRS[i];
1603
+ value = candidate;
1604
+ break;
1605
+ }
1606
+ }
1607
+ return { el: active, attr, value, tag: active.tagName, ...selectionOf(active) };
1608
+ }
1609
+ function quote(value) {
1610
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1611
+ }
1612
+ function findUniqueByIdentity(nodes, selector) {
1613
+ let el = null;
1614
+ let count = 0;
1615
+ for (let i = 0; i < nodes.length; i++) {
1616
+ const n = nodes[i];
1617
+ if (n.nodeType !== 1) continue;
1618
+ const root = n;
1619
+ if (root.matches(selector)) {
1620
+ count++;
1621
+ el ?? (el = root);
1622
+ }
1623
+ const inner = root.querySelectorAll(selector);
1624
+ for (let j = 0; j < inner.length; j++) {
1625
+ count++;
1626
+ el ?? (el = inner[j]);
1627
+ }
1628
+ if (count > 1) return { el: null, count };
1629
+ }
1630
+ return count === 1 ? { el, count } : { el: null, count };
1631
+ }
1632
+ function refocus(el, snapshot) {
1633
+ el.focus();
1634
+ if (snapshot.start !== null && typeof el.setSelectionRange === "function") {
1635
+ const field = el;
1636
+ const max = field.value.length;
1637
+ const start = Math.min(snapshot.start, max);
1638
+ const end = Math.min(snapshot.end ?? start, max);
1639
+ field.setSelectionRange(start, end, snapshot.direction ?? "none");
1640
+ }
1641
+ }
1642
+ function restoreFocusWithin(snapshot, nodes, where) {
1643
+ if (!snapshot) return;
1644
+ if (typeof document === "undefined") return;
1645
+ const active = document.activeElement;
1646
+ if (active === snapshot.el) return;
1647
+ if (active && active !== document.body && active !== document.documentElement) return;
1648
+ if (snapshot.el.isConnected) {
1649
+ refocus(snapshot.el, snapshot);
1650
+ return;
1651
+ }
1652
+ let ambiguous = false;
1653
+ if (snapshot.attr && snapshot.value) {
1654
+ const { el: replacement, count } = findUniqueByIdentity(nodes, `[${snapshot.attr}=${quote(snapshot.value)}]`);
1655
+ if (replacement && replacement.tagName === snapshot.tag) {
1656
+ refocus(replacement, snapshot);
1657
+ return;
1658
+ }
1659
+ ambiguous = count > 1;
1660
+ }
1661
+ devWarnLazy(() => {
1662
+ const id = snapshot.attr ? ` (${snapshot.attr}="${snapshot.value}")` : "";
1663
+ 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";
1664
+ 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.`;
1665
+ });
1666
+ }
1667
+
1512
1668
  // src/reactivity/bindChildNode.ts
1513
- var _isDev6 = isDev();
1514
1669
  function bindChildNode(placeholder, getter) {
1515
1670
  let lastNodes = [];
1516
1671
  function commit() {
@@ -1521,6 +1676,7 @@ function bindChildNode(placeholder, getter) {
1521
1676
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
1522
1677
  return;
1523
1678
  }
1679
+ const focused = captureFocusWithin(lastNodes);
1524
1680
  if (result == null || typeof result === "boolean") {
1525
1681
  for (let i = 0; i < lastNodes.length; i++) {
1526
1682
  const node = lastNodes[i];
@@ -1528,6 +1684,7 @@ function bindChildNode(placeholder, getter) {
1528
1684
  if (node.parentNode) node.parentNode.removeChild(node);
1529
1685
  }
1530
1686
  lastNodes.length = 0;
1687
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
1531
1688
  return;
1532
1689
  }
1533
1690
  const parent = placeholder.parentNode;
@@ -1544,8 +1701,7 @@ function bindChildNode(placeholder, getter) {
1544
1701
  if (item == null || typeof item === "boolean") continue;
1545
1702
  const node = item instanceof Node ? item : document.createTextNode(String(item));
1546
1703
  if (seen.has(node)) {
1547
- if (_isDev6)
1548
- devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1704
+ if (DEV) devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1549
1705
  continue;
1550
1706
  }
1551
1707
  seen.add(node);
@@ -1578,12 +1734,12 @@ function bindChildNode(placeholder, getter) {
1578
1734
  prev = node;
1579
1735
  }
1580
1736
  lastNodes = newNodes;
1737
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
1581
1738
  }
1582
1739
  return reactiveBinding2(commit, placeholder);
1583
1740
  }
1584
1741
 
1585
1742
  // src/core/rendering/tagFactory.ts
1586
- var _isDev7 = isDev();
1587
1743
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
1588
1744
  function isBlockedTag(tag) {
1589
1745
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -1601,18 +1757,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
1601
1757
  "self",
1602
1758
  "frames"
1603
1759
  ]);
1604
- function looksLikeClassList(s) {
1605
- const t = s.trim();
1606
- if (!t) return false;
1607
- const tokens = t.split(/\s+/);
1608
- let sawClassish = false;
1609
- for (let i = 0; i < tokens.length; i++) {
1610
- const tok = tokens[i];
1611
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
1612
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
1613
- }
1614
- return sawClassish;
1615
- }
1760
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
1761
+ var MAX_WARNED_LONE_STRINGS = 100;
1762
+ var loneStringCapAnnounced = false;
1616
1763
  var kebabCache = /* @__PURE__ */ new Map();
1617
1764
  function toKebab(prop) {
1618
1765
  let cached = kebabCache.get(prop);
@@ -1624,13 +1771,13 @@ function toKebab(prop) {
1624
1771
  function applyStyle(el, style) {
1625
1772
  if (typeof style === "function") {
1626
1773
  const teardown = reactiveBinding2(() => {
1627
- el.setAttribute("style", sanitizeStyleAttribute(String(style())));
1774
+ el.setAttribute("style", sanitizeStyleAttribute(String(style()), { element: el }));
1628
1775
  }, el);
1629
1776
  registerDisposer(el, teardown);
1630
1777
  return;
1631
1778
  }
1632
1779
  if (typeof style === "string") {
1633
- el.setAttribute("style", sanitizeStyleAttribute(style));
1780
+ el.setAttribute("style", sanitizeStyleAttribute(style, { element: el }));
1634
1781
  return;
1635
1782
  }
1636
1783
  const htmlEl = el;
@@ -1640,11 +1787,11 @@ function applyStyle(el, style) {
1640
1787
  if (typeof val === "function") {
1641
1788
  const getter = val;
1642
1789
  const teardown = reactiveBinding2(() => {
1643
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
1790
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
1644
1791
  }, el);
1645
1792
  registerDisposer(el, teardown);
1646
1793
  } else {
1647
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
1794
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
1648
1795
  }
1649
1796
  }
1650
1797
  }
@@ -1751,11 +1898,26 @@ var tagFactory = (tag, ns) => {
1751
1898
  appendChildren(el, second);
1752
1899
  return el;
1753
1900
  }
1754
- if (_isDev7 && looksLikeClassList(first)) {
1755
- devWarn(
1756
- `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.`
1757
- );
1758
- }
1901
+ devWarnLazy(() => {
1902
+ const tokens = first.trim().split(/\s+/);
1903
+ if (tokens.length < 2) return "";
1904
+ let utilityTokens = 0;
1905
+ for (let i = 0; i < tokens.length; i++) {
1906
+ const tok = tokens[i];
1907
+ if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return "";
1908
+ if (/[-:/0-9]/.test(tok)) utilityTokens++;
1909
+ }
1910
+ if (utilityTokens < 2) return "";
1911
+ const key = `${tag}|${first}`;
1912
+ if (warnedLoneStrings.has(key)) return "";
1913
+ if (warnedLoneStrings.size >= MAX_WARNED_LONE_STRINGS) {
1914
+ if (loneStringCapAnnounced) return "";
1915
+ loneStringCapAnnounced = true;
1916
+ 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.`;
1917
+ }
1918
+ warnedLoneStrings.add(key);
1919
+ 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.`;
1920
+ });
1759
1921
  el.textContent = first;
1760
1922
  return el;
1761
1923
  }
@@ -1772,7 +1934,7 @@ var tagFactory = (tag, ns) => {
1772
1934
  if (pClass != null) applyClass(el, pClass);
1773
1935
  const pId = props.id;
1774
1936
  if (pId != null) {
1775
- if (_isDev7 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
1937
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
1776
1938
  devWarn(
1777
1939
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
1778
1940
  );
@@ -1787,7 +1949,7 @@ var tagFactory = (tag, ns) => {
1787
1949
  const handler = pOn[ev];
1788
1950
  if (typeof handler === "function") {
1789
1951
  el.addEventListener(ev, handler);
1790
- } else if (_isDev7) {
1952
+ } else if (DEV) {
1791
1953
  devWarn(
1792
1954
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
1793
1955
  );
package/dist/ecosystem.js CHANGED
@@ -7,19 +7,19 @@ import {
7
7
  mobXAdapter,
8
8
  reduxAdapter,
9
9
  zustandAdapter
10
- } from "./chunk-5BVKQX2F.js";
10
+ } from "./chunk-GDUNQCCW.js";
11
11
  import "./chunk-WWV3SJ3L.js";
12
- import "./chunk-QD4I7ZGZ.js";
13
- import "./chunk-PHRH66Y7.js";
14
- import "./chunk-AD5YSZAN.js";
15
- import "./chunk-5INI7D2L.js";
12
+ import "./chunk-6ZGI3RBE.js";
13
+ import "./chunk-HJEPKZ2O.js";
14
+ import "./chunk-2LOYNGBY.js";
15
+ import "./chunk-IXKSNWV5.js";
16
16
  import "./chunk-7ZHH77QA.js";
17
- import "./chunk-XDOMTQHM.js";
18
- import "./chunk-TIRZCERI.js";
19
- import "./chunk-DHDZ7IVN.js";
20
- import "./chunk-TVURNWDK.js";
21
- import "./chunk-JYXQCELN.js";
22
- import "./chunk-VPP2FONR.js";
17
+ import "./chunk-7VEC3UD2.js";
18
+ import "./chunk-5DXA2J44.js";
19
+ import "./chunk-QKRPLZ2V.js";
20
+ import "./chunk-HECVW343.js";
21
+ import "./chunk-7ALC7KGE.js";
22
+ import "./chunk-2WLZ6757.js";
23
23
  export {
24
24
  antdAdapter,
25
25
  chakraAdapter,