@pyreon/runtime-dom 0.21.0 → 0.23.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.
package/lib/index.js CHANGED
@@ -1,85 +1,8 @@
1
- import { activateReactiveDevtools, batch, deactivateReactiveDevtools, effect, effectScope, getReactiveFires, getReactiveGraph, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
2
- import { EMPTY_PROPS, ForSymbol, Fragment, PortalSymbol, captureContextStack, createRef, cx, dispatchToErrorBoundary, h, makeReactiveProps, nativeCompat, normalizeStyleValue, onMount, onUnmount, propagateError, reportError, restoreContextStack, runWithHooks, toKebabCase } from "@pyreon/core";
1
+ import { a as applyProps, c as mountReactive, d as delegatedPropName, f as setupDelegation, i as applyProp, l as installDevTools, n as mountChild, o as sanitizeHtml, r as _bindEvent, s as setSanitizer, t as KeepAlive, u as DELEGATED_EVENTS } from "./_chunks/keep-alive-BM7bn3W9.js";
2
+ import { Transition } from "./transition-entry.js";
3
+ import { effect, effectScope, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
4
+ import { ForSymbol, Fragment, PortalSymbol, createRef, dispatchToErrorBoundary, h, makeReactiveProps, nativeCompat, onMount, onUnmount, reportError, runWithHooks } from "@pyreon/core";
3
5
 
4
- //#region src/delegate.ts
5
- /**
6
- * Event delegation — single listener per event type on the mount container.
7
- *
8
- * Instead of calling addEventListener on every element, the compiler emits
9
- * `el.__click = handler` (expando property). A single delegated listener on the
10
- * container walks event.target up the DOM tree, checking for expandos.
11
- *
12
- * Benefits:
13
- * - Saves ~2000 addEventListener calls for 1000 rows with 2 handlers each
14
- * - Reduces memory per row (no per-element listener closure)
15
- * - Faster initial mount (~0.4-0.8ms savings on 1000-row benchmarks)
16
- */
17
- /**
18
- * Events that are delegated (common bubbling events).
19
- * Non-bubbling events (focus, blur, mouseenter, mouseleave, load, error, scroll)
20
- * are NOT delegated — they must use addEventListener.
21
- */
22
- const DELEGATED_EVENTS = new Set([
23
- "click",
24
- "dblclick",
25
- "contextmenu",
26
- "focusin",
27
- "focusout",
28
- "input",
29
- "change",
30
- "keydown",
31
- "keyup",
32
- "mousedown",
33
- "mouseup",
34
- "mousemove",
35
- "mouseover",
36
- "mouseout",
37
- "pointerdown",
38
- "pointerup",
39
- "pointermove",
40
- "pointerover",
41
- "pointerout",
42
- "touchstart",
43
- "touchend",
44
- "touchmove",
45
- "submit"
46
- ]);
47
- /**
48
- * Property name used on DOM elements to store delegated event handlers.
49
- * Format: `__ev_{eventName}` e.g. `__ev_click`, `__ev_input`
50
- */
51
- function delegatedPropName(eventName) {
52
- return `__ev_${eventName}`;
53
- }
54
- const _delegated = /* @__PURE__ */ new WeakSet();
55
- /**
56
- * Install delegation listeners on a container element.
57
- * Called once from mount(). Idempotent — safe to call multiple times.
58
- */
59
- function setupDelegation(container) {
60
- if (_delegated.has(container)) return;
61
- _delegated.add(container);
62
- for (const eventName of DELEGATED_EVENTS) {
63
- const prop = delegatedPropName(eventName);
64
- container.addEventListener(eventName, (e) => {
65
- let el = e.target;
66
- while (el && el !== container) {
67
- const handler = el[prop];
68
- if (typeof handler === "function") {
69
- Object.defineProperty(e, "currentTarget", {
70
- value: el,
71
- configurable: true
72
- });
73
- batch(() => handler(e));
74
- if (e.cancelBubble) break;
75
- }
76
- el = el.parentElement;
77
- }
78
- });
79
- }
80
- }
81
-
82
- //#endregion
83
6
  //#region src/hydration-debug.ts
84
7
  let _enabled = process.env.NODE_ENV !== "production";
85
8
  function enableHydrationWarnings() {
@@ -127,1526 +50,6 @@ function warnHydrationMismatch(type, expected, actual, path) {
127
50
  }
128
51
  }
129
52
 
130
- //#endregion
131
- //#region src/devtools.ts
132
- /**
133
- * Pyreon DevTools — exposes a `__PYREON_DEVTOOLS__` global hook for browser devtools extensions
134
- * and in-app debugging utilities.
135
- *
136
- * Installed automatically on first `mount()` call in the browser.
137
- * No-op on the server (typeof window === "undefined").
138
- *
139
- * Usage:
140
- * window.__PYREON_DEVTOOLS__.getComponentTree() // root component entries
141
- * window.__PYREON_DEVTOOLS__.getAllComponents() // flat list of all live components
142
- * window.__PYREON_DEVTOOLS__.highlight("comp-id") // outline a component's DOM node
143
- * window.__PYREON_DEVTOOLS__.onComponentMount(cb) // subscribe to mount events
144
- * window.__PYREON_DEVTOOLS__.onComponentUnmount(cb)// subscribe to unmount events
145
- * window.__PYREON_DEVTOOLS__.enableOverlay() // Ctrl+Shift+P: hover to inspect components
146
- * window.__PYREON_DEVTOOLS__.reactive.activate() // opt-in: track the live signal/effect graph
147
- * window.__PYREON_DEVTOOLS__.reactive.getGraph() // snapshot of signals/derived/effects + edges
148
- */
149
- const _components = /* @__PURE__ */ new Map();
150
- const _mountListeners = [];
151
- const _unmountListeners = [];
152
- function registerComponent(id, name, el, parentId) {
153
- const entry = {
154
- id,
155
- name,
156
- el,
157
- parentId,
158
- childIds: []
159
- };
160
- _components.set(id, entry);
161
- if (parentId) {
162
- const parent = _components.get(parentId);
163
- if (parent) parent.childIds.push(id);
164
- }
165
- for (const cb of _mountListeners) cb(entry);
166
- }
167
- function unregisterComponent(id) {
168
- const entry = _components.get(id);
169
- if (!entry) return;
170
- if (entry.parentId) {
171
- const parent = _components.get(entry.parentId);
172
- if (parent) parent.childIds = parent.childIds.filter((c) => c !== id);
173
- }
174
- _components.delete(id);
175
- for (const cb of _unmountListeners) cb(id);
176
- }
177
- let _overlayActive = false;
178
- let _overlayEl = null;
179
- let _tooltipEl = null;
180
- let _currentHighlight = null;
181
- function findComponentForElement(el) {
182
- let node = el;
183
- while (node) {
184
- for (const entry of _components.values()) if (entry.el === node) return entry;
185
- node = node.parentElement;
186
- }
187
- return null;
188
- }
189
- function createOverlayElements() {
190
- if (_overlayEl) return;
191
- _overlayEl = document.createElement("div");
192
- _overlayEl.id = "__pyreon-overlay";
193
- _overlayEl.style.cssText = "position:fixed;pointer-events:none;border:2px solid #00b4d8;border-radius:3px;z-index:999999;display:none;transition:all 0.08s ease-out;";
194
- _tooltipEl = document.createElement("div");
195
- _tooltipEl.style.cssText = "position:fixed;pointer-events:none;background:#1a1a2e;color:#e0e0e0;font:12px/1.4 ui-monospace,monospace;padding:6px 10px;border-radius:4px;z-index:999999;display:none;box-shadow:0 2px 8px rgba(0,0,0,0.3);max-width:400px;white-space:pre-wrap;";
196
- document.body.appendChild(_overlayEl);
197
- document.body.appendChild(_tooltipEl);
198
- }
199
- function positionOverlay(rect) {
200
- if (!_overlayEl) return;
201
- _overlayEl.style.display = "block";
202
- _overlayEl.style.top = `${rect.top}px`;
203
- _overlayEl.style.left = `${rect.left}px`;
204
- _overlayEl.style.width = `${rect.width}px`;
205
- _overlayEl.style.height = `${rect.height}px`;
206
- }
207
- function positionTooltip(entry, rect) {
208
- if (!_tooltipEl) return;
209
- const childCount = entry.childIds.length;
210
- let info = `<${entry.name}>`;
211
- if (childCount > 0) info += `\n ${childCount} child component${childCount === 1 ? "" : "s"}`;
212
- _tooltipEl.textContent = info;
213
- _tooltipEl.style.display = "block";
214
- _tooltipEl.style.top = `${rect.top - 30}px`;
215
- _tooltipEl.style.left = `${rect.left}px`;
216
- if (rect.top < 35) _tooltipEl.style.top = `${rect.bottom + 4}px`;
217
- }
218
- function hideOverlayElements() {
219
- if (_overlayEl) _overlayEl.style.display = "none";
220
- if (_tooltipEl) _tooltipEl.style.display = "none";
221
- _currentHighlight = null;
222
- }
223
- /** @internal — exported for testing only */
224
- function onOverlayMouseMove(e) {
225
- const target = document.elementFromPoint(e.clientX, e.clientY);
226
- if (!target || target === _overlayEl || target === _tooltipEl) return;
227
- const entry = findComponentForElement(target);
228
- if (!entry?.el) {
229
- hideOverlayElements();
230
- return;
231
- }
232
- if (entry.el === _currentHighlight) return;
233
- _currentHighlight = entry.el;
234
- const rect = entry.el.getBoundingClientRect();
235
- positionOverlay(rect);
236
- positionTooltip(entry, rect);
237
- }
238
- /** @internal — exported for testing only */
239
- function onOverlayClick(e) {
240
- e.preventDefault();
241
- e.stopPropagation();
242
- const target = document.elementFromPoint(e.clientX, e.clientY);
243
- if (!target) return;
244
- const entry = findComponentForElement(target);
245
- if (entry) {
246
- console.group(`[Pyreon] <${entry.name}>`);
247
- console.log("element:", entry.el);
248
- console.log("children:", entry.childIds.length);
249
- if (entry.parentId) {
250
- const parent = _components.get(entry.parentId);
251
- if (parent) console.log("parent:", `<${parent.name}>`);
252
- }
253
- console.groupEnd();
254
- }
255
- disableOverlay();
256
- }
257
- function onOverlayKeydown(e) {
258
- if (e.key === "Escape") disableOverlay();
259
- }
260
- function enableOverlay() {
261
- if (_overlayActive) return;
262
- _overlayActive = true;
263
- createOverlayElements();
264
- document.addEventListener("mousemove", onOverlayMouseMove, true);
265
- document.addEventListener("click", onOverlayClick, true);
266
- document.addEventListener("keydown", onOverlayKeydown, true);
267
- document.body.style.cursor = "crosshair";
268
- }
269
- function disableOverlay() {
270
- if (!_overlayActive) return;
271
- _overlayActive = false;
272
- document.removeEventListener("mousemove", onOverlayMouseMove, true);
273
- document.removeEventListener("click", onOverlayClick, true);
274
- document.removeEventListener("keydown", onOverlayKeydown, true);
275
- document.body.style.cursor = "";
276
- if (_overlayEl) _overlayEl.style.display = "none";
277
- if (_tooltipEl) _tooltipEl.style.display = "none";
278
- _currentHighlight = null;
279
- }
280
- let _installed = false;
281
- const _hasWindow = typeof window !== "undefined";
282
- function installDevTools() {
283
- if (!_hasWindow || _installed) return;
284
- _installed = true;
285
- const devtools = {
286
- version: "0.1.0",
287
- getComponentTree() {
288
- return Array.from(_components.values()).filter((e) => e.parentId === null);
289
- },
290
- getAllComponents() {
291
- return Array.from(_components.values());
292
- },
293
- highlight(id) {
294
- const entry = _components.get(id);
295
- if (!entry?.el) return;
296
- const el = entry.el;
297
- const prev = el.style.outline;
298
- el.style.outline = "2px solid #00b4d8";
299
- setTimeout(() => {
300
- el.style.outline = prev;
301
- }, 1500);
302
- },
303
- onComponentMount(cb) {
304
- _mountListeners.push(cb);
305
- return () => {
306
- const i = _mountListeners.indexOf(cb);
307
- if (i >= 0) _mountListeners.splice(i, 1);
308
- };
309
- },
310
- onComponentUnmount(cb) {
311
- _unmountListeners.push(cb);
312
- return () => {
313
- const i = _unmountListeners.indexOf(cb);
314
- if (i >= 0) _unmountListeners.splice(i, 1);
315
- };
316
- },
317
- enableOverlay,
318
- disableOverlay,
319
- reactive: {
320
- activate: activateReactiveDevtools,
321
- deactivate: deactivateReactiveDevtools,
322
- getGraph: getReactiveGraph,
323
- getFires: getReactiveFires
324
- }
325
- };
326
- window.__PYREON_DEVTOOLS__ = devtools;
327
- window.addEventListener("keydown", (e) => {
328
- if (e.ctrlKey && e.shiftKey && e.key === "P") {
329
- e.preventDefault();
330
- if (_overlayActive) disableOverlay();
331
- else enableOverlay();
332
- }
333
- });
334
- const win = window;
335
- win.$p = {
336
- /** List all mounted components */
337
- components: () => devtools.getAllComponents(),
338
- /** Component tree (roots only) */
339
- tree: () => devtools.getComponentTree(),
340
- /** Highlight a component by id */
341
- highlight: (id) => devtools.highlight(id),
342
- /** Toggle component inspector overlay */
343
- inspect: () => {
344
- if (_overlayActive) disableOverlay();
345
- else enableOverlay();
346
- },
347
- /** Print component count */
348
- stats: () => {
349
- const all = devtools.getAllComponents();
350
- const roots = devtools.getComponentTree();
351
- console.log(`[Pyreon] ${all.length} component${all.length === 1 ? "" : "s"}, ${roots.length} root${roots.length === 1 ? "" : "s"}`);
352
- return {
353
- total: all.length,
354
- roots: roots.length
355
- };
356
- },
357
- /** Quick help */
358
- help: () => {
359
- console.log("[Pyreon] $p commands:\n $p.components() — list all mounted components\n $p.tree() — component tree (roots only)\n $p.highlight(id)— outline a component\n $p.inspect() — toggle component inspector\n $p.stats() — print component count\n $p.help() — this message");
360
- }
361
- };
362
- }
363
-
364
- //#endregion
365
- //#region src/nodes.ts
366
- const __DEV__$5 = process.env.NODE_ENV !== "production";
367
- const _countSink$4 = globalThis;
368
- /**
369
- * Move all nodes strictly between `start` and `end` into a throwaway
370
- * DocumentFragment, detaching them from the live DOM in O(n) top-level moves.
371
- *
372
- * This is dramatically faster than Range.deleteContents() in JS-based DOMs
373
- * (happy-dom, jsdom) where deleting connected nodes with deep subtrees is O(n²).
374
- * In real browsers both approaches are similar, but the fragment approach is
375
- * never slower and avoids the pathological case.
376
- *
377
- * After this call every moved node has isConnected=false, so cleanup functions
378
- * that guard removeChild with `isConnected !== false` become no-ops.
379
- */
380
- function clearBetween(start, end) {
381
- const frag = document.createDocumentFragment();
382
- let cur = start.nextSibling;
383
- while (cur && cur !== end) {
384
- const next = cur.nextSibling;
385
- frag.appendChild(cur);
386
- cur = next;
387
- }
388
- }
389
- /** Emit `runtime.cleanup` once per registered mount cleanup that actually runs. */
390
- function _emitCleanup() {
391
- if (__DEV__$5) _countSink$4.__pyreon_count__?.("runtime.cleanup");
392
- }
393
- /**
394
- * Mount a reactive node whose content changes over time.
395
- *
396
- * A comment node is used as a stable anchor point in the DOM.
397
- * On each change: old nodes are removed, new ones inserted before the anchor.
398
- */
399
- function mountReactive(accessor, parent, anchor, mount) {
400
- if (__DEV__$5) _countSink$4.__pyreon_count__?.("runtime.mountReactive");
401
- const marker = document.createComment("pyreon");
402
- parent.insertBefore(marker, anchor);
403
- const contextSnapshot = captureContextStack();
404
- let currentCleanup = () => {};
405
- let hasCleanup = false;
406
- let generation = 0;
407
- const e = effect(() => {
408
- const myGen = ++generation;
409
- if (hasCleanup) _emitCleanup();
410
- runUntracked(() => currentCleanup());
411
- currentCleanup = () => {};
412
- hasCleanup = false;
413
- const value = accessor();
414
- if (value != null && value !== false) {
415
- const cleanup = runUntracked(() => restoreContextStack(contextSnapshot, () => mount(value, parent, marker)));
416
- if (myGen === generation) {
417
- currentCleanup = cleanup;
418
- hasCleanup = true;
419
- } else {
420
- _emitCleanup();
421
- cleanup();
422
- }
423
- }
424
- });
425
- return () => {
426
- e.dispose();
427
- if (hasCleanup) _emitCleanup();
428
- currentCleanup();
429
- marker.parentNode?.removeChild(marker);
430
- };
431
- }
432
- const _keyedAnchors = /* @__PURE__ */ new WeakSet();
433
- function growLisArrays(lis, n) {
434
- if (n <= lis.pred.length) return lis;
435
- return {
436
- tails: new Int32Array(n + 16),
437
- tailIdx: new Int32Array(n + 16),
438
- pred: new Int32Array(n + 16),
439
- stay: new Uint8Array(n + 16)
440
- };
441
- }
442
- function computeKeyedLis(lis, n, newKeyOrder, curPos) {
443
- const { tails, tailIdx, pred } = lis;
444
- let lisLen = 0;
445
- let ops = 0;
446
- for (let i = 0; i < n; i++) {
447
- const key = newKeyOrder[i];
448
- if (key === void 0) continue;
449
- const v = curPos.get(key) ?? -1;
450
- if (v < 0) continue;
451
- let lo = 0;
452
- let hi = lisLen;
453
- while (lo < hi) {
454
- const mid = lo + hi >> 1;
455
- ops++;
456
- if (tails[mid] < v) lo = mid + 1;
457
- else hi = mid;
458
- }
459
- tails[lo] = v;
460
- tailIdx[lo] = i;
461
- if (lo > 0) pred[i] = tailIdx[lo - 1];
462
- if (lo === lisLen) lisLen++;
463
- }
464
- if (__DEV__$5 && ops > 0) _countSink$4.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
465
- return lisLen;
466
- }
467
- function markStayingEntries(lis, lisLen) {
468
- const { tailIdx, pred, stay } = lis;
469
- let cur = lisLen > 0 ? tailIdx[lisLen - 1] : -1;
470
- while (cur !== -1) {
471
- stay[cur] = 1;
472
- cur = pred[cur];
473
- }
474
- }
475
- function applyKeyedMoves(n, newKeyOrder, stay, cache, parent, tailMarker) {
476
- let cursor = tailMarker;
477
- for (let i = n - 1; i >= 0; i--) {
478
- const key = newKeyOrder[i];
479
- if (key === void 0) continue;
480
- const entry = cache.get(key);
481
- if (!entry) continue;
482
- if (!stay[i]) moveEntryBefore(parent, entry.anchor, cursor);
483
- cursor = entry.anchor;
484
- }
485
- }
486
- /** Grow LIS typed arrays if needed, then compute and apply reorder. */
487
- function keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker) {
488
- const grown = growLisArrays(lis, n);
489
- grown.pred.fill(-1, 0, n);
490
- grown.stay.fill(0, 0, n);
491
- markStayingEntries(grown, computeKeyedLis(grown, n, newKeyOrder, curPos));
492
- applyKeyedMoves(n, newKeyOrder, grown.stay, cache, parent, tailMarker);
493
- return grown;
494
- }
495
- function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
496
- const startMarker = document.createComment("");
497
- const tailMarker = document.createComment("");
498
- parent.insertBefore(startMarker, listAnchor);
499
- parent.insertBefore(tailMarker, listAnchor);
500
- const cache = /* @__PURE__ */ new Map();
501
- const curPos = /* @__PURE__ */ new Map();
502
- let currentKeyOrder = [];
503
- let lis = {
504
- tails: new Int32Array(16),
505
- tailIdx: new Int32Array(16),
506
- pred: new Int32Array(16),
507
- stay: new Uint8Array(16)
508
- };
509
- const collectKeyOrder = (newList) => {
510
- const newKeyOrder = [];
511
- const newKeySet = /* @__PURE__ */ new Set();
512
- for (const vnode of newList) {
513
- const key = vnode.key;
514
- if (key !== null && key !== void 0) {
515
- newKeyOrder.push(key);
516
- newKeySet.add(key);
517
- }
518
- }
519
- return {
520
- newKeyOrder,
521
- newKeySet
522
- };
523
- };
524
- const removeStaleEntries = (newKeySet) => {
525
- for (const [key, entry] of cache) {
526
- if (newKeySet.has(key)) continue;
527
- _emitCleanup();
528
- entry.cleanup();
529
- entry.anchor.parentNode?.removeChild(entry.anchor);
530
- cache.delete(key);
531
- curPos.delete(key);
532
- }
533
- };
534
- const mountNewEntries = (newList) => {
535
- for (const vnode of newList) {
536
- const key = vnode.key;
537
- if (key === null || key === void 0) continue;
538
- if (cache.has(key)) continue;
539
- const anchor = document.createComment("");
540
- _keyedAnchors.add(anchor);
541
- parent.insertBefore(anchor, tailMarker);
542
- const cleanup = mountVNode(vnode, parent, tailMarker);
543
- cache.set(key, {
544
- anchor,
545
- cleanup
546
- });
547
- }
548
- };
549
- const e = effect(() => {
550
- const newList = accessor();
551
- const n = newList.length;
552
- runUntracked(() => {
553
- if (n === 0 && cache.size > 0) {
554
- for (const entry of cache.values()) {
555
- _emitCleanup();
556
- entry.cleanup();
557
- }
558
- cache.clear();
559
- curPos.clear();
560
- currentKeyOrder = [];
561
- clearBetween(startMarker, tailMarker);
562
- return;
563
- }
564
- const { newKeyOrder, newKeySet } = collectKeyOrder(newList);
565
- removeStaleEntries(newKeySet);
566
- mountNewEntries(newList);
567
- if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
568
- curPos.clear();
569
- for (let i = 0; i < newKeyOrder.length; i++) {
570
- const k = newKeyOrder[i];
571
- if (k !== void 0) curPos.set(k, i);
572
- }
573
- currentKeyOrder = newKeyOrder;
574
- });
575
- });
576
- return () => {
577
- e.dispose();
578
- for (const entry of cache.values()) {
579
- _emitCleanup();
580
- entry.cleanup();
581
- entry.anchor.parentNode?.removeChild(entry.anchor);
582
- }
583
- cache.clear();
584
- startMarker.parentNode?.removeChild(startMarker);
585
- tailMarker.parentNode?.removeChild(tailMarker);
586
- };
587
- }
588
- /** Maximum number of displaced positions before falling back to full LIS. */
589
- const SMALL_K = 8;
590
- const _forAnchors = /* @__PURE__ */ new WeakSet();
591
- /** Try small-k reorder; returns true if handled, false if LIS fallback needed. */
592
- function trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker) {
593
- if (n !== currentKeys.length) return false;
594
- const diffs = [];
595
- for (let i = 0; i < n; i++) if (newKeys[i] !== currentKeys[i]) {
596
- diffs.push(i);
597
- if (diffs.length > SMALL_K) return false;
598
- }
599
- if (diffs.length > 0) smallKPlace(liveParent, diffs, newKeys, cache, tailMarker);
600
- for (const i of diffs) {
601
- const cached = cache.get(newKeys[i]);
602
- if (cached) cached.pos = i;
603
- }
604
- return true;
605
- }
606
- function computeForLis(lis, n, newKeys, cache) {
607
- const { tails, tailIdx, pred } = lis;
608
- let lisLen = 0;
609
- let ops = 0;
610
- let lastV = -1;
611
- for (let i = 0; i < n; i++) {
612
- const key = newKeys[i];
613
- const v = cache.get(key)?.pos ?? 0;
614
- if (v > lastV) {
615
- tails[lisLen] = v;
616
- tailIdx[lisLen] = i;
617
- if (lisLen > 0) pred[i] = tailIdx[lisLen - 1];
618
- lisLen++;
619
- lastV = v;
620
- continue;
621
- }
622
- let lo;
623
- if (v < lisLen && tails[v] === v) lo = v;
624
- else {
625
- lo = 0;
626
- let hi = lisLen;
627
- while (lo < hi) {
628
- const mid = lo + hi >> 1;
629
- ops++;
630
- if (tails[mid] < v) lo = mid + 1;
631
- else hi = mid;
632
- }
633
- }
634
- tails[lo] = v;
635
- tailIdx[lo] = i;
636
- if (lo > 0) pred[i] = tailIdx[lo - 1];
637
- }
638
- if (__DEV__$5 && ops > 0) _countSink$4.__pyreon_count__?.("runtime.mountFor.lisOps", ops);
639
- return lisLen;
640
- }
641
- function applyForMoves(n, newKeys, stay, cache, liveParent, tailMarker) {
642
- let cursor = tailMarker;
643
- for (let i = n - 1; i >= 0; i--) {
644
- const entry = cache.get(newKeys[i]);
645
- if (!entry) continue;
646
- if (!stay[i]) moveEntryBefore(liveParent, entry.anchor, cursor);
647
- cursor = entry.anchor;
648
- }
649
- }
650
- /** LIS-based reorder for mountFor. */
651
- function forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker) {
652
- const grown = growLisArrays(lis, n);
653
- grown.pred.fill(-1, 0, n);
654
- grown.stay.fill(0, 0, n);
655
- markStayingEntries(grown, computeForLis(grown, n, newKeys, cache));
656
- applyForMoves(n, newKeys, grown.stay, cache, liveParent, tailMarker);
657
- for (let i = 0; i < n; i++) {
658
- const cached = cache.get(newKeys[i]);
659
- if (cached) cached.pos = i;
660
- }
661
- return grown;
662
- }
663
- /**
664
- * Keyed reconciler that works directly on the source item array.
665
- *
666
- * Optimizations:
667
- * - Calls renderItem() only for NEW keys — 0 VNode allocations for reorders
668
- * - Small-k fast path: if <= SMALL_K positions changed, skips LIS
669
- * - Fast clear path: moves nodes to DocumentFragment for O(n) bulk detach
670
- * - Fresh render fast path: skips stale-check and reorder on first render
671
- */
672
- function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
673
- const startMarker = document.createComment("");
674
- const tailMarker = document.createComment("");
675
- parent.insertBefore(startMarker, anchor);
676
- parent.insertBefore(tailMarker, anchor);
677
- let cache = /* @__PURE__ */ new Map();
678
- let currentKeys = [];
679
- const _reusableKeySet = /* @__PURE__ */ new Set();
680
- let cleanupCount = 0;
681
- let anchorsRegistered = false;
682
- let lis = {
683
- tails: new Int32Array(16),
684
- tailIdx: new Int32Array(16),
685
- pred: new Int32Array(16),
686
- stay: new Uint8Array(16)
687
- };
688
- const warnForKey = (seen, key) => {
689
- if (!seen) return;
690
- if (__DEV__$5 && key == null) console.warn("[Pyreon] <For> `by` function returned null/undefined. Keys must be strings or numbers. Check your `by` prop.");
691
- if (seen.has(key)) {
692
- if (__DEV__$5) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
693
- return true;
694
- }
695
- seen.add(key);
696
- return false;
697
- };
698
- /** Render item into container, update cache+cleanupCount. No anchor registration. */
699
- const renderInto = (item, key, pos, container, before) => {
700
- const result = renderItem(item);
701
- if (result.__isNative) {
702
- const native = result;
703
- container.insertBefore(native.el, before);
704
- cache.set(key, {
705
- anchor: native.el,
706
- cleanup: native.cleanup,
707
- pos
708
- });
709
- if (native.cleanup) cleanupCount++;
710
- return;
711
- }
712
- const priorLast = before ? before.previousSibling : container.lastChild;
713
- const cl = mountChild(result, container, before);
714
- const firstMounted = priorLast ? priorLast.nextSibling : container.firstChild;
715
- if (!firstMounted || firstMounted === before) {
716
- const ph = document.createComment("");
717
- container.insertBefore(ph, before);
718
- cache.set(key, {
719
- anchor: ph,
720
- cleanup: cl,
721
- pos
722
- });
723
- } else cache.set(key, {
724
- anchor: firstMounted,
725
- cleanup: cl,
726
- pos
727
- });
728
- cleanupCount++;
729
- };
730
- const handleFreshRender = (items, n, liveParent) => {
731
- const frag = document.createDocumentFragment();
732
- const keys = new Array(n);
733
- const _seenKeys = /* @__PURE__ */ new Set();
734
- for (let i = 0; i < n; i++) {
735
- const item = items[i];
736
- const key = getKey(item);
737
- if (warnForKey(_seenKeys, key)) continue;
738
- keys[i] = key;
739
- renderInto(item, key, i, frag, null);
740
- }
741
- liveParent.insertBefore(frag, tailMarker);
742
- anchorsRegistered = false;
743
- currentKeys = keys;
744
- };
745
- const collectNewKeys = (items, n) => {
746
- const newKeys = new Array(n);
747
- const _seenUpdate = /* @__PURE__ */ new Set();
748
- for (let i = 0; i < n; i++) {
749
- newKeys[i] = getKey(items[i]);
750
- warnForKey(_seenUpdate, newKeys[i]);
751
- }
752
- return newKeys;
753
- };
754
- const handleReplaceAll = (items, n, newKeys, liveParent) => {
755
- if (cleanupCount > 0) {
756
- for (const entry of cache.values()) if (entry.cleanup) {
757
- _emitCleanup();
758
- entry.cleanup();
759
- }
760
- }
761
- cache = /* @__PURE__ */ new Map();
762
- cleanupCount = 0;
763
- const parentParent = liveParent.parentNode;
764
- const canSwap = parentParent && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker;
765
- const frag = document.createDocumentFragment();
766
- for (let i = 0; i < n; i++) renderInto(items[i], newKeys[i], i, frag, null);
767
- anchorsRegistered = false;
768
- if (canSwap) {
769
- const fresh = liveParent.cloneNode(false);
770
- fresh.appendChild(startMarker);
771
- fresh.appendChild(frag);
772
- fresh.appendChild(tailMarker);
773
- parentParent.replaceChild(fresh, liveParent);
774
- } else {
775
- clearBetween(startMarker, tailMarker);
776
- liveParent.insertBefore(frag, tailMarker);
777
- }
778
- currentKeys = newKeys;
779
- };
780
- const removeStaleForEntries = (newKeySet) => {
781
- for (const [key, entry] of cache) {
782
- if (newKeySet.has(key)) continue;
783
- if (entry.cleanup) {
784
- _emitCleanup();
785
- entry.cleanup();
786
- cleanupCount--;
787
- }
788
- entry.anchor.parentNode?.removeChild(entry.anchor);
789
- cache.delete(key);
790
- }
791
- };
792
- const mountNewForEntries = (items, n, newKeys, liveParent) => {
793
- for (let i = 0; i < n; i++) {
794
- const key = newKeys[i];
795
- if (cache.has(key)) continue;
796
- renderInto(items[i], key, i, liveParent, tailMarker);
797
- const entry = cache.get(key);
798
- if (entry) _forAnchors.add(entry.anchor);
799
- }
800
- };
801
- const handleFastClear = (liveParent) => {
802
- if (cache.size === 0) return;
803
- if (cleanupCount > 0) {
804
- for (const entry of cache.values()) if (entry.cleanup) {
805
- _emitCleanup();
806
- entry.cleanup();
807
- }
808
- }
809
- const pp = liveParent.parentNode;
810
- if (pp && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker) {
811
- const fresh = liveParent.cloneNode(false);
812
- fresh.appendChild(startMarker);
813
- fresh.appendChild(tailMarker);
814
- pp.replaceChild(fresh, liveParent);
815
- } else clearBetween(startMarker, tailMarker);
816
- cache = /* @__PURE__ */ new Map();
817
- cleanupCount = 0;
818
- currentKeys = [];
819
- };
820
- const hasAnyKeptKey = (n, newKeys) => {
821
- for (let i = 0; i < n; i++) if (cache.has(newKeys[i])) return true;
822
- return false;
823
- };
824
- const handleIncrementalUpdate = (items, n, newKeys, liveParent) => {
825
- _reusableKeySet.clear();
826
- for (let i = 0; i < newKeys.length; i++) _reusableKeySet.add(newKeys[i]);
827
- removeStaleForEntries(_reusableKeySet);
828
- mountNewForEntries(items, n, newKeys, liveParent);
829
- if (!anchorsRegistered) {
830
- for (const entry of cache.values()) _forAnchors.add(entry.anchor);
831
- anchorsRegistered = true;
832
- }
833
- if (trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker)) {
834
- currentKeys = newKeys;
835
- return;
836
- }
837
- lis = forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker);
838
- currentKeys = newKeys;
839
- };
840
- const e = effect(() => {
841
- const liveParent = startMarker.parentNode;
842
- if (!liveParent) return;
843
- const items = source();
844
- const n = items.length;
845
- runUntracked(() => {
846
- if (n === 0) {
847
- handleFastClear(liveParent);
848
- return;
849
- }
850
- if (currentKeys.length === 0) {
851
- handleFreshRender(items, n, liveParent);
852
- return;
853
- }
854
- const newKeys = collectNewKeys(items, n);
855
- if (!hasAnyKeptKey(n, newKeys)) {
856
- handleReplaceAll(items, n, newKeys, liveParent);
857
- return;
858
- }
859
- handleIncrementalUpdate(items, n, newKeys, liveParent);
860
- });
861
- });
862
- return () => {
863
- e.dispose();
864
- for (const entry of cache.values()) {
865
- if (cleanupCount > 0 && entry.cleanup) {
866
- _emitCleanup();
867
- entry.cleanup();
868
- }
869
- entry.anchor.parentNode?.removeChild(entry.anchor);
870
- }
871
- cache = /* @__PURE__ */ new Map();
872
- cleanupCount = 0;
873
- startMarker.parentNode?.removeChild(startMarker);
874
- tailMarker.parentNode?.removeChild(tailMarker);
875
- };
876
- }
877
- /**
878
- * Small-k reorder: directly place the k displaced entries without LIS.
879
- */
880
- function smallKPlace(parent, diffs, newKeys, cache, tailMarker) {
881
- const diffSet = new Set(diffs);
882
- let cursor = tailMarker;
883
- let prevDiffIdx = newKeys.length;
884
- for (let d = diffs.length - 1; d >= 0; d--) {
885
- const i = diffs[d];
886
- let nextNonDiff = -1;
887
- for (let j = i + 1; j < prevDiffIdx; j++) if (!diffSet.has(j)) {
888
- nextNonDiff = j;
889
- break;
890
- }
891
- if (nextNonDiff >= 0) {
892
- const nc = cache.get(newKeys[nextNonDiff])?.anchor;
893
- if (nc) cursor = nc;
894
- }
895
- const entry = cache.get(newKeys[i]);
896
- if (!entry) {
897
- prevDiffIdx = i;
898
- continue;
899
- }
900
- moveEntryBefore(parent, entry.anchor, cursor);
901
- cursor = entry.anchor;
902
- prevDiffIdx = i;
903
- }
904
- }
905
- /**
906
- * Move startNode and all siblings belonging to this entry to just before `before`.
907
- * Stops at the next entry anchor (identified via WeakSet) or the tail marker.
908
- *
909
- * Fast path: if the next sibling is already a boundary (another entry or tail),
910
- * this entry is a single node — skip the toMove array entirely.
911
- */
912
- function moveEntryBefore(parent, startNode, before) {
913
- const next = startNode.nextSibling;
914
- if (!next || next === before || next.parentNode === parent && (_forAnchors.has(next) || _keyedAnchors.has(next))) {
915
- parent.insertBefore(startNode, before);
916
- return;
917
- }
918
- const toMove = [startNode];
919
- let cur = next;
920
- while (cur && cur !== before) {
921
- const nextNode = cur.nextSibling;
922
- toMove.push(cur);
923
- cur = nextNode;
924
- if (cur && cur.parentNode === parent && (cur === before || _forAnchors.has(cur) || _keyedAnchors.has(cur))) break;
925
- }
926
- for (const node of toMove) parent.insertBefore(node, before);
927
- }
928
-
929
- //#endregion
930
- //#region src/props.ts
931
- const __DEV__$4 = process.env.NODE_ENV !== "production";
932
- const _countSink$3 = globalThis;
933
- let _customSanitizer = null;
934
- /**
935
- * Set a custom HTML sanitizer used by `innerHTML` and `sanitizeHtml()`.
936
- * Overrides both the Sanitizer API and the built-in fallback.
937
- *
938
- * @example
939
- * // With DOMPurify:
940
- * import DOMPurify from "dompurify"
941
- * setSanitizer((html) => DOMPurify.sanitize(html))
942
- *
943
- * // With sanitize-html:
944
- * import sanitize from "sanitize-html"
945
- * setSanitizer((html) => sanitize(html))
946
- *
947
- * // Reset to built-in:
948
- * setSanitizer(null)
949
- */
950
- function setSanitizer(fn) {
951
- _customSanitizer = fn;
952
- }
953
- const SAFE_TAGS = new Set([
954
- "a",
955
- "abbr",
956
- "address",
957
- "article",
958
- "aside",
959
- "b",
960
- "bdi",
961
- "bdo",
962
- "blockquote",
963
- "br",
964
- "caption",
965
- "cite",
966
- "code",
967
- "col",
968
- "colgroup",
969
- "dd",
970
- "del",
971
- "details",
972
- "dfn",
973
- "div",
974
- "dl",
975
- "dt",
976
- "em",
977
- "figcaption",
978
- "figure",
979
- "footer",
980
- "h1",
981
- "h2",
982
- "h3",
983
- "h4",
984
- "h5",
985
- "h6",
986
- "header",
987
- "hr",
988
- "i",
989
- "ins",
990
- "kbd",
991
- "li",
992
- "main",
993
- "mark",
994
- "nav",
995
- "ol",
996
- "p",
997
- "pre",
998
- "q",
999
- "rp",
1000
- "rt",
1001
- "ruby",
1002
- "s",
1003
- "samp",
1004
- "section",
1005
- "small",
1006
- "span",
1007
- "strong",
1008
- "sub",
1009
- "summary",
1010
- "sup",
1011
- "table",
1012
- "tbody",
1013
- "td",
1014
- "tfoot",
1015
- "th",
1016
- "thead",
1017
- "time",
1018
- "tr",
1019
- "u",
1020
- "ul",
1021
- "var",
1022
- "wbr"
1023
- ]);
1024
- const UNSAFE_ATTR_RE = /^on/i;
1025
- /**
1026
- * Fallback tag-stripping sanitizer for environments without the Sanitizer API.
1027
- * Removes all tags not in SAFE_TAGS, strips event handler attributes,
1028
- * and blocks javascript:/data: URLs in href/src/action attributes.
1029
- */
1030
- function fallbackSanitize(html) {
1031
- const doc = new DOMParser().parseFromString(html, "text/html");
1032
- sanitizeNode(doc.body);
1033
- return doc.body.innerHTML;
1034
- }
1035
- /** Strip unsafe attributes from a single element. */
1036
- function stripUnsafeAttrs(el) {
1037
- const attrs = Array.from(el.attributes);
1038
- for (const attr of attrs) if (UNSAFE_ATTR_RE.test(attr.name)) el.removeAttribute(attr.name);
1039
- else if (URL_ATTRS.has(attr.name) && UNSAFE_URL_RE.test(attr.value)) el.removeAttribute(attr.name);
1040
- }
1041
- function sanitizeNode(node) {
1042
- const children = Array.from(node.childNodes);
1043
- for (const child of children) {
1044
- if (child.nodeType !== 1) continue;
1045
- const el = child;
1046
- const tag = el.tagName.toLowerCase();
1047
- if (!SAFE_TAGS.has(tag)) {
1048
- const text = document.createTextNode(el.textContent);
1049
- node.replaceChild(text, el);
1050
- continue;
1051
- }
1052
- stripUnsafeAttrs(el);
1053
- sanitizeNode(el);
1054
- }
1055
- }
1056
- /**
1057
- * Sanitize an HTML string using the browser Sanitizer API (Chrome 105+).
1058
- * Falls back to a tag-allowlist sanitizer that strips unsafe elements and attributes.
1059
- */
1060
- function sanitizeHtml(html) {
1061
- if (_customSanitizer) return _customSanitizer(html);
1062
- return fallbackSanitize(html);
1063
- }
1064
- const EVENT_RE = /^on[A-Z]/;
1065
- /**
1066
- * Apply all props to a DOM element.
1067
- * Returns a single chained cleanup (or null if no props need teardown).
1068
- * Uses for-in instead of Object.keys() to avoid allocating a keys array.
1069
- */
1070
- function applyProps(el, props) {
1071
- let first = null;
1072
- let cleanups = null;
1073
- for (const key in props) {
1074
- if (key === "key" || key === "ref" || key === "children") continue;
1075
- const descriptor = Object.getOwnPropertyDescriptor(props, key);
1076
- let c;
1077
- if (descriptor?.get) c = renderEffect(() => applyStaticProp(el, key, props[key]));
1078
- else c = applyProp(el, key, props[key]);
1079
- if (c) if (!first) first = c;
1080
- else if (!cleanups) cleanups = [first, c];
1081
- else cleanups.push(c);
1082
- }
1083
- if (cleanups) return () => {
1084
- for (const c of cleanups) c();
1085
- };
1086
- return first;
1087
- }
1088
- /**
1089
- * Apply a single prop.
1090
- *
1091
- * - `onXxx` → addEventListener
1092
- * - `() => value` (non-event function) → reactive via effect
1093
- * - anything else → static attribute / DOM property
1094
- */
1095
- /**
1096
- * Bind an event handler (onClick → "click") with batching + delegation support.
1097
- */
1098
- function applyEventProp(el, key, value) {
1099
- if (__DEV__$4) _countSink$3.__pyreon_count__?.("runtime.applyEvent");
1100
- if (typeof value !== "function") {
1101
- if (__DEV__$4 && value != null) console.warn(`[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). Expected a function. Did you mean ${key}={() => ...}?`);
1102
- return null;
1103
- }
1104
- const eventName = (key[2]?.toLowerCase() + key.slice(3)).toLowerCase();
1105
- const handler = value;
1106
- if (DELEGATED_EVENTS.has(eventName)) {
1107
- const prop = delegatedPropName(eventName);
1108
- el[prop] = (e) => batch(() => handler(e));
1109
- return () => {
1110
- el[prop] = void 0;
1111
- };
1112
- }
1113
- const batched = (e) => batch(() => handler(e));
1114
- el.addEventListener(eventName, batched);
1115
- return () => el.removeEventListener(eventName, batched);
1116
- }
1117
- /**
1118
- * Bind ONE event handler through the CANONICAL event path
1119
- * (`applyEventProp` — the same delegation, batching, and exact
1120
- * `onXxx`→event-name normalization every compiler-emitted handler
1121
- * uses). PR 2 of the partial-collapse build (open-work #1): a
1122
- * collapsed-with-handler site (`_rsCollapseH`) re-attaches the residual
1123
- * handlers `detectPartialCollapsibleShape` (compiler PR 1) peeled off.
1124
- * Contract-consistent BY CONSTRUCTION — it IS `applyEventProp`, not a
1125
- * re-implementation — so a partially-collapsed `<Button onClick=…>`
1126
- * behaves byte-identically to the 5-layer mount it replaced (same
1127
- * delegated-event prop slot, same `batch()` wrapping, same cleanup).
1128
- */
1129
- function _bindEvent(el, key, handler) {
1130
- return applyEventProp(el, key, handler);
1131
- }
1132
- /**
1133
- * Sink for a single prop's CALLED value (always a primitive / object /
1134
- * `null` — never a function). Called both directly for static values and
1135
- * from the reactive `renderEffect` for accessor-bound values.
1136
- *
1137
- * NOTE on architecture: extracting the special-cased sinks
1138
- * (`innerHTML` / `dangerouslySetInnerHTML`) into this single dispatch
1139
- * function ensures every prop kind goes through the same reactive
1140
- * wrapping at `applyProp`'s entry. Previously each special case had its
1141
- * own early-return branch that needed to remember to handle function
1142
- * values; missing the dance once meant the closure was stringified and
1143
- * set as literal text. The structural fix (one reactive-wrap, then
1144
- * dispatch) eliminates the entire bug class.
1145
- */
1146
- function applyStaticProp(el, key, value) {
1147
- if (__DEV__$4 && typeof value === "function") console.warn(`[Pyreon] applyStaticProp received a function for "${key}". This likely means a new special-cased prop sink in applyProp() bypassed the reactive-wrap path. The closure would be stringified and set as a literal value. Verify the dispatch in applyProp().`);
1148
- if (key === "innerHTML") {
1149
- const html = String(value ?? "");
1150
- if (typeof el.setHTML === "function") el.setHTML(html);
1151
- else el.innerHTML = sanitizeHtml(html);
1152
- return;
1153
- }
1154
- if (key === "dangerouslySetInnerHTML") {
1155
- el.innerHTML = value?.__html ?? "";
1156
- return;
1157
- }
1158
- setStaticProp(el, key, value);
1159
- }
1160
- function applyProp(el, key, value) {
1161
- if (__DEV__$4) _countSink$3.__pyreon_count__?.("runtime.applyProp");
1162
- if (EVENT_RE.test(key)) return applyEventProp(el, key, value);
1163
- if (typeof value === "function") return renderEffect(() => applyStaticProp(el, key, value()));
1164
- applyStaticProp(el, key, value);
1165
- return null;
1166
- }
1167
- const URL_ATTRS = new Set([
1168
- "href",
1169
- "src",
1170
- "action",
1171
- "formaction",
1172
- "poster",
1173
- "cite",
1174
- "data"
1175
- ]);
1176
- const UNSAFE_URL_RE = /^\s*(?:javascript|data):/i;
1177
- const _prevStyleKeys = /* @__PURE__ */ new WeakMap();
1178
- /** Apply a style prop (string or object). */
1179
- function applyStyleProp(el, value) {
1180
- if (typeof value === "string") {
1181
- el.style.cssText = value;
1182
- _prevStyleKeys.delete(el);
1183
- return;
1184
- }
1185
- const prev = _prevStyleKeys.get(el);
1186
- if (value == null) {
1187
- if (prev) {
1188
- for (const propName of prev) el.style.removeProperty(propName);
1189
- _prevStyleKeys.delete(el);
1190
- }
1191
- return;
1192
- }
1193
- if (typeof value === "object") {
1194
- const obj = value;
1195
- const next = /* @__PURE__ */ new Set();
1196
- for (const k in obj) {
1197
- const propName = k.startsWith("--") ? k : toKebabCase(k);
1198
- next.add(propName);
1199
- const css = normalizeStyleValue(k, obj[k]);
1200
- el.style.setProperty(propName, css);
1201
- }
1202
- if (prev) {
1203
- for (const propName of prev) if (!next.has(propName)) el.style.removeProperty(propName);
1204
- }
1205
- if (next.size === 0) _prevStyleKeys.delete(el);
1206
- else _prevStyleKeys.set(el, next);
1207
- }
1208
- }
1209
- function applyClassProp(el, value) {
1210
- const resolved = typeof value === "string" ? value : cx(value);
1211
- el.setAttribute("class", resolved || "");
1212
- }
1213
- function setStaticProp(el, key, value) {
1214
- if (URL_ATTRS.has(key) && typeof value === "string" && UNSAFE_URL_RE.test(value)) {
1215
- if (__DEV__$4) console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`);
1216
- return;
1217
- }
1218
- if (key === "class" || key === "className") {
1219
- applyClassProp(el, value);
1220
- return;
1221
- }
1222
- if (key === "style") {
1223
- applyStyleProp(el, value);
1224
- return;
1225
- }
1226
- if (value == null) {
1227
- el.removeAttribute(key);
1228
- return;
1229
- }
1230
- if (typeof value === "boolean") {
1231
- if (value) el.setAttribute(key, "");
1232
- else el.removeAttribute(key);
1233
- return;
1234
- }
1235
- if (el.namespaceURI && el.namespaceURI !== "http://www.w3.org/1999/xhtml") {
1236
- el.setAttribute(key, String(value));
1237
- return;
1238
- }
1239
- if (key in el) {
1240
- el[key] = value;
1241
- return;
1242
- }
1243
- if (el.tagName.includes("-")) {
1244
- el[key] = value;
1245
- return;
1246
- }
1247
- el.setAttribute(key, String(value));
1248
- }
1249
-
1250
- //#endregion
1251
- //#region src/mount.ts
1252
- const __DEV__$3 = process.env.NODE_ENV !== "production";
1253
- const _countSink$2 = globalThis;
1254
- const noop$1 = () => {};
1255
- let _elementDepth = 0;
1256
- let _mountingStack;
1257
- if (__DEV__$3) _mountingStack = [];
1258
- /**
1259
- * Mount a single child into `parent`, inserting before `anchor` (null = append).
1260
- * Returns a cleanup that removes the node(s) and disposes all reactive effects.
1261
- *
1262
- * This function is the hot path — all child types are handled inline to avoid
1263
- * function call overhead in tight render loops (1000+ calls per list render).
1264
- */
1265
- function mountChild(child, parent, anchor = null) {
1266
- if (__DEV__$3) _countSink$2.__pyreon_count__?.("runtime.mountChild");
1267
- if (typeof child === "function") {
1268
- const sample = runUntracked(() => child());
1269
- if (isKeyedArray(sample)) {
1270
- const prevDepth = _elementDepth;
1271
- _elementDepth = 0;
1272
- const cleanup = mountKeyedList(child, parent, anchor, (v, p, a) => mountChild(v, p, a));
1273
- _elementDepth = prevDepth;
1274
- return cleanup;
1275
- }
1276
- if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
1277
- const text = document.createTextNode(sample === false ? "" : String(sample));
1278
- parent.insertBefore(text, anchor);
1279
- const dispose = renderEffect(() => {
1280
- const v = child();
1281
- const next = v == null || v === false ? "" : String(v);
1282
- if (next !== text.data) text.data = next;
1283
- });
1284
- if (_elementDepth > 0) return dispose;
1285
- return () => {
1286
- dispose();
1287
- const p = text.parentNode;
1288
- if (p && p.isConnected !== false) p.removeChild(text);
1289
- };
1290
- }
1291
- const prevDepth = _elementDepth;
1292
- _elementDepth = 0;
1293
- const cleanup = mountReactive(child, parent, anchor, mountChild);
1294
- _elementDepth = prevDepth;
1295
- return cleanup;
1296
- }
1297
- if (Array.isArray(child)) return mountChildren(child, parent, anchor);
1298
- if (child == null || child === false) return noop$1;
1299
- if (typeof child !== "object") {
1300
- parent.insertBefore(document.createTextNode(String(child)), anchor);
1301
- return noop$1;
1302
- }
1303
- if (child.__isNative) {
1304
- const native = child;
1305
- parent.insertBefore(native.el, anchor);
1306
- if (!native.cleanup) {
1307
- if (_elementDepth > 0) return noop$1;
1308
- return () => {
1309
- const p = native.el.parentNode;
1310
- if (p && p.isConnected !== false) p.removeChild(native.el);
1311
- };
1312
- }
1313
- if (_elementDepth > 0) return native.cleanup;
1314
- return () => {
1315
- native.cleanup?.();
1316
- const p = native.el.parentNode;
1317
- if (p && p.isConnected !== false) p.removeChild(native.el);
1318
- };
1319
- }
1320
- const vnode = child;
1321
- if (vnode.type === Fragment) return mountChildren(vnode.children ?? [], parent, anchor);
1322
- if (vnode.type === ForSymbol) {
1323
- const props = vnode.props;
1324
- const initialEach = props.each;
1325
- const source = typeof initialEach === "function" ? initialEach : (() => props.each);
1326
- const prevDepth = _elementDepth;
1327
- _elementDepth = 0;
1328
- const cleanup = mountFor(source, props.by, props.children, parent, anchor, mountChild);
1329
- _elementDepth = prevDepth;
1330
- return cleanup;
1331
- }
1332
- if (vnode.type === PortalSymbol) {
1333
- const { target, children } = vnode.props;
1334
- if (__DEV__$3 && !target) {
1335
- console.warn("[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.");
1336
- return noop$1;
1337
- }
1338
- if (__DEV__$3 && !(target instanceof Node)) console.warn(`[Pyreon] <Portal> target must be a DOM node. Received ${typeof target}. Use document.getElementById() or a ref to get the target element.`);
1339
- return mountChild(children, target, null);
1340
- }
1341
- if (typeof vnode.type === "function") return mountComponent(vnode, parent, anchor);
1342
- if (__DEV__$3 && typeof vnode.type !== "string") {
1343
- console.warn(`[Pyreon] Invalid VNode type: expected a string tag or component function, received ${typeof vnode.type} (${String(vnode.type)}). This usually means you passed an object or class instead of a component function.`);
1344
- return noop$1;
1345
- }
1346
- return mountElement(vnode, parent, anchor);
1347
- }
1348
- const VOID_ELEMENTS = new Set([
1349
- "area",
1350
- "base",
1351
- "br",
1352
- "col",
1353
- "embed",
1354
- "hr",
1355
- "img",
1356
- "input",
1357
- "link",
1358
- "meta",
1359
- "param",
1360
- "source",
1361
- "track",
1362
- "wbr"
1363
- ]);
1364
- const SVG_NS = "http://www.w3.org/2000/svg";
1365
- const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
1366
- const SVG_TAGS = new Set([
1367
- "svg",
1368
- "circle",
1369
- "ellipse",
1370
- "line",
1371
- "path",
1372
- "polygon",
1373
- "polyline",
1374
- "rect",
1375
- "g",
1376
- "defs",
1377
- "symbol",
1378
- "use",
1379
- "text",
1380
- "tspan",
1381
- "textPath",
1382
- "image",
1383
- "clipPath",
1384
- "mask",
1385
- "pattern",
1386
- "marker",
1387
- "linearGradient",
1388
- "radialGradient",
1389
- "stop",
1390
- "filter",
1391
- "feBlend",
1392
- "feColorMatrix",
1393
- "feComponentTransfer",
1394
- "feComposite",
1395
- "feConvolveMatrix",
1396
- "feDiffuseLighting",
1397
- "feDisplacementMap",
1398
- "feFlood",
1399
- "feGaussianBlur",
1400
- "feImage",
1401
- "feMerge",
1402
- "feMergeNode",
1403
- "feMorphology",
1404
- "feOffset",
1405
- "feSpecularLighting",
1406
- "feTile",
1407
- "feTurbulence",
1408
- "animate",
1409
- "animateMotion",
1410
- "animateTransform",
1411
- "set",
1412
- "desc",
1413
- "title",
1414
- "metadata",
1415
- "foreignObject"
1416
- ]);
1417
- const MATHML_TAGS = new Set([
1418
- "math",
1419
- "mi",
1420
- "mo",
1421
- "mn",
1422
- "ms",
1423
- "mtext",
1424
- "mspace",
1425
- "mrow",
1426
- "mfrac",
1427
- "msqrt",
1428
- "mroot",
1429
- "msub",
1430
- "msup",
1431
- "msubsup",
1432
- "munder",
1433
- "mover",
1434
- "munderover",
1435
- "mtable",
1436
- "mtr",
1437
- "mtd",
1438
- "mpadded",
1439
- "mphantom",
1440
- "menclose"
1441
- ]);
1442
- /** Track SVG context depth — children of <svg> inherit the SVG namespace. */
1443
- let _svgDepth = 0;
1444
- let _mathmlDepth = 0;
1445
- function createElementWithNS(tag) {
1446
- if (_svgDepth > 0 || SVG_TAGS.has(tag)) return document.createElementNS(SVG_NS, tag);
1447
- if (_mathmlDepth > 0 || MATHML_TAGS.has(tag)) return document.createElementNS(MATHML_NS, tag);
1448
- return document.createElement(tag);
1449
- }
1450
- function mountElement(vnode, parent, anchor) {
1451
- const tag = vnode.type;
1452
- const el = createElementWithNS(tag);
1453
- const isSvg = tag === "svg";
1454
- const isMathml = tag === "math";
1455
- if (isSvg) _svgDepth++;
1456
- if (isMathml) _mathmlDepth++;
1457
- if (__DEV__$3 && (vnode.children?.length ?? 0) > 0 && VOID_ELEMENTS.has(vnode.type)) console.warn(`[Pyreon] <${vnode.type}> is a void element and cannot have children. Children passed to void elements will be ignored by the browser.`);
1458
- const props = vnode.props;
1459
- const propCleanup = props !== EMPTY_PROPS ? applyProps(el, props) : null;
1460
- _elementDepth++;
1461
- const childCleanup = mountChildren(vnode.children ?? [], el, null);
1462
- _elementDepth--;
1463
- if (isSvg) _svgDepth--;
1464
- if (isMathml) _mathmlDepth--;
1465
- parent.insertBefore(el, anchor);
1466
- const ref = props.ref;
1467
- if (ref) if (typeof ref === "function") ref(el);
1468
- else ref.current = el;
1469
- if (!propCleanup && childCleanup === noop$1 && !ref) {
1470
- if (_elementDepth > 0) return noop$1;
1471
- return () => {
1472
- const p = el.parentNode;
1473
- if (p && p.isConnected !== false) p.removeChild(el);
1474
- };
1475
- }
1476
- if (_elementDepth > 0) {
1477
- if (!ref && !propCleanup) return childCleanup;
1478
- if (!ref && propCleanup) return () => {
1479
- propCleanup();
1480
- childCleanup();
1481
- };
1482
- const refToClean = ref;
1483
- return () => {
1484
- if (refToClean) if (typeof refToClean === "function") refToClean(null);
1485
- else refToClean.current = null;
1486
- if (propCleanup) propCleanup();
1487
- childCleanup();
1488
- };
1489
- }
1490
- return () => {
1491
- if (ref) if (typeof ref === "function") ref(null);
1492
- else ref.current = null;
1493
- if (propCleanup) propCleanup();
1494
- childCleanup();
1495
- const p = el.parentNode;
1496
- if (p && p.isConnected !== false) p.removeChild(el);
1497
- };
1498
- }
1499
- function mountComponent(vnode, parent, anchor) {
1500
- const scope = effectScope();
1501
- setCurrentScope(scope);
1502
- let hooks;
1503
- let output;
1504
- const componentName = vnode.type.name || "Anonymous";
1505
- let compId;
1506
- let devParentId;
1507
- if (__DEV__$3) {
1508
- compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`;
1509
- devParentId = _mountingStack[_mountingStack.length - 1] ?? null;
1510
- _mountingStack.push(compId);
1511
- }
1512
- const children = vnode.children ?? [];
1513
- const rawProps = children.length > 0 && vnode.props.children === void 0 ? {
1514
- ...vnode.props,
1515
- children: children.length === 1 ? children[0] : children
1516
- } : vnode.props;
1517
- const mergedProps = rawProps === EMPTY_PROPS ? rawProps : makeReactiveProps(rawProps);
1518
- try {
1519
- const result = runWithHooks(vnode.type, mergedProps);
1520
- hooks = result.hooks;
1521
- output = result.vnode;
1522
- } catch (err) {
1523
- if (__DEV__$3) _mountingStack.pop();
1524
- setCurrentScope(null);
1525
- scope.stop();
1526
- reportError({
1527
- component: componentName,
1528
- phase: "setup",
1529
- error: err,
1530
- timestamp: Date.now(),
1531
- props: vnode.props
1532
- });
1533
- const handled = dispatchToErrorBoundary(err);
1534
- if (!handled) console.error(`[Pyreon] <${componentName}> threw during setup:`, err);
1535
- if (__DEV__$3 && !handled) {
1536
- const overlay = document.createElement("pre");
1537
- overlay.style.cssText = "color:#e53e3e;background:#fff5f5;padding:12px;border:2px solid #e53e3e;border-radius:6px;font-size:12px;margin:4px;font-family:monospace;white-space:pre-wrap;word-break:break-word";
1538
- const e = err;
1539
- overlay.textContent = `[${componentName}] ${e.message ?? err}\n${e.stack ?? ""}`;
1540
- parent.insertBefore(overlay, anchor);
1541
- return () => overlay.remove();
1542
- }
1543
- return noop$1;
1544
- } finally {
1545
- setCurrentScope(null);
1546
- }
1547
- if (__DEV__$3 && output != null && typeof output === "object") {
1548
- if (output instanceof Promise) console.warn(`[Pyreon] Component <${componentName}> returned a Promise. Components must be synchronous — use lazy() + Suspense for async loading, or fetch data in onMount and store it in a signal.`);
1549
- else if (!("type" in output) && !Array.isArray(output) && !output.__isNative) console.warn(`[Pyreon] Component <${componentName}> returned an invalid value. Components must return a VNode, string, null, function, or array.`);
1550
- }
1551
- if (hooks.update) for (const fn of hooks.update) scope.addUpdateHook(fn);
1552
- let subtreeCleanup = noop$1;
1553
- try {
1554
- subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop$1;
1555
- } catch (err) {
1556
- if (__DEV__$3) _mountingStack.pop();
1557
- scope.stop();
1558
- if (!(propagateError(err, hooks) || dispatchToErrorBoundary(err))) {
1559
- reportError({
1560
- component: componentName,
1561
- phase: "render",
1562
- error: err,
1563
- timestamp: Date.now(),
1564
- props: vnode.props
1565
- });
1566
- console.error(`[Pyreon] <${componentName}> threw during render:`, err);
1567
- }
1568
- return noop$1;
1569
- }
1570
- if (__DEV__$3) {
1571
- _mountingStack.pop();
1572
- const firstEl = parent instanceof Element ? parent.firstElementChild : null;
1573
- registerComponent(compId, componentName, firstEl, devParentId);
1574
- }
1575
- let mountCleanups = null;
1576
- if (hooks.mount) for (const fn of hooks.mount) try {
1577
- let cleanup;
1578
- scope.runInScope(() => {
1579
- cleanup = fn();
1580
- });
1581
- if (cleanup) {
1582
- if (mountCleanups === null) mountCleanups = [];
1583
- mountCleanups.push(cleanup);
1584
- }
1585
- } catch (err) {
1586
- console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err);
1587
- reportError({
1588
- component: componentName,
1589
- phase: "mount",
1590
- error: err,
1591
- timestamp: Date.now()
1592
- });
1593
- }
1594
- return () => {
1595
- if (__DEV__$3) unregisterComponent(compId);
1596
- scope.stop();
1597
- subtreeCleanup();
1598
- if (hooks.unmount) for (const fn of hooks.unmount) try {
1599
- fn();
1600
- } catch (err) {
1601
- console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err);
1602
- reportError({
1603
- component: componentName,
1604
- phase: "unmount",
1605
- error: err,
1606
- timestamp: Date.now()
1607
- });
1608
- }
1609
- if (mountCleanups) for (const fn of mountCleanups) fn();
1610
- };
1611
- }
1612
- function mountChildren(children, parent, anchor) {
1613
- if (children.length === 0) return noop$1;
1614
- if (children.length === 1) {
1615
- const c = children[0];
1616
- if (c !== void 0) {
1617
- if (anchor === null && (typeof c === "string" || typeof c === "number")) {
1618
- parent.textContent = String(c);
1619
- return noop$1;
1620
- }
1621
- return mountChild(c, parent, anchor);
1622
- }
1623
- }
1624
- if (children.length === 2) {
1625
- const c0 = children[0];
1626
- const c1 = children[1];
1627
- if (c0 !== void 0 && c1 !== void 0) {
1628
- const d0 = mountChild(c0, parent, anchor);
1629
- const d1 = mountChild(c1, parent, anchor);
1630
- if (d0 === noop$1 && d1 === noop$1) return noop$1;
1631
- if (d0 === noop$1) return d1;
1632
- if (d1 === noop$1) return d0;
1633
- return () => {
1634
- d0();
1635
- d1();
1636
- };
1637
- }
1638
- }
1639
- const cleanups = children.map((c) => mountChild(c, parent, anchor));
1640
- return () => {
1641
- for (const fn of cleanups) fn();
1642
- };
1643
- }
1644
- /** Returns true if value is a non-empty array of VNodes that all carry keys. */
1645
- function isKeyedArray(value) {
1646
- if (!Array.isArray(value) || value.length === 0) return false;
1647
- return value.every((v) => v !== null && typeof v === "object" && !Array.isArray(v) && v.key !== null && v.key !== void 0);
1648
- }
1649
-
1650
53
  //#endregion
1651
54
  //#region src/hydrate.ts
1652
55
  const noop = () => {};
@@ -1870,62 +273,9 @@ function hydrateRoot(container, vnode) {
1870
273
  return cleanup;
1871
274
  }
1872
275
 
1873
- //#endregion
1874
- //#region src/keep-alive.ts
1875
- /**
1876
- * KeepAlive — mounts its children once and keeps them alive even when hidden.
1877
- *
1878
- * Unlike conditional rendering (which destroys and recreates component state),
1879
- * KeepAlive CSS-hides the children while preserving all reactive state,
1880
- * scroll position, form values, and in-flight async operations.
1881
- *
1882
- * Children are mounted imperatively on first activation and are never unmounted
1883
- * while the KeepAlive itself is mounted.
1884
- *
1885
- * Multi-slot pattern (one KeepAlive per route):
1886
- * @example
1887
- * h(Fragment, null, [
1888
- * h(KeepAlive, { active: () => route() === "/a" }, h(RouteA, null)),
1889
- * h(KeepAlive, { active: () => route() === "/b" }, h(RouteB, null)),
1890
- * ])
1891
- *
1892
- * With JSX:
1893
- * @example
1894
- * <>
1895
- * <KeepAlive active={() => route() === "/a"}><RouteA /></KeepAlive>
1896
- * <KeepAlive active={() => route() === "/b"}><RouteB /></KeepAlive>
1897
- * </>
1898
- */
1899
- function KeepAlive(props) {
1900
- const containerRef = createRef();
1901
- let childCleanup = null;
1902
- let childMounted = false;
1903
- onMount(() => {
1904
- const container = containerRef.current;
1905
- if (!container) return;
1906
- const e = effect(() => {
1907
- const isActive = props.active?.() ?? true;
1908
- if (!childMounted) {
1909
- childCleanup = runUntracked(() => mountChild(props.children ?? null, container, null));
1910
- childMounted = true;
1911
- }
1912
- container.style.display = isActive ? "" : "none";
1913
- });
1914
- return () => {
1915
- e.dispose();
1916
- childCleanup?.();
1917
- };
1918
- });
1919
- return h("div", {
1920
- ref: containerRef,
1921
- style: "display: contents"
1922
- });
1923
- }
1924
- nativeCompat(KeepAlive);
1925
-
1926
276
  //#endregion
1927
277
  //#region src/template.ts
1928
- const __DEV__$2 = process.env.NODE_ENV !== "production";
278
+ const __DEV__$1 = process.env.NODE_ENV !== "production";
1929
279
  const _countSink$1 = globalThis;
1930
280
  /**
1931
281
  * Creates a row/item factory backed by HTML template cloning.
@@ -1982,7 +332,7 @@ function createTemplate(html, bind) {
1982
332
  * @param node - The Text node to update
1983
333
  */
1984
334
  function _bindText(source, node) {
1985
- if (__DEV__$2) _countSink$1.__pyreon_count__?.("runtime.bindText");
335
+ if (__DEV__$1) _countSink$1.__pyreon_count__?.("runtime.bindText");
1986
336
  if (source.direct) {
1987
337
  const textUpdate = () => {
1988
338
  const v = source._v;
@@ -2016,7 +366,7 @@ function _bindText(source, node) {
2016
366
  * @param updater - Function that reads `source._v` and applies the DOM update
2017
367
  */
2018
368
  function _bindDirect(source, updater) {
2019
- if (__DEV__$2) _countSink$1.__pyreon_count__?.("runtime.bindDirect");
369
+ if (__DEV__$1) _countSink$1.__pyreon_count__?.("runtime.bindDirect");
2020
370
  if (source.direct) {
2021
371
  updater(source._v);
2022
372
  return source.direct(() => updater(source._v));
@@ -2050,7 +400,7 @@ const _tplCache = /* @__PURE__ */ new Map();
2050
400
  * })
2051
401
  */
2052
402
  function _tpl(html, bind) {
2053
- if (__DEV__$2) _countSink$1.__pyreon_count__?.("runtime.tpl");
403
+ if (__DEV__$1) _countSink$1.__pyreon_count__?.("runtime.tpl");
2054
404
  let tpl = _tplCache.get(html);
2055
405
  if (!tpl) {
2056
406
  tpl = document.createElement("template");
@@ -2175,169 +525,6 @@ function _mountSlot(children, parent, placeholder) {
2175
525
  return cleanup;
2176
526
  }
2177
527
 
2178
- //#endregion
2179
- //#region src/transition.ts
2180
- const __DEV__$1 = process.env.NODE_ENV !== "production";
2181
- /**
2182
- * Transition — adds CSS enter/leave animation classes to a single child element,
2183
- * controlled by the reactive `show` prop.
2184
- *
2185
- * Class lifecycle:
2186
- * Enter: {name}-enter-from → (next frame) → {name}-enter-active + {name}-enter-to → cleanup
2187
- * Leave: {name}-leave-from → (next frame) → {name}-leave-active + {name}-leave-to → unmount
2188
- *
2189
- * The child element stays in the DOM during the leave animation and is removed only
2190
- * after the CSS transition / animation completes.
2191
- *
2192
- * @example
2193
- * const visible = signal(false)
2194
- *
2195
- * h(Transition, { name: "fade", show: () => visible() },
2196
- * h("div", { class: "modal" }, "content")
2197
- * )
2198
- *
2199
- * // CSS:
2200
- * // .fade-enter-from, .fade-leave-to { opacity: 0; }
2201
- * // .fade-enter-active, .fade-leave-active { transition: opacity 300ms ease; }
2202
- */
2203
- function Transition(props) {
2204
- const n = props.name ?? "pyreon";
2205
- const cls = {
2206
- ef: props.enterFrom ?? `${n}-enter-from`,
2207
- ea: props.enterActive ?? `${n}-enter-active`,
2208
- et: props.enterTo ?? `${n}-enter-to`,
2209
- lf: props.leaveFrom ?? `${n}-leave-from`,
2210
- la: props.leaveActive ?? `${n}-leave-active`,
2211
- lt: props.leaveTo ?? `${n}-leave-to`
2212
- };
2213
- const ref = createRef();
2214
- const isMounted = signal(runUntracked(props.show));
2215
- let pendingEnterCancel = null;
2216
- let pendingLeaveCancel = null;
2217
- let initialized = false;
2218
- const applyEnter = (el) => {
2219
- pendingLeaveCancel?.();
2220
- pendingLeaveCancel = null;
2221
- pendingEnterCancel?.();
2222
- pendingEnterCancel = null;
2223
- props.onBeforeEnter?.(el);
2224
- el.classList.remove(cls.lf, cls.la, cls.lt);
2225
- el.classList.add(cls.ef, cls.ea);
2226
- requestAnimationFrame(() => {
2227
- el.classList.remove(cls.ef);
2228
- el.classList.add(cls.et);
2229
- let safetyTimer = null;
2230
- const done = () => {
2231
- el.removeEventListener("transitionend", done);
2232
- el.removeEventListener("animationend", done);
2233
- if (safetyTimer !== null) {
2234
- clearTimeout(safetyTimer);
2235
- safetyTimer = null;
2236
- }
2237
- pendingEnterCancel = null;
2238
- el.classList.remove(cls.ea, cls.et);
2239
- props.onAfterEnter?.(el);
2240
- };
2241
- pendingEnterCancel = () => {
2242
- el.removeEventListener("transitionend", done);
2243
- el.removeEventListener("animationend", done);
2244
- if (safetyTimer !== null) {
2245
- clearTimeout(safetyTimer);
2246
- safetyTimer = null;
2247
- }
2248
- el.classList.remove(cls.ef, cls.ea, cls.et);
2249
- };
2250
- el.addEventListener("transitionend", done, { once: true });
2251
- el.addEventListener("animationend", done, { once: true });
2252
- safetyTimer = setTimeout(done, 5e3);
2253
- });
2254
- };
2255
- const applyLeave = (el) => {
2256
- pendingEnterCancel?.();
2257
- pendingEnterCancel = null;
2258
- props.onBeforeLeave?.(el);
2259
- el.classList.remove(cls.ef, cls.ea, cls.et);
2260
- el.classList.add(cls.lf, cls.la);
2261
- requestAnimationFrame(() => {
2262
- el.classList.remove(cls.lf);
2263
- el.classList.add(cls.lt);
2264
- let safetyTimer = null;
2265
- const done = () => {
2266
- el.removeEventListener("transitionend", done);
2267
- el.removeEventListener("animationend", done);
2268
- if (safetyTimer !== null) {
2269
- clearTimeout(safetyTimer);
2270
- safetyTimer = null;
2271
- }
2272
- el.classList.remove(cls.la, cls.lt);
2273
- pendingLeaveCancel = null;
2274
- isMounted.set(false);
2275
- props.onAfterLeave?.(el);
2276
- };
2277
- pendingLeaveCancel = () => {
2278
- el.removeEventListener("transitionend", done);
2279
- el.removeEventListener("animationend", done);
2280
- if (safetyTimer !== null) {
2281
- clearTimeout(safetyTimer);
2282
- safetyTimer = null;
2283
- }
2284
- el.classList.remove(cls.lf, cls.la, cls.lt);
2285
- };
2286
- el.addEventListener("transitionend", done, { once: true });
2287
- el.addEventListener("animationend", done, { once: true });
2288
- safetyTimer = setTimeout(done, 5e3);
2289
- });
2290
- };
2291
- const handleVisibilityChange = (visible) => {
2292
- if (visible) {
2293
- if (!isMounted.peek()) isMounted.set(true);
2294
- queueMicrotask(() => applyEnter(ref.current));
2295
- return;
2296
- }
2297
- if (!isMounted.peek()) return;
2298
- const el = ref.current;
2299
- if (!el) {
2300
- isMounted.set(false);
2301
- return;
2302
- }
2303
- applyLeave(el);
2304
- };
2305
- effect(() => {
2306
- const visible = props.show();
2307
- if (!initialized) {
2308
- initialized = true;
2309
- if (visible && props.appear) queueMicrotask(() => applyEnter(ref.current));
2310
- return;
2311
- }
2312
- handleVisibilityChange(visible);
2313
- });
2314
- onUnmount(() => {
2315
- pendingEnterCancel?.();
2316
- pendingEnterCancel = null;
2317
- pendingLeaveCancel?.();
2318
- pendingLeaveCancel = null;
2319
- });
2320
- const rawChild = props.children;
2321
- const emptyFragment = h(Fragment, null);
2322
- return (() => {
2323
- if (!isMounted()) return emptyFragment;
2324
- if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) return rawChild ?? null;
2325
- const vnode = rawChild;
2326
- if (typeof vnode.type !== "string") {
2327
- if (__DEV__$1) console.warn("[Pyreon] Transition child is a component. Wrap it in a DOM element for enter/leave animations to work.");
2328
- return vnode;
2329
- }
2330
- return {
2331
- ...vnode,
2332
- props: {
2333
- ...vnode.props,
2334
- ref
2335
- }
2336
- };
2337
- });
2338
- }
2339
- nativeCompat(Transition);
2340
-
2341
528
  //#endregion
2342
529
  //#region src/transition-group.ts
2343
530
  /**