sibujs 4.0.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +29 -1
  2. package/dist/browser.cjs +21 -22
  3. package/dist/browser.js +6 -6
  4. package/dist/build.cjs +363 -110
  5. package/dist/build.js +15 -15
  6. package/dist/cdn.dev.global.js +398 -0
  7. package/dist/cdn.global.js +10 -10
  8. package/dist/{chunk-AD5YSZAN.js → chunk-2LOYNGBY.js} +2 -2
  9. package/dist/{chunk-VPP2FONR.js → chunk-2WLZ6757.js} +17 -10
  10. package/dist/{chunk-UNAYBND2.js → chunk-3VTSPNC5.js} +2 -2
  11. package/dist/{chunk-TIRZCERI.js → chunk-5DXA2J44.js} +1 -1
  12. package/dist/{chunk-QD4I7ZGZ.js → chunk-6ZGI3RBE.js} +149 -33
  13. package/dist/{chunk-JYXQCELN.js → chunk-7ALC7KGE.js} +13 -7
  14. package/dist/{chunk-XDOMTQHM.js → chunk-7VEC3UD2.js} +3 -3
  15. package/dist/{chunk-PTLJ7NQX.js → chunk-BMYYZWHX.js} +3 -3
  16. package/dist/{chunk-PHB3MCK4.js → chunk-BNKV4REK.js} +7 -8
  17. package/dist/{chunk-XET2RUJF.js → chunk-FOWBJZDL.js} +7 -8
  18. package/dist/{chunk-5BVKQX2F.js → chunk-GDUNQCCW.js} +4 -4
  19. package/dist/{chunk-TVURNWDK.js → chunk-HECVW343.js} +20 -9
  20. package/dist/{chunk-PHRH66Y7.js → chunk-HJEPKZ2O.js} +7 -8
  21. package/dist/{chunk-PJDSI2K7.js → chunk-HZEBO22J.js} +125 -42
  22. package/dist/{chunk-5INI7D2L.js → chunk-IXKSNWV5.js} +74 -11
  23. package/dist/{chunk-IKVUUE7D.js → chunk-KE4KWRDY.js} +3 -3
  24. package/dist/{chunk-AW6HPJ3L.js → chunk-KKLW7YWL.js} +1 -1
  25. package/dist/{chunk-XZMCVZW6.js → chunk-NEYWS4EX.js} +6 -6
  26. package/dist/{chunk-HYCCIYNS.js → chunk-NIOYEGBQ.js} +13 -14
  27. package/dist/{chunk-YLV26GAJ.js → chunk-OUWAIXIK.js} +3 -3
  28. package/dist/{chunk-E72G5JSK.js → chunk-P3DHXBR6.js} +1 -1
  29. package/dist/{chunk-J4WZMCIW.js → chunk-QCFLDD7C.js} +5 -5
  30. package/dist/{chunk-DHDZ7IVN.js → chunk-QKRPLZ2V.js} +7 -8
  31. package/dist/{chunk-52ERIVFD.js → chunk-QL4UREZO.js} +6 -6
  32. package/dist/{chunk-TNEHT3NG.js → chunk-SF3TB4K6.js} +8 -8
  33. package/dist/{chunk-HPXTUPJD.js → chunk-XZ2N525U.js} +6 -6
  34. package/dist/{customElement-CKLoO63w.d.ts → customElement-CNZxEB9G.d.cts} +77 -0
  35. package/dist/{customElement-CKLoO63w.d.cts → customElement-CNZxEB9G.d.ts} +77 -0
  36. package/dist/data.cjs +23 -25
  37. package/dist/data.js +9 -9
  38. package/dist/devtools.cjs +26 -23
  39. package/dist/devtools.d.cts +1 -1
  40. package/dist/devtools.d.ts +1 -1
  41. package/dist/devtools.js +8 -8
  42. package/dist/{dispose-4FZ6OFXR.js → dispose-46BOMMQJ.js} +2 -2
  43. package/dist/ecosystem.cjs +230 -68
  44. package/dist/ecosystem.js +11 -11
  45. package/dist/extras.cjs +251 -87
  46. package/dist/extras.d.cts +2 -2
  47. package/dist/extras.d.ts +2 -2
  48. package/dist/extras.js +25 -25
  49. package/dist/index.cjs +358 -104
  50. package/dist/index.d.cts +287 -34
  51. package/dist/index.d.ts +287 -34
  52. package/dist/index.js +17 -15
  53. package/dist/{introspect-RjLfIFpL.d.cts → introspect-D5xF-0Es.d.cts} +7 -0
  54. package/dist/{introspect-DOZfmC-4.d.ts → introspect-rSBIxEQi.d.ts} +7 -0
  55. package/dist/motion.cjs +21 -23
  56. package/dist/motion.js +5 -5
  57. package/dist/patterns.cjs +24 -25
  58. package/dist/patterns.d.cts +8 -0
  59. package/dist/patterns.d.ts +8 -0
  60. package/dist/patterns.js +7 -7
  61. package/dist/performance.cjs +24 -28
  62. package/dist/performance.js +9 -9
  63. package/dist/plugins.cjs +268 -87
  64. package/dist/plugins.d.cts +92 -0
  65. package/dist/plugins.d.ts +92 -0
  66. package/dist/plugins.js +18 -17
  67. package/dist/{ssr-JFP5535P.js → ssr-Y7XOEPEN.js} +5 -5
  68. package/dist/ssr.cjs +241 -80
  69. package/dist/ssr.js +12 -12
  70. package/dist/testing.cjs +19 -20
  71. package/dist/testing.js +5 -5
  72. package/dist/ui.cjs +106 -44
  73. package/dist/ui.d.cts +7 -1
  74. package/dist/ui.d.ts +7 -1
  75. package/dist/ui.js +10 -10
  76. package/dist/widgets.cjs +21 -22
  77. package/dist/widgets.d.cts +8 -0
  78. package/dist/widgets.d.ts +8 -0
  79. package/dist/widgets.js +9 -9
  80. package/package.json +11 -2
package/dist/extras.cjs CHANGED
@@ -318,23 +318,28 @@ __export(extras_exports, {
318
318
  module.exports = __toCommonJS(extras_exports);
319
319
 
320
320
  // src/core/dev.ts
321
+ var DEV = typeof __SIBU_DEV__ !== "undefined" ? !!__SIBU_DEV__ : typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
322
+ // safe default: off in browser, on in test/dev Node
323
+ typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
324
+ );
321
325
  function isDev() {
322
- return typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
323
- // The bare `__SIBU_DEV__` is a bundler define that only exists in
324
- // production builds; under the test runner it is always undefined, so
325
- // this branch is unreachable here.
326
- /* v8 ignore next 2 */
327
- typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
328
- );
326
+ if (typeof globalThis.__SIBU_DEV__ !== "undefined") return !!globalThis.__SIBU_DEV__;
327
+ if (typeof __SIBU_DEV__ !== "undefined") return !!__SIBU_DEV__;
328
+ return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
329
329
  }
330
- var _isDev = isDev();
331
330
  function devAssert(condition, message) {
332
- if (_isDev && !condition) {
331
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
333
332
  throw new Error(`[SibuJS] ${message}`);
334
333
  }
335
334
  }
335
+ function devWarnLazy(build) {
336
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
337
+ const message = build();
338
+ if (message) console.warn(`[SibuJS] ${message}`);
339
+ }
340
+ }
336
341
  function devWarn(message) {
337
- if (_isDev) {
342
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
338
343
  console.warn(`[SibuJS] ${message}`);
339
344
  }
340
345
  }
@@ -645,7 +650,14 @@ function reactiveBinding(commit, ownerNode) {
645
650
  subscriber._disposed = false;
646
651
  subscriber._errorPhase = "binding";
647
652
  subscriber._errorNode = ownerNode;
648
- run();
653
+ try {
654
+ run();
655
+ } catch (err) {
656
+ subscriber._disposed = true;
657
+ subscriber._errorNode = void 0;
658
+ cleanup(subscriber);
659
+ throw err;
660
+ }
649
661
  return subscriber._dispose ?? (subscriber._dispose = () => {
650
662
  subscriber._disposed = true;
651
663
  subscriber._errorNode = void 0;
@@ -876,17 +888,16 @@ function forEachSubscriber(signal2, visit) {
876
888
  }
877
889
 
878
890
  // src/reactivity/track.ts
879
- var _isDev2 = isDev();
880
- var _runtimeVersion = true ? "4.0.1" : "dev";
891
+ var _runtimeVersion = true ? "4.2.0" : "dev";
881
892
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
882
893
  function resolveReactiveApi() {
883
894
  const g2 = globalThis;
884
895
  const existing = g2[REGISTRY_KEY];
885
896
  if (existing) {
886
- if (_isDev2 && !existing.__dupWarned) {
897
+ if (DEV && !existing.__dupWarned) {
887
898
  existing.__dupWarned = true;
888
899
  devWarn(
889
- `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']).`
900
+ `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.`
890
901
  );
891
902
  }
892
903
  return existing;
@@ -1232,7 +1243,6 @@ var isBatching = API2.isBatching;
1232
1243
 
1233
1244
  // src/core/signals/signal.ts
1234
1245
  var _g2 = globalThis;
1235
- var _isDev3 = isDev();
1236
1246
  function signal(initial, options) {
1237
1247
  const state2 = {
1238
1248
  value: initial,
@@ -1245,7 +1255,7 @@ function signal(initial, options) {
1245
1255
  _d: false,
1246
1256
  _validate: void 0
1247
1257
  };
1248
- const debugName = _isDev3 ? options?.name : void 0;
1258
+ const debugName = DEV ? options?.name : void 0;
1249
1259
  const equalsFn = options?.equals;
1250
1260
  if (debugName) state2.__name = debugName;
1251
1261
  function get() {
@@ -1262,7 +1272,7 @@ function signal(initial, options) {
1262
1272
  if (equalsFn(prev, newValue)) return;
1263
1273
  state2.value = newValue;
1264
1274
  state2.__v++;
1265
- if (_isDev3) {
1275
+ if (DEV) {
1266
1276
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1267
1277
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1268
1278
  }
@@ -1270,7 +1280,7 @@ function signal(initial, options) {
1270
1280
  notifySubscribers2(state2);
1271
1281
  }
1272
1282
  };
1273
- } else if (_isDev3) {
1283
+ } else if (DEV) {
1274
1284
  set = (next) => {
1275
1285
  const prev = state2.value;
1276
1286
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -1295,7 +1305,7 @@ function signal(initial, options) {
1295
1305
  }
1296
1306
  };
1297
1307
  }
1298
- if (_isDev3) {
1308
+ if (DEV) {
1299
1309
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1300
1310
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
1301
1311
  }
@@ -2432,11 +2442,10 @@ function syncAdapter(config) {
2432
2442
  }
2433
2443
 
2434
2444
  // src/core/rendering/context.ts
2435
- var _isDev4 = isDev();
2436
2445
  function context(defaultValue) {
2437
2446
  const [getValue, setValue] = signal(defaultValue);
2438
2447
  const warnIfSSR = (method) => {
2439
- if (!_isDev4 || !isSSR()) return;
2448
+ if (!DEV || !isSSR()) return;
2440
2449
  devWarn(
2441
2450
  `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.`
2442
2451
  );
@@ -2464,7 +2473,7 @@ function context(defaultValue) {
2464
2473
  setValue(value);
2465
2474
  try {
2466
2475
  const result = fn();
2467
- if (_isDev4 && result !== null && typeof result?.then === "function") {
2476
+ if (DEV && result !== null && typeof result?.then === "function") {
2468
2477
  devWarn(
2469
2478
  "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."
2470
2479
  );
@@ -2717,20 +2726,75 @@ function decodeCssEscapes(value) {
2717
2726
  }
2718
2727
  return out;
2719
2728
  }
2720
- function sanitizeCSSValue(value) {
2721
- if (!CSS_DANGER_GATE.test(value)) return value;
2729
+ var BLOCKED_CSS_NEEDLES = [
2730
+ "url(",
2731
+ "expression(",
2732
+ "javascript:",
2733
+ "vbscript:",
2734
+ "-moz-binding",
2735
+ "behavior:",
2736
+ "@import",
2737
+ "image-set(",
2738
+ "filter:progid"
2739
+ ];
2740
+ function describeElement(el) {
2741
+ if (!el) return "an element";
2742
+ let out = el.tagName.toLowerCase();
2743
+ const id = el.getAttribute("id");
2744
+ if (id) out += `#${id}`;
2745
+ const cls = el.getAttribute("class");
2746
+ if (cls) out += `.${cls.trim().split(/\s+/).slice(0, 3).join(".")}`;
2747
+ return `<${out}>`;
2748
+ }
2749
+ var warnedDeclarations = /* @__PURE__ */ new Set();
2750
+ var MAX_WARNED_DECLARATIONS = 100;
2751
+ var declarationCapAnnounced = false;
2752
+ function warnDroppedDeclaration(value, reason, ctx) {
2753
+ devWarnLazy(() => {
2754
+ const reasons = [
2755
+ "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",
2756
+ "expression() executes arbitrary script in legacy engines",
2757
+ "the javascript: scheme executes script",
2758
+ "the vbscript: scheme executes script",
2759
+ "-moz-binding loads and runs remote XBL",
2760
+ "behavior: loads and runs a remote HTC script",
2761
+ "@import pulls in a remote stylesheet",
2762
+ "image-set() fetches remote images, the same exfiltration channel as url()",
2763
+ "filter:progid activates legacy scriptable filters"
2764
+ ];
2765
+ const declaration = ctx?.property ? `${ctx.property}: ${value}` : value;
2766
+ const where = describeElement(ctx?.element);
2767
+ const key = `${where}|${declaration}`;
2768
+ if (warnedDeclarations.has(key)) return "";
2769
+ if (warnedDeclarations.size >= MAX_WARNED_DECLARATIONS) {
2770
+ if (declarationCapAnnounced) return "";
2771
+ declarationCapAnnounced = true;
2772
+ 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.`;
2773
+ }
2774
+ warnedDeclarations.add(key);
2775
+ 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.`;
2776
+ });
2777
+ }
2778
+ function blockedCssReason(value) {
2779
+ if (!CSS_DANGER_GATE.test(value)) return -1;
2722
2780
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
2723
2781
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
2724
- 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")) {
2725
- return "";
2782
+ for (let i2 = 0; i2 < BLOCKED_CSS_NEEDLES.length; i2++) {
2783
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i2])) return i2;
2726
2784
  }
2727
- return value;
2785
+ return -1;
2728
2786
  }
2729
- function sanitizeStyleAttribute(cssText) {
2787
+ function sanitizeCSSValue(value, context2) {
2788
+ const reason = blockedCssReason(value);
2789
+ if (reason === -1) return value;
2790
+ warnDroppedDeclaration(value, reason, context2);
2791
+ return "";
2792
+ }
2793
+ function sanitizeStyleAttribute(cssText, context2) {
2730
2794
  const input2 = String(cssText);
2731
2795
  if (input2.trim() === "") return "";
2732
2796
  if (typeof document === "undefined") {
2733
- return sanitizeCSSValue(input2) === "" ? "" : input2;
2797
+ return sanitizeCSSValue(input2, context2) === "" ? "" : input2;
2734
2798
  }
2735
2799
  const probe = document.createElement("div");
2736
2800
  try {
@@ -2742,8 +2806,12 @@ function sanitizeStyleAttribute(cssText) {
2742
2806
  for (let i2 = 0; i2 < probe.style.length; i2++) {
2743
2807
  const property = probe.style[i2];
2744
2808
  const value = probe.style.getPropertyValue(property);
2745
- if (sanitizeCSSValue(value) === "") continue;
2746
- if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
2809
+ let reason = blockedCssReason(value);
2810
+ if (reason === -1) reason = blockedCssReason(`${property}:${value}`);
2811
+ if (reason !== -1) {
2812
+ warnDroppedDeclaration(value, reason, { property, element: context2?.element });
2813
+ continue;
2814
+ }
2747
2815
  const priority = probe.style.getPropertyPriority(property);
2748
2816
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
2749
2817
  }
@@ -2791,11 +2859,11 @@ function isPolicyAttribute(name) {
2791
2859
  const lower = name.toLowerCase();
2792
2860
  return lower === "style" || URL_ATTRIBUTES.has(lower);
2793
2861
  }
2794
- function sanitizeAttributeString(attr, value) {
2862
+ function sanitizeAttributeString(attr, value, context2) {
2795
2863
  const lower = attr.toLowerCase();
2796
2864
  if (lower === "srcset") return sanitizeSrcset(value);
2797
2865
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
2798
- if (lower === "style") return sanitizeStyleAttribute(value);
2866
+ if (lower === "style") return sanitizeStyleAttribute(value, context2);
2799
2867
  return value;
2800
2868
  }
2801
2869
 
@@ -5111,7 +5179,6 @@ function reportDrainRunaway(label2, executed, remaining) {
5111
5179
  { executed, remaining }
5112
5180
  );
5113
5181
  }
5114
- var _isDev5 = isDev();
5115
5182
  var activeBindingCount = 0;
5116
5183
  function registerDisposer(node, teardown) {
5117
5184
  let disposers = elementDisposers.get(node);
@@ -5120,7 +5187,7 @@ function registerDisposer(node, teardown) {
5120
5187
  elementDisposers.set(node, disposers);
5121
5188
  }
5122
5189
  disposers.push(teardown);
5123
- if (_isDev5) activeBindingCount++;
5190
+ if (DEV) activeBindingCount++;
5124
5191
  }
5125
5192
  function dispose(node) {
5126
5193
  const stack = [node];
@@ -5143,13 +5210,13 @@ function dispose(node) {
5143
5210
  if (!pending || pending.length === 0) break;
5144
5211
  const snapshot = pending.slice();
5145
5212
  elementDisposers.delete(current);
5146
- if (_isDev5) activeBindingCount -= snapshot.length;
5213
+ if (DEV) activeBindingCount -= snapshot.length;
5147
5214
  for (let i3 = 0; i3 < snapshot.length; i3++) {
5148
5215
  if (executed >= MAX_DRAIN_TEARDOWNS) {
5149
5216
  const rest = snapshot.slice(i3);
5150
5217
  const added = elementDisposers.get(current);
5151
5218
  elementDisposers.set(current, added ? rest.concat(added) : rest);
5152
- if (_isDev5) activeBindingCount += rest.length;
5219
+ if (DEV) activeBindingCount += rest.length;
5153
5220
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
5154
5221
  runaway = true;
5155
5222
  break;
@@ -6266,16 +6333,16 @@ function sanitizeCSS(css) {
6266
6333
  function firstCompoundEnd(selector) {
6267
6334
  let parens = 0;
6268
6335
  let brackets = 0;
6269
- let quote = null;
6336
+ let quote2 = null;
6270
6337
  for (let i2 = 0; i2 < selector.length; i2++) {
6271
6338
  const ch = selector[i2];
6272
- if (quote) {
6339
+ if (quote2) {
6273
6340
  if (ch === "\\") i2++;
6274
- else if (ch === quote) quote = null;
6341
+ else if (ch === quote2) quote2 = null;
6275
6342
  continue;
6276
6343
  }
6277
6344
  if (ch === '"' || ch === "'") {
6278
- quote = ch;
6345
+ quote2 = ch;
6279
6346
  continue;
6280
6347
  }
6281
6348
  if (ch === "\\") {
@@ -6451,7 +6518,6 @@ function removeScopedStyle(scopeId) {
6451
6518
  }
6452
6519
 
6453
6520
  // src/utils/setSafeAttribute.ts
6454
- var _isDev6 = isDev();
6455
6521
  var HTML_NS = "http://www.w3.org/1999/xhtml";
6456
6522
  var SVG_NS = "http://www.w3.org/2000/svg";
6457
6523
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -6474,7 +6540,7 @@ function setSafeAttribute(el, name, value, options = {}) {
6474
6540
  const ns = namespaceFor(el, name);
6475
6541
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
6476
6542
  if (isEventHandlerAttr(name)) {
6477
- if (_isDev6) {
6543
+ if (DEV) {
6478
6544
  devWarn(
6479
6545
  `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
6480
6546
  );
@@ -6489,7 +6555,7 @@ function setSafeAttribute(el, name, value, options = {}) {
6489
6555
  return true;
6490
6556
  }
6491
6557
  if (isHtmlContentAttribute(name)) {
6492
- if (_isDev6) {
6558
+ if (DEV) {
6493
6559
  devWarn(
6494
6560
  `${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.`
6495
6561
  );
@@ -6518,7 +6584,7 @@ function setSafeAttribute(el, name, value, options = {}) {
6518
6584
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
6519
6585
  return true;
6520
6586
  }
6521
- const safe = sanitizeAttributeString(name, str);
6587
+ const safe = sanitizeAttributeString(name, str, { element: el });
6522
6588
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
6523
6589
  if (current === safe) return true;
6524
6590
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -7090,7 +7156,6 @@ function uniqueId(suffix) {
7090
7156
  }
7091
7157
 
7092
7158
  // src/performance/domRecycler.ts
7093
- var _isDev7 = isDev();
7094
7159
  var DOMPool = class {
7095
7160
  constructor(maxSize = 50) {
7096
7161
  this.pools = /* @__PURE__ */ new Map();
@@ -7117,7 +7182,7 @@ var DOMPool = class {
7117
7182
  * is almost certainly a bug.
7118
7183
  */
7119
7184
  release(element) {
7120
- if (_isDev7 && element.isConnected) {
7185
+ if (DEV && element.isConnected) {
7121
7186
  devWarn(
7122
7187
  "DOMPool.release() called on a still-connected element. Detach it from the DOM first (remove() / dispose())."
7123
7188
  );
@@ -7262,9 +7327,9 @@ function parseMetaRefreshContent(content) {
7262
7327
  i2++;
7263
7328
  while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
7264
7329
  let destination;
7265
- const quote = raw[i2];
7266
- if (quote === '"' || quote === "'") {
7267
- const close = raw.indexOf(quote, i2 + 1);
7330
+ const quote2 = raw[i2];
7331
+ if (quote2 === '"' || quote2 === "'") {
7332
+ const close = raw.indexOf(quote2, i2 + 1);
7268
7333
  if (close === -1) return forbidden("unterminated quoted destination");
7269
7334
  destination = raw.slice(i2 + 1, close);
7270
7335
  const rest = trimAsciiWhitespace(raw.slice(close + 1));
@@ -7395,13 +7460,12 @@ function isForbiddenSsrAttr(lowerName) {
7395
7460
  function sanitizeSsrAttributeValue(lowerName, value) {
7396
7461
  return sanitizeAttributeString(lowerName, value);
7397
7462
  }
7398
- var _isDev8 = isDev();
7399
7463
  var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
7400
7464
  function isSafeAttrName(name) {
7401
7465
  return SAFE_ATTR_NAME.test(name);
7402
7466
  }
7403
7467
  function ssrErrorComment(err) {
7404
- if (_isDev8) {
7468
+ if (DEV) {
7405
7469
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
7406
7470
  return `<!--SSR error: ${safeCommentText(msg)}-->`;
7407
7471
  }
@@ -7447,10 +7511,10 @@ function renderToString(element) {
7447
7511
  }
7448
7512
  const tag = element.tagName.toLowerCase();
7449
7513
  if (tag === "script" || tag === "style") {
7450
- return _isDev8 ? `<!--ssr:${tag}-stripped-->` : "";
7514
+ return DEV ? `<!--ssr:${tag}-stripped-->` : "";
7451
7515
  }
7452
7516
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
7453
- return _isDev8 ? "<!--ssr:invalid-tag-->" : "";
7517
+ return DEV ? "<!--ssr:invalid-tag-->" : "";
7454
7518
  }
7455
7519
  let html2 = `<${tag}`;
7456
7520
  for (const attr of Array.from(element.attributes)) {
@@ -7492,7 +7556,7 @@ function hydrate(component, container, options = {}) {
7492
7556
  const first = mismatches[0];
7493
7557
  if (options.onMismatch) {
7494
7558
  options.onMismatch(first);
7495
- } else if (_isDev8) {
7559
+ } else if (DEV) {
7496
7560
  console.warn(
7497
7561
  `[SibuJS hydration] ${first.message}
7498
7562
  at ${first.path}
@@ -7693,11 +7757,11 @@ async function* renderToStream(element) {
7693
7757
  }
7694
7758
  const tag = element.tagName.toLowerCase();
7695
7759
  if (tag === "script" || tag === "style") {
7696
- if (_isDev8) yield `<!--ssr:${tag}-stripped-->`;
7760
+ if (DEV) yield `<!--ssr:${tag}-stripped-->`;
7697
7761
  return;
7698
7762
  }
7699
7763
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
7700
- if (_isDev8) yield "<!--ssr:invalid-tag-->";
7764
+ if (DEV) yield "<!--ssr:invalid-tag-->";
7701
7765
  return;
7702
7766
  }
7703
7767
  let openTag = `<${tag}`;
@@ -7840,7 +7904,7 @@ function ssrSuspense(props) {
7840
7904
  },
7841
7905
  (err) => {
7842
7906
  if (timer) clearTimeout(timer);
7843
- if (_isDev8) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
7907
+ if (DEV) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
7844
7908
  return { id, html: fallbackHtml };
7845
7909
  }
7846
7910
  );
@@ -7886,7 +7950,7 @@ function serializeState(state2, nonce, options) {
7886
7950
  }
7887
7951
  function deserializeState(validate) {
7888
7952
  if (typeof window === "undefined") return void 0;
7889
- if (_isDev8 && !validate) {
7953
+ if (DEV && !validate) {
7890
7954
  console.warn(
7891
7955
  "[SibuJS SSR] deserializeState() called without a validate guard \u2014 tampered SSR payloads will not be detected."
7892
7956
  );
@@ -8906,8 +8970,101 @@ function isWasmCached(key) {
8906
8970
  return moduleCache.has(key);
8907
8971
  }
8908
8972
 
8973
+ // src/core/rendering/focusPreservation.ts
8974
+ var IDENTITY_ATTRS = ["data-focus-key", "id", "name"];
8975
+ var SELECTABLE_TYPES = /* @__PURE__ */ new Set(["text", "search", "url", "tel", "password", ""]);
8976
+ function selectionOf(el) {
8977
+ const tag = el.tagName;
8978
+ const selectable = tag === "TEXTAREA" || tag === "INPUT" && SELECTABLE_TYPES.has(el.type.toLowerCase());
8979
+ if (!selectable) return { start: null, end: null, direction: null };
8980
+ const field = el;
8981
+ return { start: field.selectionStart, end: field.selectionEnd, direction: field.selectionDirection };
8982
+ }
8983
+ function captureFocusWithin(nodes) {
8984
+ if (nodes.length === 0 || typeof document === "undefined") return null;
8985
+ const active = document.activeElement;
8986
+ if (!active || active === document.body) return null;
8987
+ let inside = false;
8988
+ for (let i2 = 0; i2 < nodes.length; i2++) {
8989
+ const n = nodes[i2];
8990
+ if (n === active || n.nodeType === 1 && n.contains(active)) {
8991
+ inside = true;
8992
+ break;
8993
+ }
8994
+ }
8995
+ if (!inside) return null;
8996
+ let attr = null;
8997
+ let value = null;
8998
+ for (let i2 = 0; i2 < IDENTITY_ATTRS.length; i2++) {
8999
+ const candidate = active.getAttribute(IDENTITY_ATTRS[i2]);
9000
+ if (candidate) {
9001
+ attr = IDENTITY_ATTRS[i2];
9002
+ value = candidate;
9003
+ break;
9004
+ }
9005
+ }
9006
+ return { el: active, attr, value, tag: active.tagName, ...selectionOf(active) };
9007
+ }
9008
+ function quote(value) {
9009
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
9010
+ }
9011
+ function findUniqueByIdentity(nodes, selector) {
9012
+ let el = null;
9013
+ let count = 0;
9014
+ for (let i2 = 0; i2 < nodes.length; i2++) {
9015
+ const n = nodes[i2];
9016
+ if (n.nodeType !== 1) continue;
9017
+ const root = n;
9018
+ if (root.matches(selector)) {
9019
+ count++;
9020
+ el ?? (el = root);
9021
+ }
9022
+ const inner = root.querySelectorAll(selector);
9023
+ for (let j = 0; j < inner.length; j++) {
9024
+ count++;
9025
+ el ?? (el = inner[j]);
9026
+ }
9027
+ if (count > 1) return { el: null, count };
9028
+ }
9029
+ return count === 1 ? { el, count } : { el: null, count };
9030
+ }
9031
+ function refocus(el, snapshot) {
9032
+ el.focus();
9033
+ if (snapshot.start !== null && typeof el.setSelectionRange === "function") {
9034
+ const field = el;
9035
+ const max2 = field.value.length;
9036
+ const start = Math.min(snapshot.start, max2);
9037
+ const end = Math.min(snapshot.end ?? start, max2);
9038
+ field.setSelectionRange(start, end, snapshot.direction ?? "none");
9039
+ }
9040
+ }
9041
+ function restoreFocusWithin(snapshot, nodes, where) {
9042
+ if (!snapshot) return;
9043
+ if (typeof document === "undefined") return;
9044
+ const active = document.activeElement;
9045
+ if (active === snapshot.el) return;
9046
+ if (active && active !== document.body && active !== document.documentElement) return;
9047
+ if (snapshot.el.isConnected) {
9048
+ refocus(snapshot.el, snapshot);
9049
+ return;
9050
+ }
9051
+ let ambiguous = false;
9052
+ if (snapshot.attr && snapshot.value) {
9053
+ const { el: replacement, count } = findUniqueByIdentity(nodes, `[${snapshot.attr}=${quote(snapshot.value)}]`);
9054
+ if (replacement && replacement.tagName === snapshot.tag) {
9055
+ refocus(replacement, snapshot);
9056
+ return;
9057
+ }
9058
+ ambiguous = count > 1;
9059
+ }
9060
+ devWarnLazy(() => {
9061
+ const id = snapshot.attr ? ` (${snapshot.attr}="${snapshot.value}")` : "";
9062
+ 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";
9063
+ 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.`;
9064
+ });
9065
+ }
9066
+
8909
9067
  // src/reactivity/bindChildNode.ts
8910
- var _isDev9 = isDev();
8911
9068
  function bindChildNode(placeholder, getter) {
8912
9069
  let lastNodes = [];
8913
9070
  function commit() {
@@ -8918,6 +9075,7 @@ function bindChildNode(placeholder, getter) {
8918
9075
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
8919
9076
  return;
8920
9077
  }
9078
+ const focused = captureFocusWithin(lastNodes);
8921
9079
  if (result == null || typeof result === "boolean") {
8922
9080
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
8923
9081
  const node = lastNodes[i2];
@@ -8925,6 +9083,7 @@ function bindChildNode(placeholder, getter) {
8925
9083
  if (node.parentNode) node.parentNode.removeChild(node);
8926
9084
  }
8927
9085
  lastNodes.length = 0;
9086
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
8928
9087
  return;
8929
9088
  }
8930
9089
  const parent = placeholder.parentNode;
@@ -8941,8 +9100,7 @@ function bindChildNode(placeholder, getter) {
8941
9100
  if (item == null || typeof item === "boolean") continue;
8942
9101
  const node = item instanceof Node ? item : document.createTextNode(String(item));
8943
9102
  if (seen.has(node)) {
8944
- if (_isDev9)
8945
- devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
9103
+ if (DEV) devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
8946
9104
  continue;
8947
9105
  }
8948
9106
  seen.add(node);
@@ -8975,13 +9133,13 @@ function bindChildNode(placeholder, getter) {
8975
9133
  prev = node;
8976
9134
  }
8977
9135
  lastNodes = newNodes;
9136
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
8978
9137
  }
8979
9138
  return reactiveBinding2(commit, placeholder);
8980
9139
  }
8981
9140
 
8982
9141
  // src/core/rendering/tagFactory.ts
8983
9142
  var SVG_NS3 = "http://www.w3.org/2000/svg";
8984
- var _isDev10 = isDev();
8985
9143
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
8986
9144
  function isBlockedTag(tag) {
8987
9145
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -8999,18 +9157,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
8999
9157
  "self",
9000
9158
  "frames"
9001
9159
  ]);
9002
- function looksLikeClassList(s2) {
9003
- const t = s2.trim();
9004
- if (!t) return false;
9005
- const tokens = t.split(/\s+/);
9006
- let sawClassish = false;
9007
- for (let i2 = 0; i2 < tokens.length; i2++) {
9008
- const tok = tokens[i2];
9009
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
9010
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
9011
- }
9012
- return sawClassish;
9013
- }
9160
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
9161
+ var MAX_WARNED_LONE_STRINGS = 100;
9162
+ var loneStringCapAnnounced = false;
9014
9163
  var kebabCache = /* @__PURE__ */ new Map();
9015
9164
  function toKebab(prop) {
9016
9165
  let cached = kebabCache.get(prop);
@@ -9022,13 +9171,13 @@ function toKebab(prop) {
9022
9171
  function applyStyle(el, style2) {
9023
9172
  if (typeof style2 === "function") {
9024
9173
  const teardown = reactiveBinding2(() => {
9025
- el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
9174
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2()), { element: el }));
9026
9175
  }, el);
9027
9176
  registerDisposer(el, teardown);
9028
9177
  return;
9029
9178
  }
9030
9179
  if (typeof style2 === "string") {
9031
- el.setAttribute("style", sanitizeStyleAttribute(style2));
9180
+ el.setAttribute("style", sanitizeStyleAttribute(style2, { element: el }));
9032
9181
  return;
9033
9182
  }
9034
9183
  const htmlEl = el;
@@ -9038,11 +9187,11 @@ function applyStyle(el, style2) {
9038
9187
  if (typeof val === "function") {
9039
9188
  const getter = val;
9040
9189
  const teardown = reactiveBinding2(() => {
9041
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
9190
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
9042
9191
  }, el);
9043
9192
  registerDisposer(el, teardown);
9044
9193
  } else {
9045
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
9194
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
9046
9195
  }
9047
9196
  }
9048
9197
  }
@@ -9149,11 +9298,26 @@ var tagFactory = (tag, ns) => {
9149
9298
  appendChildren(el, second);
9150
9299
  return el;
9151
9300
  }
9152
- if (_isDev10 && looksLikeClassList(first)) {
9153
- devWarn(
9154
- `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.`
9155
- );
9156
- }
9301
+ devWarnLazy(() => {
9302
+ const tokens = first.trim().split(/\s+/);
9303
+ if (tokens.length < 2) return "";
9304
+ let utilityTokens = 0;
9305
+ for (let i2 = 0; i2 < tokens.length; i2++) {
9306
+ const tok = tokens[i2];
9307
+ if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return "";
9308
+ if (/[-:/0-9]/.test(tok)) utilityTokens++;
9309
+ }
9310
+ if (utilityTokens < 2) return "";
9311
+ const key = `${tag}|${first}`;
9312
+ if (warnedLoneStrings.has(key)) return "";
9313
+ if (warnedLoneStrings.size >= MAX_WARNED_LONE_STRINGS) {
9314
+ if (loneStringCapAnnounced) return "";
9315
+ loneStringCapAnnounced = true;
9316
+ 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.`;
9317
+ }
9318
+ warnedLoneStrings.add(key);
9319
+ 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.`;
9320
+ });
9157
9321
  el.textContent = first;
9158
9322
  return el;
9159
9323
  }
@@ -9170,7 +9334,7 @@ var tagFactory = (tag, ns) => {
9170
9334
  if (pClass != null) applyClass(el, pClass);
9171
9335
  const pId = props.id;
9172
9336
  if (pId != null) {
9173
- if (_isDev10 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
9337
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
9174
9338
  devWarn(
9175
9339
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
9176
9340
  );
@@ -9185,7 +9349,7 @@ var tagFactory = (tag, ns) => {
9185
9349
  const handler = pOn[ev];
9186
9350
  if (typeof handler === "function") {
9187
9351
  el.addEventListener(ev, handler);
9188
- } else if (_isDev10) {
9352
+ } else if (DEV) {
9189
9353
  devWarn(
9190
9354
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
9191
9355
  );