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/plugins.cjs CHANGED
@@ -22,29 +22,34 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
 
23
23
  // src/core/dev.ts
24
24
  function isDev() {
25
- return typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
26
- // The bare `__SIBU_DEV__` is a bundler define that only exists in
27
- // production builds; under the test runner it is always undefined, so
28
- // this branch is unreachable here.
29
- /* v8 ignore next 2 */
30
- typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
31
- );
25
+ if (typeof globalThis.__SIBU_DEV__ !== "undefined") return !!globalThis.__SIBU_DEV__;
26
+ if (typeof __SIBU_DEV__ !== "undefined") return !!__SIBU_DEV__;
27
+ return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
32
28
  }
33
29
  function devAssert(condition, message) {
34
- if (_isDev && !condition) {
30
+ if ((typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) && !condition) {
35
31
  throw new Error(`[SibuJS] ${message}`);
36
32
  }
37
33
  }
34
+ function devWarnLazy(build) {
35
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
36
+ const message = build();
37
+ if (message) console.warn(`[SibuJS] ${message}`);
38
+ }
39
+ }
38
40
  function devWarn(message) {
39
- if (_isDev) {
41
+ if (typeof __SIBU_DEV__ !== "undefined" ? __SIBU_DEV__ : DEV) {
40
42
  console.warn(`[SibuJS] ${message}`);
41
43
  }
42
44
  }
43
- var _isDev;
45
+ var DEV;
44
46
  var init_dev = __esm({
45
47
  "src/core/dev.ts"() {
46
48
  "use strict";
47
- _isDev = isDev();
49
+ DEV = typeof __SIBU_DEV__ !== "undefined" ? !!__SIBU_DEV__ : typeof globalThis.__SIBU_DEV__ !== "undefined" ? !!globalThis.__SIBU_DEV__ : (
50
+ // safe default: off in browser, on in test/dev Node
51
+ typeof process !== "undefined" && process.env?.NODE_ENV !== "production"
52
+ );
48
53
  }
49
54
  });
50
55
 
@@ -234,20 +239,61 @@ function decodeCssEscapes(value) {
234
239
  }
235
240
  return out;
236
241
  }
237
- function sanitizeCSSValue(value) {
238
- if (!CSS_DANGER_GATE.test(value)) return value;
242
+ function describeElement(el) {
243
+ if (!el) return "an element";
244
+ let out = el.tagName.toLowerCase();
245
+ const id = el.getAttribute("id");
246
+ if (id) out += `#${id}`;
247
+ const cls = el.getAttribute("class");
248
+ if (cls) out += `.${cls.trim().split(/\s+/).slice(0, 3).join(".")}`;
249
+ return `<${out}>`;
250
+ }
251
+ function warnDroppedDeclaration(value, reason, ctx) {
252
+ devWarnLazy(() => {
253
+ const reasons = [
254
+ "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",
255
+ "expression() executes arbitrary script in legacy engines",
256
+ "the javascript: scheme executes script",
257
+ "the vbscript: scheme executes script",
258
+ "-moz-binding loads and runs remote XBL",
259
+ "behavior: loads and runs a remote HTC script",
260
+ "@import pulls in a remote stylesheet",
261
+ "image-set() fetches remote images, the same exfiltration channel as url()",
262
+ "filter:progid activates legacy scriptable filters"
263
+ ];
264
+ const declaration = ctx?.property ? `${ctx.property}: ${value}` : value;
265
+ const where = describeElement(ctx?.element);
266
+ const key = `${where}|${declaration}`;
267
+ if (warnedDeclarations.has(key)) return "";
268
+ if (warnedDeclarations.size >= MAX_WARNED_DECLARATIONS) {
269
+ if (declarationCapAnnounced) return "";
270
+ declarationCapAnnounced = true;
271
+ 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.`;
272
+ }
273
+ warnedDeclarations.add(key);
274
+ 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.`;
275
+ });
276
+ }
277
+ function blockedCssReason(value) {
278
+ if (!CSS_DANGER_GATE.test(value)) return -1;
239
279
  const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
240
280
  const lower = normalized.toLowerCase().replace(/\s+/g, "");
241
- 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")) {
242
- return "";
281
+ for (let i2 = 0; i2 < BLOCKED_CSS_NEEDLES.length; i2++) {
282
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i2])) return i2;
243
283
  }
244
- return value;
284
+ return -1;
285
+ }
286
+ function sanitizeCSSValue(value, context) {
287
+ const reason = blockedCssReason(value);
288
+ if (reason === -1) return value;
289
+ warnDroppedDeclaration(value, reason, context);
290
+ return "";
245
291
  }
246
- function sanitizeStyleAttribute(cssText) {
292
+ function sanitizeStyleAttribute(cssText, context) {
247
293
  const input2 = String(cssText);
248
294
  if (input2.trim() === "") return "";
249
295
  if (typeof document === "undefined") {
250
- return sanitizeCSSValue(input2) === "" ? "" : input2;
296
+ return sanitizeCSSValue(input2, context) === "" ? "" : input2;
251
297
  }
252
298
  const probe = document.createElement("div");
253
299
  try {
@@ -259,8 +305,12 @@ function sanitizeStyleAttribute(cssText) {
259
305
  for (let i2 = 0; i2 < probe.style.length; i2++) {
260
306
  const property = probe.style[i2];
261
307
  const value = probe.style.getPropertyValue(property);
262
- if (sanitizeCSSValue(value) === "") continue;
263
- if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
308
+ let reason = blockedCssReason(value);
309
+ if (reason === -1) reason = blockedCssReason(`${property}:${value}`);
310
+ if (reason !== -1) {
311
+ warnDroppedDeclaration(value, reason, { property, element: context?.element });
312
+ continue;
313
+ }
264
314
  const priority = probe.style.getPropertyPriority(property);
265
315
  declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
266
316
  }
@@ -276,21 +326,36 @@ function isPolicyAttribute(name) {
276
326
  const lower = name.toLowerCase();
277
327
  return lower === "style" || URL_ATTRIBUTES.has(lower);
278
328
  }
279
- function sanitizeAttributeString(attr, value) {
329
+ function sanitizeAttributeString(attr, value, context) {
280
330
  const lower = attr.toLowerCase();
281
331
  if (lower === "srcset") return sanitizeSrcset(value);
282
332
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
283
- if (lower === "style") return sanitizeStyleAttribute(value);
333
+ if (lower === "style") return sanitizeStyleAttribute(value, context);
284
334
  return value;
285
335
  }
286
- var SAFE_URL_PROTOCOLS, OUTER_TRIM, SRCSET_DESCRIPTOR, CSS_DANGER_GATE, URL_ATTRIBUTES, HTML_CONTENT_ATTRIBUTES;
336
+ var SAFE_URL_PROTOCOLS, OUTER_TRIM, SRCSET_DESCRIPTOR, CSS_DANGER_GATE, BLOCKED_CSS_NEEDLES, warnedDeclarations, MAX_WARNED_DECLARATIONS, declarationCapAnnounced, URL_ATTRIBUTES, HTML_CONTENT_ATTRIBUTES;
287
337
  var init_sanitize = __esm({
288
338
  "src/utils/sanitize.ts"() {
289
339
  "use strict";
340
+ init_dev();
290
341
  SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
291
342
  OUTER_TRIM = /^[\x00-\x20\x7f-\x9f]+|[\x00-\x20\x7f-\x9f]+$/g;
292
343
  SRCSET_DESCRIPTOR = /^\s+\d+(\.\d+)?[wx]$/;
293
344
  CSS_DANGER_GATE = /[(:@\\]/;
345
+ BLOCKED_CSS_NEEDLES = [
346
+ "url(",
347
+ "expression(",
348
+ "javascript:",
349
+ "vbscript:",
350
+ "-moz-binding",
351
+ "behavior:",
352
+ "@import",
353
+ "image-set(",
354
+ "filter:progid"
355
+ ];
356
+ warnedDeclarations = /* @__PURE__ */ new Set();
357
+ MAX_WARNED_DECLARATIONS = 100;
358
+ declarationCapAnnounced = false;
294
359
  URL_ATTRIBUTES = /* @__PURE__ */ new Set([
295
360
  "href",
296
361
  "xlink:href",
@@ -338,7 +403,7 @@ function registerDisposer(node, teardown) {
338
403
  elementDisposers.set(node, disposers);
339
404
  }
340
405
  disposers.push(teardown);
341
- if (_isDev4) activeBindingCount++;
406
+ if (DEV) activeBindingCount++;
342
407
  }
343
408
  function unregisterDisposer(node, teardown) {
344
409
  const disposers = elementDisposers.get(node);
@@ -346,7 +411,7 @@ function unregisterDisposer(node, teardown) {
346
411
  const index = disposers.indexOf(teardown);
347
412
  if (index === -1) return;
348
413
  disposers.splice(index, 1);
349
- if (_isDev4) activeBindingCount--;
414
+ if (DEV) activeBindingCount--;
350
415
  if (disposers.length === 0) elementDisposers.delete(node);
351
416
  }
352
417
  function dispose(node) {
@@ -370,13 +435,13 @@ function dispose(node) {
370
435
  if (!pending || pending.length === 0) break;
371
436
  const snapshot = pending.slice();
372
437
  elementDisposers.delete(current);
373
- if (_isDev4) activeBindingCount -= snapshot.length;
438
+ if (DEV) activeBindingCount -= snapshot.length;
374
439
  for (let i3 = 0; i3 < snapshot.length; i3++) {
375
440
  if (executed >= MAX_DRAIN_TEARDOWNS) {
376
441
  const rest = snapshot.slice(i3);
377
442
  const added = elementDisposers.get(current);
378
443
  elementDisposers.set(current, added ? rest.concat(added) : rest);
379
- if (_isDev4) activeBindingCount += rest.length;
444
+ if (DEV) activeBindingCount += rest.length;
380
445
  reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
381
446
  runaway = true;
382
447
  break;
@@ -404,7 +469,7 @@ function replaceChildrenSafely(parent, ...next) {
404
469
  parent.replaceChildren(...next);
405
470
  }
406
471
  function checkLeaks(warnThreshold = 0) {
407
- if (!_isDev4) return 0;
472
+ if (!DEV) return 0;
408
473
  if (warnThreshold > 0 && activeBindingCount > warnThreshold) {
409
474
  devWarn(
410
475
  `checkLeaks: ${activeBindingCount} active DOM bindings detected. Expected \u2264${warnThreshold}. This may indicate a component was removed from the DOM without calling dispose().`
@@ -412,7 +477,7 @@ function checkLeaks(warnThreshold = 0) {
412
477
  }
413
478
  return activeBindingCount;
414
479
  }
415
- var elementDisposers, MAX_DRAIN_TEARDOWNS, _isDev4, activeBindingCount;
480
+ var elementDisposers, MAX_DRAIN_TEARDOWNS, activeBindingCount;
416
481
  var init_dispose = __esm({
417
482
  "src/core/rendering/dispose.ts"() {
418
483
  "use strict";
@@ -420,7 +485,6 @@ var init_dispose = __esm({
420
485
  init_errors();
421
486
  elementDisposers = /* @__PURE__ */ new WeakMap();
422
487
  MAX_DRAIN_TEARDOWNS = 1e4;
423
- _isDev4 = isDev();
424
488
  activeBindingCount = 0;
425
489
  }
426
490
  });
@@ -507,9 +571,9 @@ function parseMetaRefreshContent(content) {
507
571
  i2++;
508
572
  while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
509
573
  let destination;
510
- const quote = raw[i2];
511
- if (quote === '"' || quote === "'") {
512
- const close = raw.indexOf(quote, i2 + 1);
574
+ const quote2 = raw[i2];
575
+ if (quote2 === '"' || quote2 === "'") {
576
+ const close = raw.indexOf(quote2, i2 + 1);
513
577
  if (close === -1) return forbidden("unterminated quoted destination");
514
578
  destination = raw.slice(i2 + 1, close);
515
579
  const rest = trimAsciiWhitespace(raw.slice(close + 1));
@@ -681,7 +745,7 @@ function isSafeAttrName(name) {
681
745
  return SAFE_ATTR_NAME.test(name);
682
746
  }
683
747
  function ssrErrorComment(err) {
684
- if (_isDev8) {
748
+ if (DEV) {
685
749
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
686
750
  return `<!--SSR error: ${safeCommentText(msg)}-->`;
687
751
  }
@@ -711,10 +775,10 @@ function renderToString(element) {
711
775
  }
712
776
  const tag = element.tagName.toLowerCase();
713
777
  if (tag === "script" || tag === "style") {
714
- return _isDev8 ? `<!--ssr:${tag}-stripped-->` : "";
778
+ return DEV ? `<!--ssr:${tag}-stripped-->` : "";
715
779
  }
716
780
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
717
- return _isDev8 ? "<!--ssr:invalid-tag-->" : "";
781
+ return DEV ? "<!--ssr:invalid-tag-->" : "";
718
782
  }
719
783
  let html2 = `<${tag}`;
720
784
  for (const attr of Array.from(element.attributes)) {
@@ -756,7 +820,7 @@ function hydrate(component, container, options = {}) {
756
820
  const first = mismatches[0];
757
821
  if (options.onMismatch) {
758
822
  options.onMismatch(first);
759
- } else if (_isDev8) {
823
+ } else if (DEV) {
760
824
  console.warn(
761
825
  `[SibuJS hydration] ${first.message}
762
826
  at ${first.path}
@@ -957,11 +1021,11 @@ async function* renderToStream(element) {
957
1021
  }
958
1022
  const tag = element.tagName.toLowerCase();
959
1023
  if (tag === "script" || tag === "style") {
960
- if (_isDev8) yield `<!--ssr:${tag}-stripped-->`;
1024
+ if (DEV) yield `<!--ssr:${tag}-stripped-->`;
961
1025
  return;
962
1026
  }
963
1027
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
964
- if (_isDev8) yield "<!--ssr:invalid-tag-->";
1028
+ if (DEV) yield "<!--ssr:invalid-tag-->";
965
1029
  return;
966
1030
  }
967
1031
  let openTag = `<${tag}`;
@@ -1103,7 +1167,7 @@ function ssrSuspense(props) {
1103
1167
  },
1104
1168
  (err) => {
1105
1169
  if (timer) clearTimeout(timer);
1106
- if (_isDev8) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
1170
+ if (DEV) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
1107
1171
  return { id, html: fallbackHtml };
1108
1172
  }
1109
1173
  );
@@ -1146,7 +1210,7 @@ function serializeState(state2, nonce, options) {
1146
1210
  }
1147
1211
  function deserializeState(validate) {
1148
1212
  if (typeof window === "undefined") return void 0;
1149
- if (_isDev8 && !validate) {
1213
+ if (DEV && !validate) {
1150
1214
  console.warn(
1151
1215
  "[SibuJS SSR] deserializeState() called without a validate guard \u2014 tampered SSR payloads will not be detected."
1152
1216
  );
@@ -1163,7 +1227,7 @@ function escapeHtml(str) {
1163
1227
  function escapeAttr(str) {
1164
1228
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1165
1229
  }
1166
- var _isDev8, SAFE_ATTR_NAME, VOID_ELEMENTS, SAFE_ID, SAFE_SUSPENSE_ID, SSR_DATA_ATTR, DEFAULT_MAX_SSR_BYTES;
1230
+ var SAFE_ATTR_NAME, VOID_ELEMENTS, SAFE_ID, SAFE_SUSPENSE_ID, SSR_DATA_ATTR, DEFAULT_MAX_SSR_BYTES;
1167
1231
  var init_ssr = __esm({
1168
1232
  "src/platform/ssr.ts"() {
1169
1233
  "use strict";
@@ -1173,7 +1237,6 @@ var init_ssr = __esm({
1173
1237
  init_headEntry();
1174
1238
  init_metaRefresh();
1175
1239
  init_sanitize();
1176
- _isDev8 = isDev();
1177
1240
  SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
1178
1241
  VOID_ELEMENTS = /* @__PURE__ */ new Set([
1179
1242
  "area",
@@ -1290,7 +1353,6 @@ init_sanitize();
1290
1353
  // src/utils/setSafeAttribute.ts
1291
1354
  init_dev();
1292
1355
  init_sanitize();
1293
- var _isDev2 = isDev();
1294
1356
  var HTML_NS = "http://www.w3.org/1999/xhtml";
1295
1357
  var SVG_NS = "http://www.w3.org/2000/svg";
1296
1358
  var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -1313,7 +1375,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1313
1375
  const ns = namespaceFor(el, name);
1314
1376
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
1315
1377
  if (isEventHandlerAttr(name)) {
1316
- if (_isDev2) {
1378
+ if (DEV) {
1317
1379
  devWarn(
1318
1380
  `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
1319
1381
  );
@@ -1328,7 +1390,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1328
1390
  return true;
1329
1391
  }
1330
1392
  if (isHtmlContentAttribute(name)) {
1331
- if (_isDev2) {
1393
+ if (DEV) {
1332
1394
  devWarn(
1333
1395
  `${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.`
1334
1396
  );
@@ -1357,7 +1419,7 @@ function setSafeAttribute(el, name, value, options = {}) {
1357
1419
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
1358
1420
  return true;
1359
1421
  }
1360
- const safe = sanitizeAttributeString(name, str);
1422
+ const safe = sanitizeAttributeString(name, str, { element: el });
1361
1423
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
1362
1424
  if (current === safe) return true;
1363
1425
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -1587,7 +1649,14 @@ function reactiveBinding(commit, ownerNode) {
1587
1649
  subscriber._disposed = false;
1588
1650
  subscriber._errorPhase = "binding";
1589
1651
  subscriber._errorNode = ownerNode;
1590
- run();
1652
+ try {
1653
+ run();
1654
+ } catch (err) {
1655
+ subscriber._disposed = true;
1656
+ subscriber._errorNode = void 0;
1657
+ cleanup(subscriber);
1658
+ throw err;
1659
+ }
1591
1660
  return subscriber._dispose ?? (subscriber._dispose = () => {
1592
1661
  subscriber._disposed = true;
1593
1662
  subscriber._errorNode = void 0;
@@ -1818,17 +1887,16 @@ function forEachSubscriber(signal2, visit) {
1818
1887
  }
1819
1888
 
1820
1889
  // src/reactivity/track.ts
1821
- var _isDev3 = isDev();
1822
- var _runtimeVersion = true ? "4.0.1" : "dev";
1890
+ var _runtimeVersion = true ? "4.2.0" : "dev";
1823
1891
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
1824
1892
  function resolveReactiveApi() {
1825
1893
  const g2 = globalThis;
1826
1894
  const existing = g2[REGISTRY_KEY];
1827
1895
  if (existing) {
1828
- if (_isDev3 && !existing.__dupWarned) {
1896
+ if (DEV && !existing.__dupWarned) {
1829
1897
  existing.__dupWarned = true;
1830
1898
  devWarn(
1831
- `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']).`
1899
+ `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.`
1832
1900
  );
1833
1901
  }
1834
1902
  return existing;
@@ -1899,7 +1967,103 @@ function bindAttribute(el, attr, getter) {
1899
1967
  init_dev();
1900
1968
  init_errors();
1901
1969
  init_dispose();
1902
- var _isDev5 = isDev();
1970
+
1971
+ // src/core/rendering/focusPreservation.ts
1972
+ init_dev();
1973
+ var IDENTITY_ATTRS = ["data-focus-key", "id", "name"];
1974
+ var SELECTABLE_TYPES = /* @__PURE__ */ new Set(["text", "search", "url", "tel", "password", ""]);
1975
+ function selectionOf(el) {
1976
+ const tag = el.tagName;
1977
+ const selectable = tag === "TEXTAREA" || tag === "INPUT" && SELECTABLE_TYPES.has(el.type.toLowerCase());
1978
+ if (!selectable) return { start: null, end: null, direction: null };
1979
+ const field = el;
1980
+ return { start: field.selectionStart, end: field.selectionEnd, direction: field.selectionDirection };
1981
+ }
1982
+ function captureFocusWithin(nodes) {
1983
+ if (nodes.length === 0 || typeof document === "undefined") return null;
1984
+ const active = document.activeElement;
1985
+ if (!active || active === document.body) return null;
1986
+ let inside = false;
1987
+ for (let i2 = 0; i2 < nodes.length; i2++) {
1988
+ const n = nodes[i2];
1989
+ if (n === active || n.nodeType === 1 && n.contains(active)) {
1990
+ inside = true;
1991
+ break;
1992
+ }
1993
+ }
1994
+ if (!inside) return null;
1995
+ let attr = null;
1996
+ let value = null;
1997
+ for (let i2 = 0; i2 < IDENTITY_ATTRS.length; i2++) {
1998
+ const candidate = active.getAttribute(IDENTITY_ATTRS[i2]);
1999
+ if (candidate) {
2000
+ attr = IDENTITY_ATTRS[i2];
2001
+ value = candidate;
2002
+ break;
2003
+ }
2004
+ }
2005
+ return { el: active, attr, value, tag: active.tagName, ...selectionOf(active) };
2006
+ }
2007
+ function quote(value) {
2008
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
2009
+ }
2010
+ function findUniqueByIdentity(nodes, selector) {
2011
+ let el = null;
2012
+ let count = 0;
2013
+ for (let i2 = 0; i2 < nodes.length; i2++) {
2014
+ const n = nodes[i2];
2015
+ if (n.nodeType !== 1) continue;
2016
+ const root = n;
2017
+ if (root.matches(selector)) {
2018
+ count++;
2019
+ el ?? (el = root);
2020
+ }
2021
+ const inner = root.querySelectorAll(selector);
2022
+ for (let j = 0; j < inner.length; j++) {
2023
+ count++;
2024
+ el ?? (el = inner[j]);
2025
+ }
2026
+ if (count > 1) return { el: null, count };
2027
+ }
2028
+ return count === 1 ? { el, count } : { el: null, count };
2029
+ }
2030
+ function refocus(el, snapshot) {
2031
+ el.focus();
2032
+ if (snapshot.start !== null && typeof el.setSelectionRange === "function") {
2033
+ const field = el;
2034
+ const max = field.value.length;
2035
+ const start = Math.min(snapshot.start, max);
2036
+ const end = Math.min(snapshot.end ?? start, max);
2037
+ field.setSelectionRange(start, end, snapshot.direction ?? "none");
2038
+ }
2039
+ }
2040
+ function restoreFocusWithin(snapshot, nodes, where) {
2041
+ if (!snapshot) return;
2042
+ if (typeof document === "undefined") return;
2043
+ const active = document.activeElement;
2044
+ if (active === snapshot.el) return;
2045
+ if (active && active !== document.body && active !== document.documentElement) return;
2046
+ if (snapshot.el.isConnected) {
2047
+ refocus(snapshot.el, snapshot);
2048
+ return;
2049
+ }
2050
+ let ambiguous = false;
2051
+ if (snapshot.attr && snapshot.value) {
2052
+ const { el: replacement, count } = findUniqueByIdentity(nodes, `[${snapshot.attr}=${quote(snapshot.value)}]`);
2053
+ if (replacement && replacement.tagName === snapshot.tag) {
2054
+ refocus(replacement, snapshot);
2055
+ return;
2056
+ }
2057
+ ambiguous = count > 1;
2058
+ }
2059
+ devWarnLazy(() => {
2060
+ const id = snapshot.attr ? ` (${snapshot.attr}="${snapshot.value}")` : "";
2061
+ 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";
2062
+ 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.`;
2063
+ });
2064
+ }
2065
+
2066
+ // src/reactivity/bindChildNode.ts
1903
2067
  function bindChildNode(placeholder, getter) {
1904
2068
  let lastNodes = [];
1905
2069
  function commit() {
@@ -1910,6 +2074,7 @@ function bindChildNode(placeholder, getter) {
1910
2074
  reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
1911
2075
  return;
1912
2076
  }
2077
+ const focused = captureFocusWithin(lastNodes);
1913
2078
  if (result == null || typeof result === "boolean") {
1914
2079
  for (let i2 = 0; i2 < lastNodes.length; i2++) {
1915
2080
  const node = lastNodes[i2];
@@ -1917,6 +2082,7 @@ function bindChildNode(placeholder, getter) {
1917
2082
  if (node.parentNode) node.parentNode.removeChild(node);
1918
2083
  }
1919
2084
  lastNodes.length = 0;
2085
+ restoreFocusWithin(focused, lastNodes, "bindChildNode");
1920
2086
  return;
1921
2087
  }
1922
2088
  const parent = placeholder.parentNode;
@@ -1933,8 +2099,7 @@ function bindChildNode(placeholder, getter) {
1933
2099
  if (item == null || typeof item === "boolean") continue;
1934
2100
  const node = item instanceof Node ? item : document.createTextNode(String(item));
1935
2101
  if (seen.has(node)) {
1936
- if (_isDev5)
1937
- devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
2102
+ if (DEV) devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1938
2103
  continue;
1939
2104
  }
1940
2105
  seen.add(node);
@@ -1967,6 +2132,7 @@ function bindChildNode(placeholder, getter) {
1967
2132
  prev = node;
1968
2133
  }
1969
2134
  lastNodes = newNodes;
2135
+ restoreFocusWithin(focused, newNodes, "bindChildNode");
1970
2136
  }
1971
2137
  return reactiveBinding2(commit, placeholder);
1972
2138
  }
@@ -1975,7 +2141,6 @@ function bindChildNode(placeholder, getter) {
1975
2141
  init_sanitize();
1976
2142
  init_dispose();
1977
2143
  var SVG_NS2 = "http://www.w3.org/2000/svg";
1978
- var _isDev6 = isDev();
1979
2144
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
1980
2145
  function isBlockedTag(tag) {
1981
2146
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -1993,18 +2158,9 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
1993
2158
  "self",
1994
2159
  "frames"
1995
2160
  ]);
1996
- function looksLikeClassList(s2) {
1997
- const t2 = s2.trim();
1998
- if (!t2) return false;
1999
- const tokens = t2.split(/\s+/);
2000
- let sawClassish = false;
2001
- for (let i2 = 0; i2 < tokens.length; i2++) {
2002
- const tok = tokens[i2];
2003
- if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return false;
2004
- if (/[-:/0-9]/.test(tok)) sawClassish = true;
2005
- }
2006
- return sawClassish;
2007
- }
2161
+ var warnedLoneStrings = /* @__PURE__ */ new Set();
2162
+ var MAX_WARNED_LONE_STRINGS = 100;
2163
+ var loneStringCapAnnounced = false;
2008
2164
  var kebabCache = /* @__PURE__ */ new Map();
2009
2165
  function toKebab(prop) {
2010
2166
  let cached = kebabCache.get(prop);
@@ -2016,13 +2172,13 @@ function toKebab(prop) {
2016
2172
  function applyStyle(el, style2) {
2017
2173
  if (typeof style2 === "function") {
2018
2174
  const teardown = reactiveBinding2(() => {
2019
- el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
2175
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2()), { element: el }));
2020
2176
  }, el);
2021
2177
  registerDisposer(el, teardown);
2022
2178
  return;
2023
2179
  }
2024
2180
  if (typeof style2 === "string") {
2025
- el.setAttribute("style", sanitizeStyleAttribute(style2));
2181
+ el.setAttribute("style", sanitizeStyleAttribute(style2, { element: el }));
2026
2182
  return;
2027
2183
  }
2028
2184
  const htmlEl = el;
@@ -2032,14 +2188,26 @@ function applyStyle(el, style2) {
2032
2188
  if (typeof val === "function") {
2033
2189
  const getter = val;
2034
2190
  const teardown = reactiveBinding2(() => {
2035
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter())));
2191
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(getter()), { property: name, element: el }));
2036
2192
  }, el);
2037
2193
  registerDisposer(el, teardown);
2038
2194
  } else {
2039
- htmlEl.style.setProperty(name, sanitizeCSSValue(String(val)));
2195
+ htmlEl.style.setProperty(name, sanitizeCSSValue(String(val), { property: name, element: el }));
2040
2196
  }
2041
2197
  }
2042
2198
  }
2199
+ function resolveClassValue(cls) {
2200
+ if (typeof cls === "string") return cls;
2201
+ if (typeof cls === "function") return cls();
2202
+ if (!cls) return "";
2203
+ let out = "";
2204
+ for (const name in cls) {
2205
+ const val = cls[name];
2206
+ const active = typeof val === "function" ? val() : val;
2207
+ if (active) out = out ? `${out} ${name}` : name;
2208
+ }
2209
+ return out;
2210
+ }
2043
2211
  function applyClass(el, cls) {
2044
2212
  if (typeof cls === "string") {
2045
2213
  el.setAttribute("class", cls);
@@ -2143,11 +2311,26 @@ var tagFactory = (tag, ns) => {
2143
2311
  appendChildren(el, second);
2144
2312
  return el;
2145
2313
  }
2146
- if (_isDev6 && looksLikeClassList(first)) {
2147
- devWarn(
2148
- `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.`
2149
- );
2150
- }
2314
+ devWarnLazy(() => {
2315
+ const tokens = first.trim().split(/\s+/);
2316
+ if (tokens.length < 2) return "";
2317
+ let utilityTokens = 0;
2318
+ for (let i2 = 0; i2 < tokens.length; i2++) {
2319
+ const tok = tokens[i2];
2320
+ if (!/^-?[A-Za-z_][A-Za-z0-9_:/.-]*$/.test(tok)) return "";
2321
+ if (/[-:/0-9]/.test(tok)) utilityTokens++;
2322
+ }
2323
+ if (utilityTokens < 2) return "";
2324
+ const key = `${tag}|${first}`;
2325
+ if (warnedLoneStrings.has(key)) return "";
2326
+ if (warnedLoneStrings.size >= MAX_WARNED_LONE_STRINGS) {
2327
+ if (loneStringCapAnnounced) return "";
2328
+ loneStringCapAnnounced = true;
2329
+ 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.`;
2330
+ }
2331
+ warnedLoneStrings.add(key);
2332
+ 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.`;
2333
+ });
2151
2334
  el.textContent = first;
2152
2335
  return el;
2153
2336
  }
@@ -2164,7 +2347,7 @@ var tagFactory = (tag, ns) => {
2164
2347
  if (pClass != null) applyClass(el, pClass);
2165
2348
  const pId = props.id;
2166
2349
  if (pId != null) {
2167
- if (_isDev6 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2350
+ if (DEV && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2168
2351
  devWarn(
2169
2352
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
2170
2353
  );
@@ -2179,7 +2362,7 @@ var tagFactory = (tag, ns) => {
2179
2362
  const handler = pOn[ev];
2180
2363
  if (typeof handler === "function") {
2181
2364
  el.addEventListener(ev, handler);
2182
- } else if (_isDev6) {
2365
+ } else if (DEV) {
2183
2366
  devWarn(
2184
2367
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
2185
2368
  );
@@ -2411,7 +2594,6 @@ var isBatching = API2.isBatching;
2411
2594
  // src/core/signals/signal.ts
2412
2595
  init_dev();
2413
2596
  var _g = globalThis;
2414
- var _isDev7 = isDev();
2415
2597
  function signal(initial, options) {
2416
2598
  const state2 = {
2417
2599
  value: initial,
@@ -2424,7 +2606,7 @@ function signal(initial, options) {
2424
2606
  _d: false,
2425
2607
  _validate: void 0
2426
2608
  };
2427
- const debugName = _isDev7 ? options?.name : void 0;
2609
+ const debugName = DEV ? options?.name : void 0;
2428
2610
  const equalsFn = options?.equals;
2429
2611
  if (debugName) state2.__name = debugName;
2430
2612
  function get() {
@@ -2441,7 +2623,7 @@ function signal(initial, options) {
2441
2623
  if (equalsFn(prev, newValue)) return;
2442
2624
  state2.value = newValue;
2443
2625
  state2.__v++;
2444
- if (_isDev7) {
2626
+ if (DEV) {
2445
2627
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
2446
2628
  if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
2447
2629
  }
@@ -2449,7 +2631,7 @@ function signal(initial, options) {
2449
2631
  notifySubscribers2(state2);
2450
2632
  }
2451
2633
  };
2452
- } else if (_isDev7) {
2634
+ } else if (DEV) {
2453
2635
  set = (next) => {
2454
2636
  const prev = state2.value;
2455
2637
  const newValue = typeof next === "function" ? next(prev) : next;
@@ -2474,7 +2656,7 @@ function signal(initial, options) {
2474
2656
  }
2475
2657
  };
2476
2658
  }
2477
- if (_isDev7) {
2659
+ if (DEV) {
2478
2660
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
2479
2661
  if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
2480
2662
  }
@@ -4140,7 +4322,6 @@ function RouterLink(props, children) {
4140
4322
  if (!_routerRef.current) throw new Error("Router not initialized. Call createRouter() first.");
4141
4323
  const { to, replace: replace2 = false, activeClass, exactActiveClass, nodes, target, rel, class: classAttr, ...attrs } = props;
4142
4324
  const content = children !== void 0 ? children : nodes;
4143
- const baseClass = typeof classAttr === "string" ? classAttr : "";
4144
4325
  const routeGetter = _routerRef.current.routeGetter;
4145
4326
  const rawHref = _routerRef.current["resolvePath"](to);
4146
4327
  const kind = classifyNavigationTarget(rawHref);
@@ -4176,7 +4357,7 @@ function RouterLink(props, children) {
4176
4357
  if (exactActiveClass) classes.push(exactActiveClass);
4177
4358
  else if (options.linkExactActiveClass) classes.push(options.linkExactActiveClass);
4178
4359
  }
4179
- link2.className = [baseClass, ...classes].filter(Boolean).join(" ");
4360
+ link2.className = [resolveClassValue(classAttr), ...classes].filter(Boolean).join(" ");
4180
4361
  });
4181
4362
  registerDisposer(link2, effectCleanup);
4182
4363
  Object.entries(attrs).forEach(([key, value]) => {
@@ -4188,7 +4369,7 @@ function RouterLink(props, children) {
4188
4369
  const safe = sanitizeUrl(str);
4189
4370
  if (safe) link2.setAttribute(key, safe);
4190
4371
  } else if (lkey === "style") {
4191
- link2.setAttribute(key, sanitizeStyleAttribute(str));
4372
+ link2.setAttribute(key, sanitizeStyleAttribute(str, { element: link2 }));
4192
4373
  } else {
4193
4374
  link2.setAttribute(key, str);
4194
4375
  }