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/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
  }
@@ -883,17 +888,16 @@ function forEachSubscriber(signal2, visit) {
883
888
  }
884
889
 
885
890
  // src/reactivity/track.ts
886
- var _isDev2 = isDev();
887
- var _runtimeVersion = true ? "4.1.0" : "dev";
891
+ var _runtimeVersion = true ? "4.2.0" : "dev";
888
892
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
889
893
  function resolveReactiveApi() {
890
894
  const g2 = globalThis;
891
895
  const existing = g2[REGISTRY_KEY];
892
896
  if (existing) {
893
- if (_isDev2 && !existing.__dupWarned) {
897
+ if (DEV && !existing.__dupWarned) {
894
898
  existing.__dupWarned = true;
895
899
  devWarn(
896
- `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.`
897
901
  );
898
902
  }
899
903
  return existing;
@@ -1239,7 +1243,6 @@ var isBatching = API2.isBatching;
1239
1243
 
1240
1244
  // src/core/signals/signal.ts
1241
1245
  var _g2 = globalThis;
1242
- var _isDev3 = isDev();
1243
1246
  function signal(initial, options) {
1244
1247
  const state2 = {
1245
1248
  value: initial,
@@ -1252,7 +1255,7 @@ function signal(initial, options) {
1252
1255
  _d: false,
1253
1256
  _validate: void 0
1254
1257
  };
1255
- const debugName = _isDev3 ? options?.name : void 0;
1258
+ const debugName = DEV ? options?.name : void 0;
1256
1259
  const equalsFn = options?.equals;
1257
1260
  if (debugName) state2.__name = debugName;
1258
1261
  function get() {
@@ -1269,7 +1272,7 @@ function signal(initial, options) {
1269
1272
  if (equalsFn(prev, newValue)) return;
1270
1273
  state2.value = newValue;
1271
1274
  state2.__v++;
1272
- if (_isDev3) {
1275
+ if (DEV) {
1273
1276
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1274
1277
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1275
1278
  }
@@ -1277,7 +1280,7 @@ function signal(initial, options) {
1277
1280
  notifySubscribers2(state2);
1278
1281
  }
1279
1282
  };
1280
- } else if (_isDev3) {
1283
+ } else if (DEV) {
1281
1284
  set = (next) => {
1282
1285
  const prev = state2.value;
1283
1286
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -1302,7 +1305,7 @@ function signal(initial, options) {
1302
1305
  }
1303
1306
  };
1304
1307
  }
1305
- if (_isDev3) {
1308
+ if (DEV) {
1306
1309
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1307
1310
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
1308
1311
  }
@@ -2439,11 +2442,10 @@ function syncAdapter(config) {
2439
2442
  }
2440
2443
 
2441
2444
  // src/core/rendering/context.ts
2442
- var _isDev4 = isDev();
2443
2445
  function context(defaultValue) {
2444
2446
  const [getValue, setValue] = signal(defaultValue);
2445
2447
  const warnIfSSR = (method) => {
2446
- if (!_isDev4 || !isSSR()) return;
2448
+ if (!DEV || !isSSR()) return;
2447
2449
  devWarn(
2448
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.`
2449
2451
  );
@@ -2471,7 +2473,7 @@ function context(defaultValue) {
2471
2473
  setValue(value);
2472
2474
  try {
2473
2475
  const result = fn();
2474
- if (_isDev4 && result !== null && typeof result?.then === "function") {
2476
+ if (DEV && result !== null && typeof result?.then === "function") {
2475
2477
  devWarn(
2476
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."
2477
2479
  );
@@ -2724,20 +2726,75 @@ function decodeCssEscapes(value) {
2724
2726
  }
2725
2727
  return out;
2726
2728
  }
2727
- function sanitizeCSSValue(value) {
2728
- 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;
2729
2780
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
2730
2781
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
2731
- 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")) {
2732
- return "";
2782
+ for (let i2 = 0; i2 < BLOCKED_CSS_NEEDLES.length; i2++) {
2783
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i2])) return i2;
2733
2784
  }
2734
- return value;
2785
+ return -1;
2735
2786
  }
2736
- 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) {
2737
2794
  const input2 = String(cssText);
2738
2795
  if (input2.trim() === "") return "";
2739
2796
  if (typeof document === "undefined") {
2740
- return sanitizeCSSValue(input2) === "" ? "" : input2;
2797
+ return sanitizeCSSValue(input2, context2) === "" ? "" : input2;
2741
2798
  }
2742
2799
  const probe = document.createElement("div");
2743
2800
  try {
@@ -2749,8 +2806,12 @@ function sanitizeStyleAttribute(cssText) {
2749
2806
  for (let i2 = 0; i2 < probe.style.length; i2++) {
2750
2807
  const property = probe.style[i2];
2751
2808
  const value = probe.style.getPropertyValue(property);
2752
- if (sanitizeCSSValue(value) === "") continue;
2753
- 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
+ }
2754
2815
  const priority = probe.style.getPropertyPriority(property);
2755
2816
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
2756
2817
  }
@@ -2798,11 +2859,11 @@ function isPolicyAttribute(name) {
2798
2859
  const lower = name.toLowerCase();
2799
2860
  return lower === "style" || URL_ATTRIBUTES.has(lower);
2800
2861
  }
2801
- function sanitizeAttributeString(attr, value) {
2862
+ function sanitizeAttributeString(attr, value, context2) {
2802
2863
  const lower = attr.toLowerCase();
2803
2864
  if (lower === "srcset") return sanitizeSrcset(value);
2804
2865
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
2805
- if (lower === "style") return sanitizeStyleAttribute(value);
2866
+ if (lower === "style") return sanitizeStyleAttribute(value, context2);
2806
2867
  return value;
2807
2868
  }
2808
2869
 
@@ -5118,7 +5179,6 @@ function reportDrainRunaway(label2, executed, remaining) {
5118
5179
  { executed, remaining }
5119
5180
  );
5120
5181
  }
5121
- var _isDev5 = isDev();
5122
5182
  var activeBindingCount = 0;
5123
5183
  function registerDisposer(node, teardown) {
5124
5184
  let disposers = elementDisposers.get(node);
@@ -5127,7 +5187,7 @@ function registerDisposer(node, teardown) {
5127
5187
  elementDisposers.set(node, disposers);
5128
5188
  }
5129
5189
  disposers.push(teardown);
5130
- if (_isDev5) activeBindingCount++;
5190
+ if (DEV) activeBindingCount++;
5131
5191
  }
5132
5192
  function dispose(node) {
5133
5193
  const stack = [node];
@@ -5150,13 +5210,13 @@ function dispose(node) {
5150
5210
  if (!pending || pending.length === 0) break;
5151
5211
  const snapshot = pending.slice();
5152
5212
  elementDisposers.delete(current);
5153
- if (_isDev5) activeBindingCount -= snapshot.length;
5213
+ if (DEV) activeBindingCount -= snapshot.length;
5154
5214
  for (let i3 = 0; i3 < snapshot.length; i3++) {
5155
5215
  if (executed >= MAX_DRAIN_TEARDOWNS) {
5156
5216
  const rest = snapshot.slice(i3);
5157
5217
  const added = elementDisposers.get(current);
5158
5218
  elementDisposers.set(current, added ? rest.concat(added) : rest);
5159
- if (_isDev5) activeBindingCount += rest.length;
5219
+ if (DEV) activeBindingCount += rest.length;
5160
5220
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
5161
5221
  runaway = true;
5162
5222
  break;
@@ -6273,16 +6333,16 @@ function sanitizeCSS(css) {
6273
6333
  function firstCompoundEnd(selector) {
6274
6334
  let parens = 0;
6275
6335
  let brackets = 0;
6276
- let quote = null;
6336
+ let quote2 = null;
6277
6337
  for (let i2 = 0; i2 < selector.length; i2++) {
6278
6338
  const ch = selector[i2];
6279
- if (quote) {
6339
+ if (quote2) {
6280
6340
  if (ch === "\\") i2++;
6281
- else if (ch === quote) quote = null;
6341
+ else if (ch === quote2) quote2 = null;
6282
6342
  continue;
6283
6343
  }
6284
6344
  if (ch === '"' || ch === "'") {
6285
- quote = ch;
6345
+ quote2 = ch;
6286
6346
  continue;
6287
6347
  }
6288
6348
  if (ch === "\\") {
@@ -6458,7 +6518,6 @@ function removeScopedStyle(scopeId) {
6458
6518
  }
6459
6519
 
6460
6520
  // src/utils/setSafeAttribute.ts
6461
- var _isDev6 = isDev();
6462
6521
  var HTML_NS = "http://www.w3.org/1999/xhtml";
6463
6522
  var SVG_NS = "http://www.w3.org/2000/svg";
6464
6523
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -6481,7 +6540,7 @@ function setSafeAttribute(el, name, value, options = {}) {
6481
6540
  const ns = namespaceFor(el, name);
6482
6541
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
6483
6542
  if (isEventHandlerAttr(name)) {
6484
- if (_isDev6) {
6543
+ if (DEV) {
6485
6544
  devWarn(
6486
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.`
6487
6546
  );
@@ -6496,7 +6555,7 @@ function setSafeAttribute(el, name, value, options = {}) {
6496
6555
  return true;
6497
6556
  }
6498
6557
  if (isHtmlContentAttribute(name)) {
6499
- if (_isDev6) {
6558
+ if (DEV) {
6500
6559
  devWarn(
6501
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.`
6502
6561
  );
@@ -6525,7 +6584,7 @@ function setSafeAttribute(el, name, value, options = {}) {
6525
6584
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
6526
6585
  return true;
6527
6586
  }
6528
- const safe = sanitizeAttributeString(name, str);
6587
+ const safe = sanitizeAttributeString(name, str, { element: el });
6529
6588
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
6530
6589
  if (current === safe) return true;
6531
6590
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -7097,7 +7156,6 @@ function uniqueId(suffix) {
7097
7156
  }
7098
7157
 
7099
7158
  // src/performance/domRecycler.ts
7100
- var _isDev7 = isDev();
7101
7159
  var DOMPool = class {
7102
7160
  constructor(maxSize = 50) {
7103
7161
  this.pools = /* @__PURE__ */ new Map();
@@ -7124,7 +7182,7 @@ var DOMPool = class {
7124
7182
  * is almost certainly a bug.
7125
7183
  */
7126
7184
  release(element) {
7127
- if (_isDev7 && element.isConnected) {
7185
+ if (DEV && element.isConnected) {
7128
7186
  devWarn(
7129
7187
  "DOMPool.release() called on a still-connected element. Detach it from the DOM first (remove() / dispose())."
7130
7188
  );
@@ -7269,9 +7327,9 @@ function parseMetaRefreshContent(content) {
7269
7327
  i2++;
7270
7328
  while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
7271
7329
  let destination;
7272
- const quote = raw[i2];
7273
- if (quote === '"' || quote === "'") {
7274
- const close = raw.indexOf(quote, i2 + 1);
7330
+ const quote2 = raw[i2];
7331
+ if (quote2 === '"' || quote2 === "'") {
7332
+ const close = raw.indexOf(quote2, i2 + 1);
7275
7333
  if (close === -1) return forbidden("unterminated quoted destination");
7276
7334
  destination = raw.slice(i2 + 1, close);
7277
7335
  const rest = trimAsciiWhitespace(raw.slice(close + 1));
@@ -7402,13 +7460,12 @@ function isForbiddenSsrAttr(lowerName) {
7402
7460
  function sanitizeSsrAttributeValue(lowerName, value) {
7403
7461
  return sanitizeAttributeString(lowerName, value);
7404
7462
  }
7405
- var _isDev8 = isDev();
7406
7463
  var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
7407
7464
  function isSafeAttrName(name) {
7408
7465
  return SAFE_ATTR_NAME.test(name);
7409
7466
  }
7410
7467
  function ssrErrorComment(err) {
7411
- if (_isDev8) {
7468
+ if (DEV) {
7412
7469
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
7413
7470
  return `<!--SSR error: ${safeCommentText(msg)}-->`;
7414
7471
  }
@@ -7454,10 +7511,10 @@ function renderToString(element) {
7454
7511
  }
7455
7512
  const tag = element.tagName.toLowerCase();
7456
7513
  if (tag === "script" || tag === "style") {
7457
- return _isDev8 ? `<!--ssr:${tag}-stripped-->` : "";
7514
+ return DEV ? `<!--ssr:${tag}-stripped-->` : "";
7458
7515
  }
7459
7516
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
7460
- return _isDev8 ? "<!--ssr:invalid-tag-->" : "";
7517
+ return DEV ? "<!--ssr:invalid-tag-->" : "";
7461
7518
  }
7462
7519
  let html2 = `<${tag}`;
7463
7520
  for (const attr of Array.from(element.attributes)) {
@@ -7499,7 +7556,7 @@ function hydrate(component, container, options = {}) {
7499
7556
  const first = mismatches[0];
7500
7557
  if (options.onMismatch) {
7501
7558
  options.onMismatch(first);
7502
- } else if (_isDev8) {
7559
+ } else if (DEV) {
7503
7560
  console.warn(
7504
7561
  `[SibuJS hydration] ${first.message}
7505
7562
  at ${first.path}
@@ -7700,11 +7757,11 @@ async function* renderToStream(element) {
7700
7757
  }
7701
7758
  const tag = element.tagName.toLowerCase();
7702
7759
  if (tag === "script" || tag === "style") {
7703
- if (_isDev8) yield `<!--ssr:${tag}-stripped-->`;
7760
+ if (DEV) yield `<!--ssr:${tag}-stripped-->`;
7704
7761
  return;
7705
7762
  }
7706
7763
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
7707
- if (_isDev8) yield "<!--ssr:invalid-tag-->";
7764
+ if (DEV) yield "<!--ssr:invalid-tag-->";
7708
7765
  return;
7709
7766
  }
7710
7767
  let openTag = `<${tag}`;
@@ -7847,7 +7904,7 @@ function ssrSuspense(props) {
7847
7904
  },
7848
7905
  (err) => {
7849
7906
  if (timer) clearTimeout(timer);
7850
- if (_isDev8) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
7907
+ if (DEV) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
7851
7908
  return { id, html: fallbackHtml };
7852
7909
  }
7853
7910
  );
@@ -7893,7 +7950,7 @@ function serializeState(state2, nonce, options) {
7893
7950
  }
7894
7951
  function deserializeState(validate) {
7895
7952
  if (typeof window === "undefined") return void 0;
7896
- if (_isDev8 && !validate) {
7953
+ if (DEV && !validate) {
7897
7954
  console.warn(
7898
7955
  "[SibuJS SSR] deserializeState() called without a validate guard \u2014 tampered SSR payloads will not be detected."
7899
7956
  );
@@ -8913,8 +8970,101 @@ function isWasmCached(key) {
8913
8970
  return moduleCache.has(key);
8914
8971
  }
8915
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
+
8916
9067
  // src/reactivity/bindChildNode.ts
8917
- var _isDev9 = isDev();
8918
9068
  function bindChildNode(placeholder, getter) {
8919
9069
  let lastNodes = [];
8920
9070
  function commit() {
@@ -8925,6 +9075,7 @@ function bindChildNode(placeholder, getter) {
8925
9075
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
8926
9076
  return;
8927
9077
  }
9078
+ const focused = captureFocusWithin(lastNodes);
8928
9079
  if (result == null || typeof result === "boolean") {
8929
9080
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
8930
9081
  const node = lastNodes[i2];
@@ -8932,6 +9083,7 @@ function bindChildNode(placeholder, getter) {
8932
9083
  if (node.parentNode) node.parentNode.removeChild(node);
8933
9084
  }
8934
9085
  lastNodes.length = 0;
9086
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
8935
9087
  return;
8936
9088
  }
8937
9089
  const parent = placeholder.parentNode;
@@ -8948,8 +9100,7 @@ function bindChildNode(placeholder, getter) {
8948
9100
  if (item == null || typeof item === "boolean") continue;
8949
9101
  const node = item instanceof Node ? item : document.createTextNode(String(item));
8950
9102
  if (seen.has(node)) {
8951
- if (_isDev9)
8952
- 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.");
8953
9104
  continue;
8954
9105
  }
8955
9106
  seen.add(node);
@@ -8982,13 +9133,13 @@ function bindChildNode(placeholder, getter) {
8982
9133
  prev = node;
8983
9134
  }
8984
9135
  lastNodes = newNodes;
9136
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
8985
9137
  }
8986
9138
  return reactiveBinding2(commit, placeholder);
8987
9139
  }
8988
9140
 
8989
9141
  // src/core/rendering/tagFactory.ts
8990
9142
  var SVG_NS3 = "http://www.w3.org/2000/svg";
8991
- var _isDev10 = isDev();
8992
9143
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
8993
9144
  function isBlockedTag(tag) {
8994
9145
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -9006,18 +9157,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
9006
9157
  "self",
9007
9158
  "frames"
9008
9159
  ]);
9009
- function looksLikeClassList(s2) {
9010
- const t = s2.trim();
9011
- if (!t) return false;
9012
- const tokens = t.split(/\s+/);
9013
- let sawClassish = false;
9014
- for (let i2 = 0; i2 < tokens.length; i2++) {
9015
- const tok = tokens[i2];
9016
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
9017
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
9018
- }
9019
- return sawClassish;
9020
- }
9160
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
9161
+ var MAX_WARNED_LONE_STRINGS = 100;
9162
+ var loneStringCapAnnounced = false;
9021
9163
  var kebabCache = /* @__PURE__ */ new Map();
9022
9164
  function toKebab(prop) {
9023
9165
  let cached = kebabCache.get(prop);
@@ -9029,13 +9171,13 @@ function toKebab(prop) {
9029
9171
  function applyStyle(el, style2) {
9030
9172
  if (typeof style2 === "function") {
9031
9173
  const teardown = reactiveBinding2(() => {
9032
- el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
9174
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2()), { element: el }));
9033
9175
  }, el);
9034
9176
  registerDisposer(el, teardown);
9035
9177
  return;
9036
9178
  }
9037
9179
  if (typeof style2 === "string") {
9038
- el.setAttribute("style", sanitizeStyleAttribute(style2));
9180
+ el.setAttribute("style", sanitizeStyleAttribute(style2, { element: el }));
9039
9181
  return;
9040
9182
  }
9041
9183
  const htmlEl = el;
@@ -9045,11 +9187,11 @@ function applyStyle(el, style2) {
9045
9187
  if (typeof val === "function") {
9046
9188
  const getter = val;
9047
9189
  const teardown = reactiveBinding2(() => {
9048
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
9190
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
9049
9191
  }, el);
9050
9192
  registerDisposer(el, teardown);
9051
9193
  } else {
9052
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
9194
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
9053
9195
  }
9054
9196
  }
9055
9197
  }
@@ -9156,11 +9298,26 @@ var tagFactory = (tag, ns) => {
9156
9298
  appendChildren(el, second);
9157
9299
  return el;
9158
9300
  }
9159
- if (_isDev10 && looksLikeClassList(first)) {
9160
- devWarn(
9161
- `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.`
9162
- );
9163
- }
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
+ });
9164
9321
  el.textContent = first;
9165
9322
  return el;
9166
9323
  }
@@ -9177,7 +9334,7 @@ var tagFactory = (tag, ns) => {
9177
9334
  if (pClass != null) applyClass(el, pClass);
9178
9335
  const pId = props.id;
9179
9336
  if (pId != null) {
9180
- if (_isDev10 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
9337
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
9181
9338
  devWarn(
9182
9339
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
9183
9340
  );
@@ -9192,7 +9349,7 @@ var tagFactory = (tag, ns) => {
9192
9349
  const handler = pOn[ev];
9193
9350
  if (typeof handler === "function") {
9194
9351
  el.addEventListener(ev, handler);
9195
- } else if (_isDev10) {
9352
+ } else if (DEV) {
9196
9353
  devWarn(
9197
9354
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
9198
9355
  );