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/ssr.cjs CHANGED
@@ -66,23 +66,23 @@ __export(ssr_exports, {
66
66
  module.exports = __toCommonJS(ssr_exports);
67
67
 
68
68
  // src/core/dev.ts
69
- function isDev() {
70
- return typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
71
- // The bare `__SIBU_DEV__` is a bundler define that only exists in
72
- // production builds; under the test runner it is always undefined, so
73
- // this branch is unreachable here.
74
- /* v8 ignore next 2 */
75
- typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
76
- );
77
- }
78
- var _isDev = isDev();
69
+ var DEV = typeof __SIBU_DEV__ !== "undefined" ? !!__SIBU_DEV__ : typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
70
+ // safe default: off in browser, on in test/dev Node
71
+ typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
72
+ );
79
73
  function devAssert(condition, message) {
80
- if (_isDev && !condition) {
74
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
81
75
  throw new Error(`[SibuJS] ${message}`);
82
76
  }
83
77
  }
78
+ function devWarnLazy(build) {
79
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
80
+ const message = build();
81
+ if (message) console.warn(`[SibuJS] ${message}`);
82
+ }
83
+ }
84
84
  function devWarn(message) {
85
- if (_isDev) {
85
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
86
86
  console.warn(`[SibuJS] ${message}`);
87
87
  }
88
88
  }
@@ -185,7 +185,6 @@ function reportDrainRunaway(label2, executed, remaining) {
185
185
  { executed, remaining }
186
186
  );
187
187
  }
188
- var _isDev2 = isDev();
189
188
  var activeBindingCount = 0;
190
189
  function registerDisposer(node, teardown) {
191
190
  let disposers = elementDisposers.get(node);
@@ -194,7 +193,7 @@ function registerDisposer(node, teardown) {
194
193
  elementDisposers.set(node, disposers);
195
194
  }
196
195
  disposers.push(teardown);
197
- if (_isDev2) activeBindingCount++;
196
+ if (DEV) activeBindingCount++;
198
197
  }
199
198
  function dispose(node) {
200
199
  const stack = [node];
@@ -217,13 +216,13 @@ function dispose(node) {
217
216
  if (!pending || pending.length === 0) break;
218
217
  const snapshot = pending.slice();
219
218
  elementDisposers.delete(current);
220
- if (_isDev2) activeBindingCount -= snapshot.length;
219
+ if (DEV) activeBindingCount -= snapshot.length;
221
220
  for (let i3 = 0; i3 < snapshot.length; i3++) {
222
221
  if (executed >= MAX_DRAIN_TEARDOWNS) {
223
222
  const rest = snapshot.slice(i3);
224
223
  const added = elementDisposers.get(current);
225
224
  elementDisposers.set(current, added ? rest.concat(added) : rest);
226
- if (_isDev2) activeBindingCount += rest.length;
225
+ if (DEV) activeBindingCount += rest.length;
227
226
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
228
227
  runaway = true;
229
228
  break;
@@ -386,20 +385,75 @@ function decodeCssEscapes(value) {
386
385
  }
387
386
  return out;
388
387
  }
389
- function sanitizeCSSValue(value) {
390
- if (!CSS_DANGER_GATE.test(value)) return value;
388
+ var BLOCKED_CSS_NEEDLES = [
389
+ "url(",
390
+ "expression(",
391
+ "javascript:",
392
+ "vbscript:",
393
+ "-moz-binding",
394
+ "behavior:",
395
+ "@import",
396
+ "image-set(",
397
+ "filter:progid"
398
+ ];
399
+ function describeElement(el) {
400
+ if (!el) return "an element";
401
+ let out = el.tagName.toLowerCase();
402
+ const id = el.getAttribute("id");
403
+ if (id) out += `#${id}`;
404
+ const cls = el.getAttribute("class");
405
+ if (cls) out += `.${cls.trim().split(/\s+/).slice(0, 3).join(".")}`;
406
+ return `<${out}>`;
407
+ }
408
+ var warnedDeclarations = /* @__PURE__ */ new Set();
409
+ var MAX_WARNED_DECLARATIONS = 100;
410
+ var declarationCapAnnounced = false;
411
+ function warnDroppedDeclaration(value, reason, ctx) {
412
+ devWarnLazy(() => {
413
+ const reasons = [
414
+ "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",
415
+ "expression() executes arbitrary script in legacy engines",
416
+ "the javascript: scheme executes script",
417
+ "the vbscript: scheme executes script",
418
+ "-moz-binding loads and runs remote XBL",
419
+ "behavior: loads and runs a remote HTC script",
420
+ "@import pulls in a remote stylesheet",
421
+ "image-set() fetches remote images, the same exfiltration channel as url()",
422
+ "filter:progid activates legacy scriptable filters"
423
+ ];
424
+ const declaration = ctx?.property ? `${ctx.property}: ${value}` : value;
425
+ const where = describeElement(ctx?.element);
426
+ const key = `${where}|${declaration}`;
427
+ if (warnedDeclarations.has(key)) return "";
428
+ if (warnedDeclarations.size >= MAX_WARNED_DECLARATIONS) {
429
+ if (declarationCapAnnounced) return "";
430
+ declarationCapAnnounced = true;
431
+ 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.`;
432
+ }
433
+ warnedDeclarations.add(key);
434
+ 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.`;
435
+ });
436
+ }
437
+ function blockedCssReason(value) {
438
+ if (!CSS_DANGER_GATE.test(value)) return -1;
391
439
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
392
440
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
393
- 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")) {
394
- return "";
441
+ for (let i2 = 0; i2 < BLOCKED_CSS_NEEDLES.length; i2++) {
442
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i2])) return i2;
395
443
  }
396
- return value;
444
+ return -1;
397
445
  }
398
- function sanitizeStyleAttribute(cssText) {
446
+ function sanitizeCSSValue(value, context) {
447
+ const reason = blockedCssReason(value);
448
+ if (reason === -1) return value;
449
+ warnDroppedDeclaration(value, reason, context);
450
+ return "";
451
+ }
452
+ function sanitizeStyleAttribute(cssText, context) {
399
453
  const input2 = String(cssText);
400
454
  if (input2.trim() === "") return "";
401
455
  if (typeof document === "undefined") {
402
- return sanitizeCSSValue(input2) === "" ? "" : input2;
456
+ return sanitizeCSSValue(input2, context) === "" ? "" : input2;
403
457
  }
404
458
  const probe = document.createElement("div");
405
459
  try {
@@ -411,8 +465,12 @@ function sanitizeStyleAttribute(cssText) {
411
465
  for (let i2 = 0; i2 < probe.style.length; i2++) {
412
466
  const property = probe.style[i2];
413
467
  const value = probe.style.getPropertyValue(property);
414
- if (sanitizeCSSValue(value) === "") continue;
415
- if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
468
+ let reason = blockedCssReason(value);
469
+ if (reason === -1) reason = blockedCssReason(`${property}:${value}`);
470
+ if (reason !== -1) {
471
+ warnDroppedDeclaration(value, reason, { property, element: context?.element });
472
+ continue;
473
+ }
416
474
  const priority = probe.style.getPropertyPriority(property);
417
475
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
418
476
  }
@@ -444,11 +502,11 @@ function isPolicyAttribute(name) {
444
502
  const lower = name.toLowerCase();
445
503
  return lower === "style" || URL_ATTRIBUTES.has(lower);
446
504
  }
447
- function sanitizeAttributeString(attr, value) {
505
+ function sanitizeAttributeString(attr, value, context) {
448
506
  const lower = attr.toLowerCase();
449
507
  if (lower === "srcset") return sanitizeSrcset(value);
450
508
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
451
- if (lower === "style") return sanitizeStyleAttribute(value);
509
+ if (lower === "style") return sanitizeStyleAttribute(value, context);
452
510
  return value;
453
511
  }
454
512
 
@@ -486,9 +544,9 @@ function parseMetaRefreshContent(content) {
486
544
  i2++;
487
545
  while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
488
546
  let destination;
489
- const quote = raw[i2];
490
- if (quote === '"' || quote === "'") {
491
- const close = raw.indexOf(quote, i2 + 1);
547
+ const quote2 = raw[i2];
548
+ if (quote2 === '"' || quote2 === "'") {
549
+ const close = raw.indexOf(quote2, i2 + 1);
492
550
  if (close === -1) return forbidden("unterminated quoted destination");
493
551
  destination = raw.slice(i2 + 1, close);
494
552
  const rest = trimAsciiWhitespace(raw.slice(close + 1));
@@ -619,13 +677,12 @@ function isForbiddenSsrAttr(lowerName) {
619
677
  function sanitizeSsrAttributeValue(lowerName, value) {
620
678
  return sanitizeAttributeString(lowerName, value);
621
679
  }
622
- var _isDev3 = isDev();
623
680
  var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
624
681
  function isSafeAttrName(name) {
625
682
  return SAFE_ATTR_NAME.test(name);
626
683
  }
627
684
  function ssrErrorComment(err) {
628
- if (_isDev3) {
685
+ if (DEV) {
629
686
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
630
687
  return `<!--SSR error: ${safeCommentText(msg)}-->`;
631
688
  }
@@ -671,10 +728,10 @@ function renderToString(element) {
671
728
  }
672
729
  const tag = element.tagName.toLowerCase();
673
730
  if (tag === "script" || tag === "style") {
674
- return _isDev3 ? `<!--ssr:${tag}-stripped-->` : "";
731
+ return DEV ? `<!--ssr:${tag}-stripped-->` : "";
675
732
  }
676
733
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
677
- return _isDev3 ? "<!--ssr:invalid-tag-->" : "";
734
+ return DEV ? "<!--ssr:invalid-tag-->" : "";
678
735
  }
679
736
  let html2 = `<${tag}`;
680
737
  for (const attr of Array.from(element.attributes)) {
@@ -716,7 +773,7 @@ function hydrate(component, container, options = {}) {
716
773
  const first = mismatches[0];
717
774
  if (options.onMismatch) {
718
775
  options.onMismatch(first);
719
- } else if (_isDev3) {
776
+ } else if (DEV) {
720
777
  console.warn(
721
778
  `[SibuJS hydration] ${first.message}
722
779
  at ${first.path}
@@ -917,11 +974,11 @@ async function* renderToStream(element) {
917
974
  }
918
975
  const tag = element.tagName.toLowerCase();
919
976
  if (tag === "script" || tag === "style") {
920
- if (_isDev3) yield `<!--ssr:${tag}-stripped-->`;
977
+ if (DEV) yield `<!--ssr:${tag}-stripped-->`;
921
978
  return;
922
979
  }
923
980
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
924
- if (_isDev3) yield "<!--ssr:invalid-tag-->";
981
+ if (DEV) yield "<!--ssr:invalid-tag-->";
925
982
  return;
926
983
  }
927
984
  let openTag = `<${tag}`;
@@ -1064,7 +1121,7 @@ function ssrSuspense(props) {
1064
1121
  },
1065
1122
  (err) => {
1066
1123
  if (timer) clearTimeout(timer);
1067
- if (_isDev3) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
1124
+ if (DEV) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
1068
1125
  return { id, html: fallbackHtml };
1069
1126
  }
1070
1127
  );
@@ -1110,7 +1167,7 @@ function serializeState(state2, nonce, options) {
1110
1167
  }
1111
1168
  function deserializeState(validate) {
1112
1169
  if (typeof window === "undefined") return void 0;
1113
- if (_isDev3 && !validate) {
1170
+ if (DEV && !validate) {
1114
1171
  console.warn(
1115
1172
  "[SibuJS SSR] deserializeState() called without a validate guard \u2014 tampered SSR payloads will not be detected."
1116
1173
  );
@@ -1584,17 +1641,16 @@ function forEachSubscriber(signal2, visit) {
1584
1641
  }
1585
1642
 
1586
1643
  // src/reactivity/track.ts
1587
- var _isDev4 = isDev();
1588
- var _runtimeVersion = true ? "4.1.0" : "dev";
1644
+ var _runtimeVersion = true ? "4.2.0" : "dev";
1589
1645
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
1590
1646
  function resolveReactiveApi() {
1591
1647
  const g2 = globalThis;
1592
1648
  const existing = g2[REGISTRY_KEY];
1593
1649
  if (existing) {
1594
- if (_isDev4 && !existing.__dupWarned) {
1650
+ if (DEV && !existing.__dupWarned) {
1595
1651
  existing.__dupWarned = true;
1596
1652
  devWarn(
1597
- `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']).`
1653
+ `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.`
1598
1654
  );
1599
1655
  }
1600
1656
  return existing;
@@ -2097,7 +2153,6 @@ var isBatching = API2.isBatching;
2097
2153
 
2098
2154
  // src/core/signals/signal.ts
2099
2155
  var _g2 = globalThis;
2100
- var _isDev5 = isDev();
2101
2156
  function signal(initial, options) {
2102
2157
  const state2 = {
2103
2158
  value: initial,
@@ -2110,7 +2165,7 @@ function signal(initial, options) {
2110
2165
  _d: false,
2111
2166
  _validate: void 0
2112
2167
  };
2113
- const debugName = _isDev5 ? options?.name : void 0;
2168
+ const debugName = DEV ? options?.name : void 0;
2114
2169
  const equalsFn = options?.equals;
2115
2170
  if (debugName) state2.__name = debugName;
2116
2171
  function get() {
@@ -2127,7 +2182,7 @@ function signal(initial, options) {
2127
2182
  if (equalsFn(prev, newValue)) return;
2128
2183
  state2.value = newValue;
2129
2184
  state2.__v++;
2130
- if (_isDev5) {
2185
+ if (DEV) {
2131
2186
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
2132
2187
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
2133
2188
  }
@@ -2135,7 +2190,7 @@ function signal(initial, options) {
2135
2190
  notifySubscribers2(state2);
2136
2191
  }
2137
2192
  };
2138
- } else if (_isDev5) {
2193
+ } else if (DEV) {
2139
2194
  set = (next) => {
2140
2195
  const prev = state2.value;
2141
2196
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -2160,7 +2215,7 @@ function signal(initial, options) {
2160
2215
  }
2161
2216
  };
2162
2217
  }
2163
- if (_isDev5) {
2218
+ if (DEV) {
2164
2219
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
2165
2220
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
2166
2221
  }
@@ -2364,7 +2419,6 @@ function createMiddlewareChain() {
2364
2419
  }
2365
2420
 
2366
2421
  // src/utils/setSafeAttribute.ts
2367
- var _isDev6 = isDev();
2368
2422
  var HTML_NS = "http://www.w3.org/1999/xhtml";
2369
2423
  var SVG_NS = "http://www.w3.org/2000/svg";
2370
2424
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -2387,7 +2441,7 @@ function setSafeAttribute(el, name, value, options = {}) {
2387
2441
  const ns = namespaceFor(el, name);
2388
2442
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
2389
2443
  if (isEventHandlerAttr(name)) {
2390
- if (_isDev6) {
2444
+ if (DEV) {
2391
2445
  devWarn(
2392
2446
  `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
2393
2447
  );
@@ -2402,7 +2456,7 @@ function setSafeAttribute(el, name, value, options = {}) {
2402
2456
  return true;
2403
2457
  }
2404
2458
  if (isHtmlContentAttribute(name)) {
2405
- if (_isDev6) {
2459
+ if (DEV) {
2406
2460
  devWarn(
2407
2461
  `${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.`
2408
2462
  );
@@ -2431,7 +2485,7 @@ function setSafeAttribute(el, name, value, options = {}) {
2431
2485
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
2432
2486
  return true;
2433
2487
  }
2434
- const safe = sanitizeAttributeString(name, str);
2488
+ const safe = sanitizeAttributeString(name, str, { element: el });
2435
2489
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
2436
2490
  if (current === safe) return true;
2437
2491
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -2459,8 +2513,101 @@ function bindAttribute(el, attr, getter) {
2459
2513
  return reactiveBinding2(commit, el);
2460
2514
  }
2461
2515
 
2516
+ // src/core/rendering/focusPreservation.ts
2517
+ var IDENTITY_ATTRS = ["data-focus-key", "id", "name"];
2518
+ var SELECTABLE_TYPES = /* @__PURE__ */ new Set(["text", "search", "url", "tel", "password", ""]);
2519
+ function selectionOf(el) {
2520
+ const tag = el.tagName;
2521
+ const selectable = tag === "TEXTAREA" || tag === "INPUT" && SELECTABLE_TYPES.has(el.type.toLowerCase());
2522
+ if (!selectable) return { start: null, end: null, direction: null };
2523
+ const field = el;
2524
+ return { start: field.selectionStart, end: field.selectionEnd, direction: field.selectionDirection };
2525
+ }
2526
+ function captureFocusWithin(nodes) {
2527
+ if (nodes.length === 0 || typeof document === "undefined") return null;
2528
+ const active = document.activeElement;
2529
+ if (!active || active === document.body) return null;
2530
+ let inside = false;
2531
+ for (let i2 = 0; i2 < nodes.length; i2++) {
2532
+ const n = nodes[i2];
2533
+ if (n === active || n.nodeType === 1 && n.contains(active)) {
2534
+ inside = true;
2535
+ break;
2536
+ }
2537
+ }
2538
+ if (!inside) return null;
2539
+ let attr = null;
2540
+ let value = null;
2541
+ for (let i2 = 0; i2 < IDENTITY_ATTRS.length; i2++) {
2542
+ const candidate = active.getAttribute(IDENTITY_ATTRS[i2]);
2543
+ if (candidate) {
2544
+ attr = IDENTITY_ATTRS[i2];
2545
+ value = candidate;
2546
+ break;
2547
+ }
2548
+ }
2549
+ return { el: active, attr, value, tag: active.tagName, ...selectionOf(active) };
2550
+ }
2551
+ function quote(value) {
2552
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
2553
+ }
2554
+ function findUniqueByIdentity(nodes, selector) {
2555
+ let el = null;
2556
+ let count = 0;
2557
+ for (let i2 = 0; i2 < nodes.length; i2++) {
2558
+ const n = nodes[i2];
2559
+ if (n.nodeType !== 1) continue;
2560
+ const root = n;
2561
+ if (root.matches(selector)) {
2562
+ count++;
2563
+ el ?? (el = root);
2564
+ }
2565
+ const inner = root.querySelectorAll(selector);
2566
+ for (let j = 0; j < inner.length; j++) {
2567
+ count++;
2568
+ el ?? (el = inner[j]);
2569
+ }
2570
+ if (count > 1) return { el: null, count };
2571
+ }
2572
+ return count === 1 ? { el, count } : { el: null, count };
2573
+ }
2574
+ function refocus(el, snapshot) {
2575
+ el.focus();
2576
+ if (snapshot.start !== null && typeof el.setSelectionRange === "function") {
2577
+ const field = el;
2578
+ const max = field.value.length;
2579
+ const start = Math.min(snapshot.start, max);
2580
+ const end = Math.min(snapshot.end ?? start, max);
2581
+ field.setSelectionRange(start, end, snapshot.direction ?? "none");
2582
+ }
2583
+ }
2584
+ function restoreFocusWithin(snapshot, nodes, where) {
2585
+ if (!snapshot) return;
2586
+ if (typeof document === "undefined") return;
2587
+ const active = document.activeElement;
2588
+ if (active === snapshot.el) return;
2589
+ if (active && active !== document.body && active !== document.documentElement) return;
2590
+ if (snapshot.el.isConnected) {
2591
+ refocus(snapshot.el, snapshot);
2592
+ return;
2593
+ }
2594
+ let ambiguous = false;
2595
+ if (snapshot.attr && snapshot.value) {
2596
+ const { el: replacement, count } = findUniqueByIdentity(nodes, `[${snapshot.attr}=${quote(snapshot.value)}]`);
2597
+ if (replacement && replacement.tagName === snapshot.tag) {
2598
+ refocus(replacement, snapshot);
2599
+ return;
2600
+ }
2601
+ ambiguous = count > 1;
2602
+ }
2603
+ devWarnLazy(() => {
2604
+ const id = snapshot.attr ? ` (${snapshot.attr}="${snapshot.value}")` : "";
2605
+ 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";
2606
+ 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.`;
2607
+ });
2608
+ }
2609
+
2462
2610
  // src/reactivity/bindChildNode.ts
2463
- var _isDev7 = isDev();
2464
2611
  function bindChildNode(placeholder, getter) {
2465
2612
  let lastNodes = [];
2466
2613
  function commit() {
@@ -2471,6 +2618,7 @@ function bindChildNode(placeholder, getter) {
2471
2618
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
2472
2619
  return;
2473
2620
  }
2621
+ const focused = captureFocusWithin(lastNodes);
2474
2622
  if (result == null || typeof result === "boolean") {
2475
2623
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
2476
2624
  const node = lastNodes[i2];
@@ -2478,6 +2626,7 @@ function bindChildNode(placeholder, getter) {
2478
2626
  if (node.parentNode) node.parentNode.removeChild(node);
2479
2627
  }
2480
2628
  lastNodes.length = 0;
2629
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
2481
2630
  return;
2482
2631
  }
2483
2632
  const parent = placeholder.parentNode;
@@ -2494,8 +2643,7 @@ function bindChildNode(placeholder, getter) {
2494
2643
  if (item == null || typeof item === "boolean") continue;
2495
2644
  const node = item instanceof Node ? item : document.createTextNode(String(item));
2496
2645
  if (seen.has(node)) {
2497
- if (_isDev7)
2498
- devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
2646
+ if (DEV) devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
2499
2647
  continue;
2500
2648
  }
2501
2649
  seen.add(node);
@@ -2528,13 +2676,13 @@ function bindChildNode(placeholder, getter) {
2528
2676
  prev = node;
2529
2677
  }
2530
2678
  lastNodes = newNodes;
2679
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
2531
2680
  }
2532
2681
  return reactiveBinding2(commit, placeholder);
2533
2682
  }
2534
2683
 
2535
2684
  // src/core/rendering/tagFactory.ts
2536
2685
  var SVG_NS2 = "http://www.w3.org/2000/svg";
2537
- var _isDev8 = isDev();
2538
2686
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
2539
2687
  function isBlockedTag(tag) {
2540
2688
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -2552,18 +2700,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
2552
2700
  "self",
2553
2701
  "frames"
2554
2702
  ]);
2555
- function looksLikeClassList(s2) {
2556
- const t = s2.trim();
2557
- if (!t) return false;
2558
- const tokens = t.split(/\s+/);
2559
- let sawClassish = false;
2560
- for (let i2 = 0; i2 < tokens.length; i2++) {
2561
- const tok = tokens[i2];
2562
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
2563
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
2564
- }
2565
- return sawClassish;
2566
- }
2703
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
2704
+ var MAX_WARNED_LONE_STRINGS = 100;
2705
+ var loneStringCapAnnounced = false;
2567
2706
  var kebabCache = /* @__PURE__ */ new Map();
2568
2707
  function toKebab(prop) {
2569
2708
  let cached = kebabCache.get(prop);
@@ -2575,13 +2714,13 @@ function toKebab(prop) {
2575
2714
  function applyStyle(el, style2) {
2576
2715
  if (typeof style2 === "function") {
2577
2716
  const teardown = reactiveBinding2(() => {
2578
- el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
2717
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2()), { element: el }));
2579
2718
  }, el);
2580
2719
  registerDisposer(el, teardown);
2581
2720
  return;
2582
2721
  }
2583
2722
  if (typeof style2 === "string") {
2584
- el.setAttribute("style", sanitizeStyleAttribute(style2));
2723
+ el.setAttribute("style", sanitizeStyleAttribute(style2, { element: el }));
2585
2724
  return;
2586
2725
  }
2587
2726
  const htmlEl = el;
@@ -2591,11 +2730,11 @@ function applyStyle(el, style2) {
2591
2730
  if (typeof val === "function") {
2592
2731
  const getter = val;
2593
2732
  const teardown = reactiveBinding2(() => {
2594
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
2733
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
2595
2734
  }, el);
2596
2735
  registerDisposer(el, teardown);
2597
2736
  } else {
2598
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
2737
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
2599
2738
  }
2600
2739
  }
2601
2740
  }
@@ -2702,11 +2841,26 @@ var tagFactory = (tag, ns) => {
2702
2841
  appendChildren(el, second);
2703
2842
  return el;
2704
2843
  }
2705
- if (_isDev8 && looksLikeClassList(first)) {
2706
- devWarn(
2707
- `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.`
2708
- );
2709
- }
2844
+ devWarnLazy(() => {
2845
+ const tokens = first.trim().split(/\s+/);
2846
+ if (tokens.length < 2) return "";
2847
+ let utilityTokens = 0;
2848
+ for (let i2 = 0; i2 < tokens.length; i2++) {
2849
+ const tok = tokens[i2];
2850
+ if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return "";
2851
+ if (/[-:/0-9]/.test(tok)) utilityTokens++;
2852
+ }
2853
+ if (utilityTokens < 2) return "";
2854
+ const key = `${tag}|${first}`;
2855
+ if (warnedLoneStrings.has(key)) return "";
2856
+ if (warnedLoneStrings.size >= MAX_WARNED_LONE_STRINGS) {
2857
+ if (loneStringCapAnnounced) return "";
2858
+ loneStringCapAnnounced = true;
2859
+ 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.`;
2860
+ }
2861
+ warnedLoneStrings.add(key);
2862
+ 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.`;
2863
+ });
2710
2864
  el.textContent = first;
2711
2865
  return el;
2712
2866
  }
@@ -2723,7 +2877,7 @@ var tagFactory = (tag, ns) => {
2723
2877
  if (pClass != null) applyClass(el, pClass);
2724
2878
  const pId = props.id;
2725
2879
  if (pId != null) {
2726
- if (_isDev8 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2880
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2727
2881
  devWarn(
2728
2882
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
2729
2883
  );
@@ -2738,7 +2892,7 @@ var tagFactory = (tag, ns) => {
2738
2892
  const handler = pOn[ev];
2739
2893
  if (typeof handler === "function") {
2740
2894
  el.addEventListener(ev, handler);
2741
- } else if (_isDev8) {
2895
+ } else if (DEV) {
2742
2896
  devWarn(
2743
2897
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
2744
2898
  );
package/dist/ssr.js CHANGED
@@ -23,9 +23,9 @@ import {
23
23
  wasm,
24
24
  worker,
25
25
  workerFn
26
- } from "./chunk-BMSN6NWQ.js";
26
+ } from "./chunk-NEYWS4EX.js";
27
27
  import "./chunk-52XFPGSN.js";
28
- import "./chunk-MRAMKDHQ.js";
28
+ import "./chunk-P3DHXBR6.js";
29
29
  import {
30
30
  collectStream,
31
31
  deserializeState,
@@ -45,17 +45,17 @@ import {
45
45
  ssrSuspense,
46
46
  suspenseSwapScript,
47
47
  trustHTML
48
- } from "./chunk-HYCCIYNS.js";
49
- import "./chunk-I3VI34DP.js";
50
- import "./chunk-SHDVWATN.js";
51
- import "./chunk-5INI7D2L.js";
48
+ } from "./chunk-NIOYEGBQ.js";
49
+ import "./chunk-6ZGI3RBE.js";
50
+ import "./chunk-HJEPKZ2O.js";
51
+ import "./chunk-IXKSNWV5.js";
52
52
  import "./chunk-7ZHH77QA.js";
53
- import "./chunk-PFTDGEPZ.js";
54
- import "./chunk-TIRZCERI.js";
55
- import "./chunk-DHDZ7IVN.js";
56
- import "./chunk-UFSQEO7P.js";
57
- import "./chunk-D3SZ4LVS.js";
58
- import "./chunk-VPP2FONR.js";
53
+ import "./chunk-7VEC3UD2.js";
54
+ import "./chunk-5DXA2J44.js";
55
+ import "./chunk-QKRPLZ2V.js";
56
+ import "./chunk-HECVW343.js";
57
+ import "./chunk-7ALC7KGE.js";
58
+ import "./chunk-2WLZ6757.js";
59
59
  export {
60
60
  Head,
61
61
  clearWasmCache,