sibujs 3.4.1 → 4.0.1

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 (98) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +118 -15
  3. package/dist/browser.cjs +410 -101
  4. package/dist/browser.d.cts +50 -5
  5. package/dist/browser.d.ts +50 -5
  6. package/dist/browser.js +8 -6
  7. package/dist/build.cjs +784 -392
  8. package/dist/build.js +15 -15
  9. package/dist/cdn.global.js +11 -7
  10. package/dist/{chunk-PBZWJQNY.js → chunk-52ERIVFD.js} +8 -8
  11. package/dist/chunk-52XFPGSN.js +123 -0
  12. package/dist/{chunk-Y22QAKJX.js → chunk-5BVKQX2F.js} +4 -4
  13. package/dist/chunk-5INI7D2L.js +194 -0
  14. package/dist/{chunk-4I3M326N.js → chunk-AD5YSZAN.js} +19 -25
  15. package/dist/{chunk-F7HW2NI5.js → chunk-AW6HPJ3L.js} +1 -1
  16. package/dist/chunk-DHDZ7IVN.js +109 -0
  17. package/dist/{chunk-VQZDHAEK.js → chunk-E72G5JSK.js} +1 -1
  18. package/dist/{chunk-MPE75KOA.js → chunk-HPXTUPJD.js} +259 -124
  19. package/dist/{chunk-EX77FXTT.js → chunk-HYCCIYNS.js} +228 -51
  20. package/dist/{chunk-FWFEBYQ6.js → chunk-IKVUUE7D.js} +115 -41
  21. package/dist/{chunk-5MFF5EPT.js → chunk-J4WZMCIW.js} +5 -5
  22. package/dist/{chunk-BQLVTAUZ.js → chunk-JYXQCELN.js} +57 -22
  23. package/dist/{chunk-WEZ5SP7L.js → chunk-PHB3MCK4.js} +73 -34
  24. package/dist/chunk-PHRH66Y7.js +147 -0
  25. package/dist/{chunk-S2VW4NBA.js → chunk-PJDSI2K7.js} +295 -198
  26. package/dist/{chunk-QHPUFNHZ.js → chunk-PTLJ7NQX.js} +3 -3
  27. package/dist/{chunk-NXR2YFZB.js → chunk-QD4I7ZGZ.js} +24 -34
  28. package/dist/{chunk-S3NFJO6L.js → chunk-TIRZCERI.js} +36 -5
  29. package/dist/{chunk-7PNKW3FJ.js → chunk-TNEHT3NG.js} +232 -67
  30. package/dist/{chunk-335KJEN2.js → chunk-TVURNWDK.js} +5 -3
  31. package/dist/{chunk-CV7FAQ2R.js → chunk-UNAYBND2.js} +9 -3
  32. package/dist/chunk-VPP2FONR.js +142 -0
  33. package/dist/{chunk-X4JS367G.js → chunk-XDOMTQHM.js} +14 -10
  34. package/dist/chunk-XET2RUJF.js +61 -0
  35. package/dist/{chunk-3FFCUIGV.js → chunk-XZMCVZW6.js} +309 -188
  36. package/dist/{chunk-YDWP3ZOU.js → chunk-YLV26GAJ.js} +228 -86
  37. package/dist/{contracts-ey_Qh8ef.d.ts → contracts-Hg6ITAEF.d.cts} +6 -6
  38. package/dist/{contracts-ey_Qh8ef.d.cts → contracts-Hg6ITAEF.d.ts} +6 -6
  39. package/dist/{customElement-CPfIrbvg.d.cts → customElement-CKLoO63w.d.cts} +73 -11
  40. package/dist/{customElement-CPfIrbvg.d.ts → customElement-CKLoO63w.d.ts} +73 -11
  41. package/dist/data.cjs +483 -205
  42. package/dist/data.d.cts +139 -7
  43. package/dist/data.d.ts +139 -7
  44. package/dist/data.js +9 -9
  45. package/dist/devtools.cjs +222 -87
  46. package/dist/devtools.js +8 -8
  47. package/dist/dispose-4FZ6OFXR.js +19 -0
  48. package/dist/ecosystem.cjs +426 -182
  49. package/dist/ecosystem.js +11 -11
  50. package/dist/extras.cjs +1988 -772
  51. package/dist/extras.d.cts +7 -7
  52. package/dist/extras.d.ts +7 -7
  53. package/dist/extras.js +29 -26
  54. package/dist/index.cjs +791 -392
  55. package/dist/index.d.cts +299 -28
  56. package/dist/index.d.ts +299 -28
  57. package/dist/index.js +33 -18
  58. package/dist/motion.cjs +180 -50
  59. package/dist/motion.d.cts +26 -0
  60. package/dist/motion.d.ts +26 -0
  61. package/dist/motion.js +5 -5
  62. package/dist/patterns.cjs +437 -170
  63. package/dist/patterns.d.cts +60 -13
  64. package/dist/patterns.d.ts +60 -13
  65. package/dist/patterns.js +7 -7
  66. package/dist/performance.cjs +333 -148
  67. package/dist/performance.d.cts +55 -7
  68. package/dist/performance.d.ts +55 -7
  69. package/dist/performance.js +9 -9
  70. package/dist/plugins.cjs +1378 -583
  71. package/dist/plugins.d.cts +227 -9
  72. package/dist/plugins.d.ts +227 -9
  73. package/dist/plugins.js +610 -321
  74. package/dist/{ssr-D62yFwuw.d.cts → ssr-BiPRdZ6n.d.cts} +44 -15
  75. package/dist/{ssr-D62yFwuw.d.ts → ssr-BiPRdZ6n.d.ts} +44 -15
  76. package/dist/{ssr-2PPULEK2.js → ssr-JFP5535P.js} +5 -4
  77. package/dist/ssr.cjs +1111 -450
  78. package/dist/ssr.d.cts +230 -20
  79. package/dist/ssr.d.ts +230 -20
  80. package/dist/ssr.js +15 -12
  81. package/dist/{startup-0Qv6aosO.d.cts → startup-BMpaiMhP.d.cts} +1 -1
  82. package/dist/{startup-0Qv6aosO.d.ts → startup-BMpaiMhP.d.ts} +1 -1
  83. package/dist/testing.cjs +205 -63
  84. package/dist/testing.d.cts +1 -1
  85. package/dist/testing.d.ts +1 -1
  86. package/dist/testing.js +8 -10
  87. package/dist/ui.cjs +640 -199
  88. package/dist/ui.d.cts +2 -2
  89. package/dist/ui.d.ts +2 -2
  90. package/dist/ui.js +10 -10
  91. package/dist/widgets.cjs +207 -82
  92. package/dist/widgets.js +9 -9
  93. package/package.json +12 -7
  94. package/dist/chunk-5VGSK6D2.js +0 -80
  95. package/dist/chunk-6CAJU5TP.js +0 -38
  96. package/dist/chunk-COY6PUD2.js +0 -34
  97. package/dist/chunk-DLBFEAIS.js +0 -96
  98. package/dist/chunk-L3GAGWCC.js +0 -111
@@ -1,39 +1,195 @@
1
1
  import {
2
+ canonicalAttrName,
2
3
  isEventHandlerAttr,
3
- sanitizeSrcset,
4
+ isHtmlContentAttribute,
5
+ isPolicyAttribute,
6
+ sanitizeAttributeString,
4
7
  sanitizeUrl,
5
8
  stripControlChars
6
- } from "./chunk-L3GAGWCC.js";
9
+ } from "./chunk-5INI7D2L.js";
7
10
  import {
8
11
  getSSRStore
9
- } from "./chunk-S3NFJO6L.js";
12
+ } from "./chunk-TIRZCERI.js";
13
+ import {
14
+ dispose,
15
+ replaceChildrenSafely
16
+ } from "./chunk-DHDZ7IVN.js";
10
17
  import {
11
18
  isDev
12
- } from "./chunk-COY6PUD2.js";
19
+ } from "./chunk-VPP2FONR.js";
20
+
21
+ // src/utils/metaRefresh.ts
22
+ function isAsciiWhitespace(ch) {
23
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
24
+ }
25
+ function trimAsciiWhitespace(value) {
26
+ let start = 0;
27
+ let end = value.length;
28
+ while (start < end && isAsciiWhitespace(value[start])) start++;
29
+ while (end > start && isAsciiWhitespace(value[end - 1])) end--;
30
+ return value.slice(start, end);
31
+ }
32
+ var forbidden = (reason) => ({ kind: "forbidden", reason });
33
+ function parseMetaRefreshContent(content) {
34
+ const raw = trimAsciiWhitespace(stripControlChars(content));
35
+ if (raw === "") return forbidden("empty content");
36
+ let i = 0;
37
+ while (i < raw.length && raw[i] >= "0" && raw[i] <= "9") i++;
38
+ if (i === 0) return forbidden("missing or non-numeric delay");
39
+ const delay = Number.parseInt(raw.slice(0, i), 10);
40
+ if (!Number.isFinite(delay)) return forbidden("unparseable delay");
41
+ while (i < raw.length && isAsciiWhitespace(raw[i])) i++;
42
+ if (i === raw.length) return { kind: "delay-only", delay };
43
+ if (raw[i] !== ";" && raw[i] !== ",") return forbidden("unexpected text after delay");
44
+ i++;
45
+ while (i < raw.length && isAsciiWhitespace(raw[i])) i++;
46
+ const keyStart = i;
47
+ while (i < raw.length && !isAsciiWhitespace(raw[i]) && raw[i] !== "=") i++;
48
+ const key = raw.slice(keyStart, i).toLowerCase();
49
+ if (key !== "url") return forbidden(`unsupported refresh key ${JSON.stringify(key)}`);
50
+ while (i < raw.length && isAsciiWhitespace(raw[i])) i++;
51
+ if (raw[i] !== "=") return forbidden("missing '=' after url");
52
+ i++;
53
+ while (i < raw.length && isAsciiWhitespace(raw[i])) i++;
54
+ let destination;
55
+ const quote = raw[i];
56
+ if (quote === '"' || quote === "'") {
57
+ const close = raw.indexOf(quote, i + 1);
58
+ if (close === -1) return forbidden("unterminated quoted destination");
59
+ destination = raw.slice(i + 1, close);
60
+ const rest = trimAsciiWhitespace(raw.slice(close + 1));
61
+ if (rest !== "") return forbidden("trailing text after quoted destination");
62
+ } else {
63
+ destination = trimAsciiWhitespace(raw.slice(i));
64
+ if (/[;,]/.test(destination)) return forbidden("multiple refresh assignments");
65
+ }
66
+ if (destination === "") return forbidden("empty destination");
67
+ const sanitizedDestination = sanitizeUrl(destination);
68
+ if (sanitizedDestination === "") return forbidden("destination uses a disallowed protocol");
69
+ return { kind: "allowed", delay, destination, sanitizedDestination };
70
+ }
71
+ function resolveMetaRefreshPolicy(attributes) {
72
+ let httpEquiv;
73
+ let content;
74
+ let duplicateName;
75
+ const seen = /* @__PURE__ */ new Set();
76
+ for (const [key, value] of attributes) {
77
+ const canonical = canonicalAttrName(key);
78
+ if (seen.has(canonical)) duplicateName = canonical;
79
+ seen.add(canonical);
80
+ if (canonical === "http-equiv") httpEquiv = value;
81
+ else if (canonical === "content") content = value;
82
+ }
83
+ if (duplicateName !== void 0) {
84
+ return forbidden(`duplicate case-insensitive attribute ${JSON.stringify(duplicateName)}`);
85
+ }
86
+ if (typeof httpEquiv !== "string") return { kind: "not-refresh" };
87
+ if (trimAsciiWhitespace(stripControlChars(httpEquiv)).toLowerCase() !== "refresh") {
88
+ return { kind: "not-refresh" };
89
+ }
90
+ if (typeof content !== "string") return { kind: "not-refresh" };
91
+ return parseMetaRefreshContent(content);
92
+ }
93
+ function normalizeMetaAttributes(props) {
94
+ const out = /* @__PURE__ */ new Map();
95
+ const seen = /* @__PURE__ */ new Set();
96
+ for (const key of Object.keys(props)) {
97
+ const canonical = canonicalAttrName(key);
98
+ if (seen.has(canonical)) return null;
99
+ seen.add(canonical);
100
+ out.set(key, props[key]);
101
+ }
102
+ return out;
103
+ }
104
+ function isForbiddenMetaEntry(props) {
105
+ const normalized = normalizeMetaAttributes(props);
106
+ if (normalized === null) return true;
107
+ return resolveMetaRefreshPolicy(normalized).kind === "forbidden";
108
+ }
109
+
110
+ // src/utils/headEntry.ts
111
+ var SAFE_HEAD_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
112
+ function isEmittableHeadAttrName(name) {
113
+ if (!SAFE_HEAD_ATTR_NAME.test(name)) return false;
114
+ const canonical = canonicalAttrName(name);
115
+ if (isEventHandlerAttr(canonical)) return false;
116
+ if (isHtmlContentAttribute(canonical)) return false;
117
+ return true;
118
+ }
119
+ function sanitizeHeadAttrValue(canonical, value) {
120
+ if (!isPolicyAttribute(canonical)) return value;
121
+ const safe = sanitizeAttributeString(canonical, value);
122
+ return safe === "" ? null : safe;
123
+ }
124
+ function findDuplicateAttributeName(names) {
125
+ const seen = /* @__PURE__ */ new Set();
126
+ for (const name of names) {
127
+ const canonical = canonicalAttrName(name);
128
+ if (seen.has(canonical)) return canonical;
129
+ seen.add(canonical);
130
+ }
131
+ return null;
132
+ }
133
+ function planEntry(props, resolver, applyRefreshPolicy) {
134
+ const rawNames = Object.keys(props);
135
+ const duplicate = findDuplicateAttributeName(rawNames);
136
+ if (duplicate !== null) {
137
+ return { kind: "drop", reason: `duplicate case-insensitive attribute ${JSON.stringify(duplicate)}` };
138
+ }
139
+ const attributes = /* @__PURE__ */ new Map();
140
+ let reactive = false;
141
+ for (const name of rawNames) {
142
+ if (!Object.hasOwn(props, name)) continue;
143
+ if (!isEmittableHeadAttrName(name)) continue;
144
+ const authored = props[name];
145
+ if (resolver.isReactiveValue?.(authored)) reactive = true;
146
+ const canonical = canonicalAttrName(name);
147
+ const sanitized = sanitizeHeadAttrValue(canonical, resolver.resolveValue(name, authored));
148
+ if (sanitized === null) continue;
149
+ attributes.set(canonical, sanitized);
150
+ }
151
+ if (attributes.size === 0) return { kind: "drop", reason: "no effective attributes remain" };
152
+ const refresh = applyRefreshPolicy ? resolveMetaRefreshPolicy(attributes) : { kind: "not-refresh" };
153
+ if (refresh.kind === "forbidden") return { kind: "drop", reason: refresh.reason };
154
+ if (reactive && (refresh.kind === "allowed" || refresh.kind === "delay-only")) {
155
+ return { kind: "drop", reason: "a reactive meta entry may not publish a native refresh directive" };
156
+ }
157
+ return { kind: "publish", attributes, refresh };
158
+ }
159
+ function planMetaEntry(props, resolver) {
160
+ return planEntry(props, resolver, true);
161
+ }
162
+ function planHeadElementEntry(props, resolver) {
163
+ return planEntry(props, resolver, false);
164
+ }
165
+ var STATIC_VALUE_RESOLVER = {
166
+ resolveValue: (_name, value) => String(value)
167
+ };
168
+ function escapeHeadAttrValue(value) {
169
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
170
+ }
171
+ function serializeHeadEntry(tag, attrs) {
172
+ const plan = tag === "meta" ? planMetaEntry(attrs, STATIC_VALUE_RESOLVER) : planHeadElementEntry(attrs, STATIC_VALUE_RESOLVER);
173
+ if (plan.kind === "drop") return "";
174
+ const pairs = Array.from(plan.attributes, ([name, value]) => `${name}="${escapeHeadAttrValue(value)}"`).join(" ");
175
+ return `<${tag} ${pairs} />`;
176
+ }
13
177
 
14
178
  // src/platform/ssr.ts
15
- function sanitizeUrlAttr(name, value) {
16
- return name === "srcset" ? sanitizeSrcset(value) : sanitizeUrl(value);
179
+ function isPolicyAttr(lowerName) {
180
+ return isPolicyAttribute(lowerName);
181
+ }
182
+ function isForbiddenSsrAttr(lowerName) {
183
+ return isHtmlContentAttribute(lowerName);
184
+ }
185
+ function sanitizeSsrAttributeValue(lowerName, value) {
186
+ return sanitizeAttributeString(lowerName, value);
17
187
  }
18
188
  var _isDev = isDev();
19
189
  var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
20
190
  function isSafeAttrName(name) {
21
191
  return SAFE_ATTR_NAME.test(name);
22
192
  }
23
- var URL_ATTRS = /* @__PURE__ */ new Set([
24
- "href",
25
- "src",
26
- "action",
27
- "formaction",
28
- "cite",
29
- "poster",
30
- "background",
31
- "srcset",
32
- "ping",
33
- "manifest",
34
- "data",
35
- "xlink:href"
36
- ]);
37
193
  function ssrErrorComment(err) {
38
194
  if (_isDev) {
39
195
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
@@ -91,10 +247,11 @@ function renderToString(element) {
91
247
  const rawName = attr.name;
92
248
  if (!isSafeAttrName(rawName)) continue;
93
249
  if (isEventHandlerAttr(rawName)) continue;
250
+ if (isForbiddenSsrAttr(rawName)) continue;
94
251
  const lowerName = rawName.toLowerCase();
95
252
  let value = attr.value;
96
- if (URL_ATTRS.has(lowerName)) {
97
- value = sanitizeUrlAttr(lowerName, value);
253
+ if (isPolicyAttr(lowerName)) {
254
+ value = sanitizeSsrAttributeValue(lowerName, value);
98
255
  if (!value) continue;
99
256
  }
100
257
  html += ` ${rawName}="${escapeAttr(value)}"`;
@@ -135,9 +292,20 @@ function hydrate(component, container, options = {}) {
135
292
  }
136
293
  }
137
294
  }
138
- container.replaceChildren(clientTree);
295
+ replaceChildrenSafely(container, clientTree);
139
296
  container.setAttribute("data-sibu-hydrated", "true");
140
297
  }
298
+ function directTextOf(node) {
299
+ let out = "";
300
+ for (const child of Array.from(node.childNodes)) {
301
+ if (child.nodeType === 3) out += child.nodeValue ?? "";
302
+ }
303
+ return out.replace(/\s+/g, " ").trim();
304
+ }
305
+ function disposeAndReplace(outgoing, next) {
306
+ dispose(outgoing);
307
+ outgoing.replaceWith(next);
308
+ }
141
309
  function collectMismatches(serverNode, clientNode, path, out, max = 5) {
142
310
  if (out.length >= max) return;
143
311
  const nodePath = path || clientNode?.tagName?.toLowerCase() || "(root)";
@@ -213,6 +381,18 @@ function collectMismatches(serverNode, clientNode, path, out, max = 5) {
213
381
  });
214
382
  }
215
383
  }
384
+ const serverText = directTextOf(serverNode);
385
+ const clientText = directTextOf(clientNode);
386
+ if (serverText !== clientText) {
387
+ out.push({
388
+ kind: "text",
389
+ path: nodePath,
390
+ serverValue: serverText,
391
+ clientValue: clientText,
392
+ message: "Text content differs between server and client."
393
+ });
394
+ if (out.length >= max) return;
395
+ }
216
396
  const serverChildren = Array.from(serverNode.children);
217
397
  const clientChildren = Array.from(clientNode.children);
218
398
  const max2 = Math.max(serverChildren.length, clientChildren.length);
@@ -232,10 +412,11 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
232
412
  if (!Object.hasOwn(attrs, rawKey)) continue;
233
413
  if (!isSafeAttrName(rawKey)) continue;
234
414
  if (!allowEventHandlers && isEventHandlerAttr(rawKey)) continue;
415
+ if (isForbiddenSsrAttr(rawKey)) continue;
235
416
  const lowerKey = rawKey.toLowerCase();
236
417
  let value = String(attrs[rawKey]);
237
- if (URL_ATTRS.has(lowerKey)) {
238
- value = sanitizeUrlAttr(lowerKey, value);
418
+ if (isPolicyAttr(lowerKey)) {
419
+ value = sanitizeSsrAttributeValue(lowerKey, value);
239
420
  if (!value) continue;
240
421
  }
241
422
  out.push(`${rawKey}="${escapeAttr(value)}"`);
@@ -243,18 +424,7 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
243
424
  return out.join(" ");
244
425
  }
245
426
  function isDangerousMetaRefresh(metaProps) {
246
- let httpEquiv;
247
- let content;
248
- for (const k in metaProps) {
249
- const lk = k.toLowerCase();
250
- if (lk === "http-equiv") httpEquiv = metaProps[k];
251
- else if (lk === "content") content = metaProps[k];
252
- }
253
- if (typeof httpEquiv !== "string") return false;
254
- if (httpEquiv.toLowerCase() !== "refresh") return false;
255
- if (typeof content !== "string") return false;
256
- const normalized = stripControlChars(content).toLowerCase();
257
- return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
427
+ return isForbiddenMetaEntry(metaProps);
258
428
  }
259
429
  function renderToDocument(component, options = {}) {
260
430
  let content;
@@ -263,15 +433,8 @@ function renderToDocument(component, options = {}) {
263
433
  } catch (err) {
264
434
  content = ssrErrorComment(err);
265
435
  }
266
- const metaTags = (options.meta || []).map((attrs) => {
267
- if (isDangerousMetaRefresh(attrs)) return "";
268
- const pairs = buildAttrString(attrs);
269
- return pairs ? `<meta ${pairs} />` : "";
270
- }).filter(Boolean).join("\n ");
271
- const linkTags = (options.links || []).map((attrs) => {
272
- const pairs = buildAttrString(attrs);
273
- return pairs ? `<link ${pairs} />` : "";
274
- }).filter(Boolean).join("\n ");
436
+ const metaTags = (options.meta || []).map((attrs) => serializeHeadEntry("meta", attrs)).filter(Boolean).join("\n ");
437
+ const linkTags = (options.links || []).map((attrs) => serializeHeadEntry("link", attrs)).filter(Boolean).join("\n ");
275
438
  const scriptTags = (options.scripts || []).map((src) => {
276
439
  const safe = sanitizeUrl(String(src));
277
440
  if (!safe) return "";
@@ -332,14 +495,18 @@ async function* renderToStream(element) {
332
495
  const rawName = attr.name;
333
496
  if (!isSafeAttrName(rawName)) continue;
334
497
  if (isEventHandlerAttr(rawName)) continue;
498
+ if (isForbiddenSsrAttr(rawName)) continue;
335
499
  const lowerName = rawName.toLowerCase();
336
500
  let value = attr.value;
337
- if (URL_ATTRS.has(lowerName)) {
338
- value = sanitizeUrlAttr(lowerName, value);
501
+ if (isPolicyAttr(lowerName)) {
502
+ value = sanitizeSsrAttributeValue(lowerName, value);
339
503
  if (!value) continue;
340
504
  }
341
505
  openTag += ` ${rawName}="${escapeAttr(value)}"`;
342
506
  }
507
+ if (element.dataset && !element.dataset.sibuHydrate) {
508
+ openTag += ` data-sibu-ssr="true"`;
509
+ }
343
510
  if (VOID_ELEMENTS.has(tag)) {
344
511
  yield `${openTag} />`;
345
512
  return;
@@ -386,9 +553,13 @@ function island(id, component) {
386
553
  el.setAttribute("data-sibu-island", id);
387
554
  return el;
388
555
  }
556
+ function isAlreadyHydrated(element) {
557
+ return element.hasAttribute("data-sibu-hydrated");
558
+ }
389
559
  function hydrateIslands(container, islands) {
390
560
  const markers = container.querySelectorAll("[data-sibu-island]");
391
561
  for (const marker of Array.from(markers)) {
562
+ if (isAlreadyHydrated(marker)) continue;
392
563
  const id = marker.getAttribute("data-sibu-island") ?? "";
393
564
  if (!Object.hasOwn(islands, id)) continue;
394
565
  const factory = islands[id];
@@ -396,7 +567,7 @@ function hydrateIslands(container, islands) {
396
567
  const clientTree = factory();
397
568
  clientTree.setAttribute("data-sibu-island", id);
398
569
  clientTree.setAttribute("data-sibu-hydrated", "true");
399
- marker.replaceWith(clientTree);
570
+ disposeAndReplace(marker, clientTree);
400
571
  }
401
572
  container.setAttribute("data-sibu-hydrated", "partial");
402
573
  }
@@ -404,6 +575,7 @@ function hydrateProgressively(container, islands, options) {
404
575
  const markers = container.querySelectorAll("[data-sibu-island]");
405
576
  const cleanups = [];
406
577
  for (const marker of Array.from(markers)) {
578
+ if (isAlreadyHydrated(marker)) continue;
407
579
  const id = marker.getAttribute("data-sibu-island") ?? "";
408
580
  if (!Object.hasOwn(islands, id)) continue;
409
581
  const factory = islands[id];
@@ -413,10 +585,11 @@ function hydrateProgressively(container, islands, options) {
413
585
  for (const entry of entries) {
414
586
  if (entry.isIntersecting) {
415
587
  observer.disconnect();
588
+ if (isAlreadyHydrated(marker) || marker.parentNode === null) break;
416
589
  const clientTree = factory();
417
590
  clientTree.setAttribute("data-sibu-island", id);
418
591
  clientTree.setAttribute("data-sibu-hydrated", "true");
419
- marker.replaceWith(clientTree);
592
+ disposeAndReplace(marker, clientTree);
420
593
  break;
421
594
  }
422
595
  }
@@ -472,7 +645,7 @@ function suspenseSwapScript(id, nonce) {
472
645
  );
473
646
  }
474
647
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
475
- return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
648
+ return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(f.firstChild)f.removeChild(f.firstChild);while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
476
649
  }
477
650
  async function* renderToSuspenseStream(element, pendingBoundaries = [], options) {
478
651
  yield* renderToStream(element);
@@ -522,6 +695,10 @@ function escapeAttr(str) {
522
695
  }
523
696
 
524
697
  export {
698
+ planMetaEntry,
699
+ planHeadElementEntry,
700
+ STATIC_VALUE_RESOLVER,
701
+ serializeHeadEntry,
525
702
  renderToString,
526
703
  hydrate,
527
704
  trustHTML,
@@ -1,13 +1,19 @@
1
1
  import {
2
2
  isUnsafeKey
3
3
  } from "./chunk-H3SRKIYX.js";
4
+ import {
5
+ acquireTitle
6
+ } from "./chunk-52XFPGSN.js";
4
7
  import {
5
8
  effect
6
- } from "./chunk-X4JS367G.js";
9
+ } from "./chunk-XDOMTQHM.js";
7
10
  import {
8
11
  batch,
9
12
  signal
10
- } from "./chunk-335KJEN2.js";
13
+ } from "./chunk-TVURNWDK.js";
14
+ import {
15
+ reportError
16
+ } from "./chunk-VPP2FONR.js";
11
17
 
12
18
  // src/browser/media.ts
13
19
  function media(query) {
@@ -280,6 +286,7 @@ function permissions(name) {
280
286
  };
281
287
  status.addEventListener("change", onChange);
282
288
  }).catch(() => {
289
+ if (disposed) return;
283
290
  setState("unsupported");
284
291
  });
285
292
  function dispose() {
@@ -298,20 +305,25 @@ function clipboard() {
298
305
  const [text, setText] = signal("");
299
306
  const [copied, setCopied] = signal(false);
300
307
  let copiedTimer = null;
308
+ let disposed = false;
301
309
  async function copy(value) {
310
+ if (disposed) return;
302
311
  if (typeof navigator === "undefined" || !navigator.clipboard) {
303
312
  return;
304
313
  }
305
314
  await navigator.clipboard.writeText(value);
315
+ if (disposed) return;
306
316
  setText(value);
307
317
  setCopied(true);
308
318
  if (copiedTimer !== null) clearTimeout(copiedTimer);
309
319
  copiedTimer = setTimeout(() => {
320
+ if (disposed) return;
310
321
  setCopied(false);
311
322
  copiedTimer = null;
312
323
  }, 2e3);
313
324
  }
314
325
  function dispose() {
326
+ disposed = true;
315
327
  if (copiedTimer !== null) {
316
328
  clearTimeout(copiedTimer);
317
329
  copiedTimer = null;
@@ -437,20 +449,21 @@ function title(value) {
437
449
  return () => {
438
450
  };
439
451
  }
440
- const previousTitle = document.title;
441
452
  if (typeof value === "function") {
453
+ let lease2 = null;
442
454
  const cleanup = effect(() => {
443
- document.title = value();
455
+ const next = value();
456
+ if (lease2) lease2.set(next);
457
+ else lease2 = acquireTitle(next);
444
458
  });
445
459
  return () => {
446
460
  cleanup();
447
- document.title = previousTitle;
461
+ lease2?.release();
462
+ lease2 = null;
448
463
  };
449
464
  }
450
- document.title = value;
451
- return () => {
452
- document.title = previousTitle;
453
- };
465
+ const lease = acquireTitle(value);
466
+ return () => lease.release();
454
467
  }
455
468
 
456
469
  // src/browser/colorScheme.ts
@@ -764,51 +777,112 @@ function wakeLock() {
764
777
  };
765
778
  }
766
779
  const api = navigator.wakeLock;
767
- let sentinel = null;
780
+ let current = null;
781
+ let desire = false;
782
+ let generation = 0;
783
+ let inflight = null;
768
784
  let disposed = false;
785
+ function publishActive(value) {
786
+ if (disposed) return;
787
+ setActive(value);
788
+ }
789
+ function syncReleasedState() {
790
+ if (current?.released) {
791
+ current = null;
792
+ publishActive(false);
793
+ }
794
+ }
795
+ function discard(sentinel) {
796
+ if (sentinel.released) return;
797
+ sentinel.release().catch((err) => {
798
+ reportError(err, { phase: "async", name: "wakeLock(discard)" });
799
+ });
800
+ }
769
801
  async function request() {
770
802
  if (disposed) return;
771
- try {
772
- const s = await api.request("screen");
773
- if (disposed) {
774
- s.release().catch(() => {
775
- });
776
- return;
777
- }
778
- sentinel = s;
779
- setActive(true);
780
- sentinel.addEventListener("release", () => {
781
- setActive(false);
782
- });
783
- } catch {
784
- setActive(false);
803
+ desire = true;
804
+ syncReleasedState();
805
+ if (current && !current.released) {
806
+ publishActive(true);
807
+ return;
785
808
  }
809
+ if (inflight) return inflight;
810
+ const myGeneration = ++generation;
811
+ const attempt = (async () => {
812
+ try {
813
+ const sentinel = await api.request("screen");
814
+ if (disposed || !desire || generation !== myGeneration) {
815
+ discard(sentinel);
816
+ return;
817
+ }
818
+ if (sentinel.released) {
819
+ publishActive(false);
820
+ return;
821
+ }
822
+ try {
823
+ sentinel.addEventListener("release", () => {
824
+ if (current !== sentinel) return;
825
+ current = null;
826
+ publishActive(false);
827
+ });
828
+ } catch (listenerError) {
829
+ discard(sentinel);
830
+ publishActive(false);
831
+ reportError(listenerError, { phase: "async", name: "wakeLock(listener registration)" });
832
+ return;
833
+ }
834
+ if (sentinel.released) {
835
+ publishActive(false);
836
+ return;
837
+ }
838
+ current = sentinel;
839
+ publishActive(true);
840
+ } catch (err) {
841
+ if (generation === myGeneration) publishActive(false);
842
+ reportError(err, { phase: "async", name: "wakeLock(request)" });
843
+ }
844
+ })();
845
+ inflight = attempt;
846
+ void attempt.finally(() => {
847
+ if (inflight === attempt) inflight = null;
848
+ }).catch(() => {
849
+ });
850
+ return attempt;
786
851
  }
787
852
  async function release() {
788
- if (sentinel && !sentinel.released) {
789
- await sentinel.release();
853
+ desire = false;
854
+ generation++;
855
+ inflight = null;
856
+ const held = current;
857
+ current = null;
858
+ publishActive(false);
859
+ if (held && !held.released) {
860
+ await held.release();
790
861
  }
791
- sentinel = null;
792
- setActive(false);
793
862
  }
794
863
  const onVisibility = () => {
795
- if (sentinel?.released && !document.hidden) {
796
- request().catch((err) => {
797
- if (typeof console !== "undefined") {
798
- console.warn("[SibuJS wakeLock] re-acquire failed:", err);
799
- }
800
- });
801
- }
864
+ syncReleasedState();
865
+ if (!desire || current || document.hidden) return;
866
+ request().catch((err) => {
867
+ reportError(err, { phase: "async", name: "wakeLock(visibility re-acquire)" });
868
+ });
802
869
  };
803
870
  document.addEventListener("visibilitychange", onVisibility);
804
871
  function dispose() {
805
- disposed = true;
872
+ if (disposed) return;
873
+ desire = false;
874
+ generation++;
875
+ inflight = null;
806
876
  document.removeEventListener("visibilitychange", onVisibility);
807
- release().catch((err) => {
808
- if (typeof console !== "undefined") {
809
- console.warn("[SibuJS wakeLock] release failed:", err);
810
- }
811
- });
877
+ const held = current;
878
+ current = null;
879
+ setActive(false);
880
+ disposed = true;
881
+ if (held && !held.released) {
882
+ held.release().catch((err) => {
883
+ reportError(err, { phase: "async", name: "wakeLock(dispose release)" });
884
+ });
885
+ }
812
886
  }
813
887
  return { active, request, release, dispose };
814
888
  }
@@ -1,22 +1,22 @@
1
1
  import {
2
2
  watch
3
- } from "./chunk-QHPUFNHZ.js";
3
+ } from "./chunk-PTLJ7NQX.js";
4
4
  import {
5
5
  createId
6
6
  } from "./chunk-CCSJMTRN.js";
7
7
  import {
8
8
  derived
9
- } from "./chunk-4I3M326N.js";
9
+ } from "./chunk-AD5YSZAN.js";
10
10
  import {
11
11
  stripHtml
12
- } from "./chunk-L3GAGWCC.js";
12
+ } from "./chunk-5INI7D2L.js";
13
13
  import {
14
14
  effect
15
- } from "./chunk-X4JS367G.js";
15
+ } from "./chunk-XDOMTQHM.js";
16
16
  import {
17
17
  batch,
18
18
  signal
19
- } from "./chunk-335KJEN2.js";
19
+ } from "./chunk-TVURNWDK.js";
20
20
 
21
21
  // src/widgets/Combobox.ts
22
22
  var boundComboboxes = /* @__PURE__ */ new WeakMap();