@pyreon/runtime-dom 0.5.6 → 0.5.7

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.
@@ -1,1967 +1,418 @@
1
- import { batch, effect, effectScope, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
2
- import { EMPTY_PROPS, ForSymbol, Fragment, PortalSymbol, createRef, dispatchToErrorBoundary, h, normalizeStyleValue, onMount, onUnmount, propagateError, reportError, runWithHooks, toKebabCase } from "@pyreon/core";
1
+ import { NativeItem, Props, VNode, VNodeChild } from "@pyreon/core";
3
2
 
4
- //#region src/delegate.ts
3
+ //#region src/delegate.d.ts
5
4
  /**
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
- */
5
+ * Event delegation — single listener per event type on the mount container.
6
+ *
7
+ * Instead of calling addEventListener on every element, the compiler emits
8
+ * `el.__click = handler` (expando property). A single delegated listener on the
9
+ * container walks event.target up the DOM tree, checking for expandos.
10
+ *
11
+ * Benefits:
12
+ * - Saves ~2000 addEventListener calls for 1000 rows with 2 handlers each
13
+ * - Reduces memory per row (no per-element listener closure)
14
+ * - Faster initial mount (~0.4-0.8ms savings on 1000-row benchmarks)
15
+ */
17
16
  /**
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
-
17
+ * Events that are delegated (common bubbling events).
18
+ * Non-bubbling events (focus, blur, mouseenter, mouseleave, load, error, scroll)
19
+ * are NOT delegated — they must use addEventListener.
20
+ */
21
+ declare const DELEGATED_EVENTS: Set<string>;
23
22
  /**
24
- * Property name used on DOM elements to store delegated event handlers.
25
- * Format: `__ev_{eventName}` e.g. `__ev_click`, `__ev_input`
26
- */
27
- function delegatedPropName(eventName) {
28
- return `__ev_${eventName}`;
29
- }
23
+ * Property name used on DOM elements to store delegated event handlers.
24
+ * Format: `__ev_{eventName}` e.g. `__ev_click`, `__ev_input`
25
+ */
26
+ declare function delegatedPropName(eventName: string): string;
30
27
  /**
31
- * Install delegation listeners on a container element.
32
- * Called once from mount(). Idempotent — safe to call multiple times.
33
- */
34
- function setupDelegation(container) {
35
- if (_delegated.has(container)) return;
36
- _delegated.add(container);
37
- for (const eventName of DELEGATED_EVENTS) {
38
- const prop = delegatedPropName(eventName);
39
- container.addEventListener(eventName, e => {
40
- let el = e.target;
41
- while (el && el !== container) {
42
- const handler = el[prop];
43
- if (handler) {
44
- batch(() => handler(e));
45
- if (e.cancelBubble) break;
46
- }
47
- el = el.parentElement;
48
- }
49
- });
50
- }
51
- }
52
-
28
+ * Install delegation listeners on a container element.
29
+ * Called once from mount(). Idempotent — safe to call multiple times.
30
+ */
31
+ declare function setupDelegation(container: Element): void;
53
32
  //#endregion
54
- //#region src/hydration-debug.ts
55
-
56
- function enableHydrationWarnings() {
57
- _enabled = true;
58
- }
59
- function disableHydrationWarnings() {
60
- _enabled = false;
61
- }
33
+ //#region src/devtools.d.ts
62
34
  /**
63
- * Emit a hydration mismatch warning.
64
- * @param type - Kind of mismatch
65
- * @param expected - What the VNode expected
66
- * @param actual - What the DOM had
67
- * @param path - Human-readable path in the tree, e.g. "root > div > span"
68
- */
69
- function warnHydrationMismatch(_type, _expected, _actual, _path) {
70
- if (!_enabled) return;
71
- console.warn(`[Pyreon] Hydration mismatch (${_type}): expected ${String(_expected)}, got ${String(_actual)} at ${_path}`);
35
+ * Pyreon DevTools — exposes a `__PYREON_DEVTOOLS__` global hook for browser devtools extensions
36
+ * and in-app debugging utilities.
37
+ *
38
+ * Installed automatically on first `mount()` call in the browser.
39
+ * No-op on the server (typeof window === "undefined").
40
+ *
41
+ * Usage:
42
+ * window.__PYREON_DEVTOOLS__.getComponentTree() // root component entries
43
+ * window.__PYREON_DEVTOOLS__.getAllComponents() // flat list of all live components
44
+ * window.__PYREON_DEVTOOLS__.highlight("comp-id") // outline a component's DOM node
45
+ * window.__PYREON_DEVTOOLS__.onComponentMount(cb) // subscribe to mount events
46
+ * window.__PYREON_DEVTOOLS__.onComponentUnmount(cb)// subscribe to unmount events
47
+ * window.__PYREON_DEVTOOLS__.enableOverlay() // Ctrl+Shift+P: hover to inspect components
48
+ */
49
+ interface DevtoolsComponentEntry {
50
+ id: string;
51
+ name: string;
52
+ /** First DOM element produced by this component, if any */
53
+ el: Element | null;
54
+ parentId: string | null;
55
+ childIds: string[];
56
+ }
57
+ interface PyreonDevtools {
58
+ readonly version: string;
59
+ getComponentTree(): DevtoolsComponentEntry[];
60
+ getAllComponents(): DevtoolsComponentEntry[];
61
+ highlight(id: string): void;
62
+ onComponentMount(cb: (entry: DevtoolsComponentEntry) => void): () => void;
63
+ onComponentUnmount(cb: (id: string) => void): () => void;
64
+ /** Toggle the component inspector overlay (also: Ctrl+Shift+P) */
65
+ enableOverlay(): void;
66
+ disableOverlay(): void;
72
67
  }
73
-
74
68
  //#endregion
75
- //#region src/devtools.ts
76
-
77
- function registerComponent(id, name, el, parentId) {
78
- const entry = {
79
- id,
80
- name,
81
- el,
82
- parentId,
83
- childIds: []
84
- };
85
- _components.set(id, entry);
86
- if (parentId) {
87
- const parent = _components.get(parentId);
88
- if (parent) parent.childIds.push(id);
89
- }
90
- for (const cb of _mountListeners) cb(entry);
91
- }
92
- function unregisterComponent(id) {
93
- const entry = _components.get(id);
94
- if (!entry) return;
95
- if (entry.parentId) {
96
- const parent = _components.get(entry.parentId);
97
- if (parent) parent.childIds = parent.childIds.filter(c => c !== id);
98
- }
99
- _components.delete(id);
100
- for (const cb of _unmountListeners) cb(id);
101
- }
102
- function findComponentForElement(el) {
103
- let node = el;
104
- while (node) {
105
- for (const entry of _components.values()) if (entry.el === node) return entry;
106
- node = node.parentElement;
107
- }
108
- return null;
109
- }
110
- function createOverlayElements() {
111
- if (_overlayEl) return;
112
- _overlayEl = document.createElement("div");
113
- _overlayEl.id = "__pyreon-overlay";
114
- _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;";
115
- _tooltipEl = document.createElement("div");
116
- _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;";
117
- document.body.appendChild(_overlayEl);
118
- document.body.appendChild(_tooltipEl);
119
- }
120
- function positionOverlay(rect) {
121
- if (!_overlayEl) return;
122
- _overlayEl.style.display = "block";
123
- _overlayEl.style.top = `${rect.top}px`;
124
- _overlayEl.style.left = `${rect.left}px`;
125
- _overlayEl.style.width = `${rect.width}px`;
126
- _overlayEl.style.height = `${rect.height}px`;
127
- }
128
- function positionTooltip(entry, rect) {
129
- if (!_tooltipEl) return;
130
- const childCount = entry.childIds.length;
131
- let info = `<${entry.name}>`;
132
- if (childCount > 0) info += `\n ${childCount} child component${childCount === 1 ? "" : "s"}`;
133
- _tooltipEl.textContent = info;
134
- _tooltipEl.style.display = "block";
135
- _tooltipEl.style.top = `${rect.top - 30}px`;
136
- _tooltipEl.style.left = `${rect.left}px`;
137
- if (rect.top < 35) _tooltipEl.style.top = `${rect.bottom + 4}px`;
138
- }
139
- function hideOverlayElements() {
140
- if (_overlayEl) _overlayEl.style.display = "none";
141
- if (_tooltipEl) _tooltipEl.style.display = "none";
142
- _currentHighlight = null;
143
- }
144
- /** @internal — exported for testing only */
145
- function onOverlayMouseMove(e) {
146
- const target = document.elementFromPoint(e.clientX, e.clientY);
147
- if (!target || target === _overlayEl || target === _tooltipEl) return;
148
- const entry = findComponentForElement(target);
149
- if (!entry?.el) {
150
- hideOverlayElements();
151
- return;
152
- }
153
- if (entry.el === _currentHighlight) return;
154
- _currentHighlight = entry.el;
155
- const rect = entry.el.getBoundingClientRect();
156
- positionOverlay(rect);
157
- positionTooltip(entry, rect);
158
- }
159
- /** @internal — exported for testing only */
160
- function onOverlayClick(e) {
161
- e.preventDefault();
162
- e.stopPropagation();
163
- const target = document.elementFromPoint(e.clientX, e.clientY);
164
- if (!target) return;
165
- const entry = findComponentForElement(target);
166
- if (entry) {
167
- console.group(`[Pyreon] <${entry.name}>`);
168
- console.log("element:", entry.el);
169
- console.log("children:", entry.childIds.length);
170
- if (entry.parentId) {
171
- const parent = _components.get(entry.parentId);
172
- if (parent) console.log("parent:", `<${parent.name}>`);
173
- }
174
- console.groupEnd();
175
- }
176
- disableOverlay();
177
- }
178
- function onOverlayKeydown(e) {
179
- if (e.key === "Escape") disableOverlay();
180
- }
181
- function enableOverlay() {
182
- if (_overlayActive) return;
183
- _overlayActive = true;
184
- createOverlayElements();
185
- document.addEventListener("mousemove", onOverlayMouseMove, true);
186
- document.addEventListener("click", onOverlayClick, true);
187
- document.addEventListener("keydown", onOverlayKeydown, true);
188
- document.body.style.cursor = "crosshair";
189
- }
190
- function disableOverlay() {
191
- if (!_overlayActive) return;
192
- _overlayActive = false;
193
- document.removeEventListener("mousemove", onOverlayMouseMove, true);
194
- document.removeEventListener("click", onOverlayClick, true);
195
- document.removeEventListener("keydown", onOverlayKeydown, true);
196
- document.body.style.cursor = "";
197
- if (_overlayEl) _overlayEl.style.display = "none";
198
- if (_tooltipEl) _tooltipEl.style.display = "none";
199
- _currentHighlight = null;
200
- }
201
- function installDevTools() {
202
- if (!_hasWindow || _installed) return;
203
- _installed = true;
204
- const devtools = {
205
- version: "0.1.0",
206
- getComponentTree() {
207
- return Array.from(_components.values()).filter(e => e.parentId === null);
208
- },
209
- getAllComponents() {
210
- return Array.from(_components.values());
211
- },
212
- highlight(id) {
213
- const entry = _components.get(id);
214
- if (!entry?.el) return;
215
- const el = entry.el;
216
- const prev = el.style.outline;
217
- el.style.outline = "2px solid #00b4d8";
218
- setTimeout(() => {
219
- el.style.outline = prev;
220
- }, 1500);
221
- },
222
- onComponentMount(cb) {
223
- _mountListeners.push(cb);
224
- return () => {
225
- const i = _mountListeners.indexOf(cb);
226
- if (i >= 0) _mountListeners.splice(i, 1);
227
- };
228
- },
229
- onComponentUnmount(cb) {
230
- _unmountListeners.push(cb);
231
- return () => {
232
- const i = _unmountListeners.indexOf(cb);
233
- if (i >= 0) _unmountListeners.splice(i, 1);
234
- };
235
- },
236
- enableOverlay,
237
- disableOverlay
238
- };
239
- window.__PYREON_DEVTOOLS__ = devtools;
240
- window.addEventListener("keydown", e => {
241
- if (e.ctrlKey && e.shiftKey && e.key === "P") {
242
- e.preventDefault();
243
- if (_overlayActive) disableOverlay();else enableOverlay();
244
- }
245
- });
246
- const win = window;
247
- win.$p = {
248
- components: () => devtools.getAllComponents(),
249
- tree: () => devtools.getComponentTree(),
250
- highlight: id => devtools.highlight(id),
251
- inspect: () => {
252
- if (_overlayActive) disableOverlay();else enableOverlay();
253
- },
254
- stats: () => {
255
- const all = devtools.getAllComponents();
256
- const roots = devtools.getComponentTree();
257
- console.log(`[Pyreon] ${all.length} component${all.length === 1 ? "" : "s"}, ${roots.length} root${roots.length === 1 ? "" : "s"}`);
258
- return {
259
- total: all.length,
260
- roots: roots.length
261
- };
262
- },
263
- help: () => {
264
- 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");
265
- }
266
- };
267
- }
268
-
269
- //#endregion
270
- //#region src/nodes.ts
271
-
69
+ //#region src/hydrate.d.ts
272
70
  /**
273
- * Move all nodes strictly between `start` and `end` into a throwaway
274
- * DocumentFragment, detaching them from the live DOM in O(n) top-level moves.
275
- *
276
- * This is dramatically faster than Range.deleteContents() in JS-based DOMs
277
- * (happy-dom, jsdom) where deleting connected nodes with deep subtrees is O(n²).
278
- * In real browsers both approaches are similar, but the fragment approach is
279
- * never slower and avoids the pathological case.
280
- *
281
- * After this call every moved node has isConnected=false, so cleanup functions
282
- * that guard removeChild with `isConnected !== false` become no-ops.
283
- */
284
- function clearBetween(start, end) {
285
- const frag = document.createDocumentFragment();
286
- let cur = start.nextSibling;
287
- while (cur && cur !== end) {
288
- const next = cur.nextSibling;
289
- frag.appendChild(cur);
290
- cur = next;
291
- }
292
- }
293
- /**
294
- * Mount a reactive node whose content changes over time.
295
- *
296
- * A comment node is used as a stable anchor point in the DOM.
297
- * On each change: old nodes are removed, new ones inserted before the anchor.
298
- */
299
- function mountReactive(accessor, parent, anchor, mount) {
300
- const marker = document.createComment("pyreon");
301
- parent.insertBefore(marker, anchor);
302
- let currentCleanup = () => {};
303
- let generation = 0;
304
- const e = effect(() => {
305
- const myGen = ++generation;
306
- runUntracked(() => currentCleanup());
307
- currentCleanup = () => {};
308
- const value = accessor();
309
- if (__DEV__$4 && typeof value === "function") console.warn("[Pyreon] Reactive accessor returned a function instead of a value. Did you forget to call the signal?");
310
- if (value != null && value !== false) {
311
- const cleanup = mount(value, parent, marker);
312
- if (myGen === generation) currentCleanup = cleanup;else cleanup();
313
- }
314
- });
315
- return () => {
316
- e.dispose();
317
- currentCleanup();
318
- marker.parentNode?.removeChild(marker);
319
- };
320
- }
321
- function growLisArrays(lis, n) {
322
- if (n <= lis.pred.length) return lis;
323
- return {
324
- tails: new Int32Array(n + 16),
325
- tailIdx: new Int32Array(n + 16),
326
- pred: new Int32Array(n + 16),
327
- stay: new Uint8Array(n + 16)
328
- };
329
- }
330
- function computeKeyedLis(lis, n, newKeyOrder, curPos) {
331
- const {
332
- tails,
333
- tailIdx,
334
- pred
335
- } = lis;
336
- let lisLen = 0;
337
- for (let i = 0; i < n; i++) {
338
- const key = newKeyOrder[i];
339
- if (key === void 0) continue;
340
- const v = curPos.get(key) ?? -1;
341
- if (v < 0) continue;
342
- let lo = 0;
343
- let hi = lisLen;
344
- while (lo < hi) {
345
- const mid = lo + hi >> 1;
346
- if (tails[mid] < v) lo = mid + 1;else hi = mid;
347
- }
348
- tails[lo] = v;
349
- tailIdx[lo] = i;
350
- if (lo > 0) pred[i] = tailIdx[lo - 1];
351
- if (lo === lisLen) lisLen++;
352
- }
353
- return lisLen;
354
- }
355
- function markStayingEntries(lis, lisLen) {
356
- const {
357
- tailIdx,
358
- pred,
359
- stay
360
- } = lis;
361
- let cur = lisLen > 0 ? tailIdx[lisLen - 1] : -1;
362
- while (cur !== -1) {
363
- stay[cur] = 1;
364
- cur = pred[cur];
365
- }
366
- }
367
- function applyKeyedMoves(n, newKeyOrder, stay, cache, parent, tailMarker) {
368
- let cursor = tailMarker;
369
- for (let i = n - 1; i >= 0; i--) {
370
- const key = newKeyOrder[i];
371
- if (key === void 0) continue;
372
- const entry = cache.get(key);
373
- if (!entry) continue;
374
- if (!stay[i]) moveEntryBefore(parent, entry.anchor, cursor);
375
- cursor = entry.anchor;
376
- }
377
- }
378
- /** Grow LIS typed arrays if needed, then compute and apply reorder. */
379
- function keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker) {
380
- const grown = growLisArrays(lis, n);
381
- grown.pred.fill(-1, 0, n);
382
- grown.stay.fill(0, 0, n);
383
- markStayingEntries(grown, computeKeyedLis(grown, n, newKeyOrder, curPos));
384
- applyKeyedMoves(n, newKeyOrder, grown.stay, cache, parent, tailMarker);
385
- return grown;
386
- }
387
- function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
388
- const startMarker = document.createComment("");
389
- const tailMarker = document.createComment("");
390
- parent.insertBefore(startMarker, listAnchor);
391
- parent.insertBefore(tailMarker, listAnchor);
392
- const cache = /* @__PURE__ */new Map();
393
- const curPos = /* @__PURE__ */new Map();
394
- let currentKeyOrder = [];
395
- let lis = {
396
- tails: new Int32Array(16),
397
- tailIdx: new Int32Array(16),
398
- pred: new Int32Array(16),
399
- stay: new Uint8Array(16)
400
- };
401
- const collectKeyOrder = newList => {
402
- const newKeyOrder = [];
403
- const newKeySet = /* @__PURE__ */new Set();
404
- for (const vnode of newList) {
405
- const key = vnode.key;
406
- if (key !== null && key !== void 0) {
407
- newKeyOrder.push(key);
408
- newKeySet.add(key);
409
- }
410
- }
411
- return {
412
- newKeyOrder,
413
- newKeySet
414
- };
415
- };
416
- const removeStaleEntries = newKeySet => {
417
- for (const [key, entry] of cache) {
418
- if (newKeySet.has(key)) continue;
419
- entry.cleanup();
420
- entry.anchor.parentNode?.removeChild(entry.anchor);
421
- cache.delete(key);
422
- curPos.delete(key);
423
- }
424
- };
425
- const mountNewEntries = newList => {
426
- for (const vnode of newList) {
427
- const key = vnode.key;
428
- if (key === null || key === void 0) continue;
429
- if (cache.has(key)) continue;
430
- const anchor = document.createComment("");
431
- _keyedAnchors.add(anchor);
432
- parent.insertBefore(anchor, tailMarker);
433
- const cleanup = mountVNode(vnode, parent, tailMarker);
434
- cache.set(key, {
435
- anchor,
436
- cleanup
437
- });
438
- }
439
- };
440
- const e = effect(() => {
441
- const newList = accessor();
442
- const n = newList.length;
443
- if (n === 0 && cache.size > 0) {
444
- for (const entry of cache.values()) entry.cleanup();
445
- cache.clear();
446
- curPos.clear();
447
- currentKeyOrder = [];
448
- clearBetween(startMarker, tailMarker);
449
- return;
450
- }
451
- const {
452
- newKeyOrder,
453
- newKeySet
454
- } = collectKeyOrder(newList);
455
- removeStaleEntries(newKeySet);
456
- mountNewEntries(newList);
457
- if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
458
- curPos.clear();
459
- for (let i = 0; i < newKeyOrder.length; i++) {
460
- const k = newKeyOrder[i];
461
- if (k !== void 0) curPos.set(k, i);
462
- }
463
- currentKeyOrder = newKeyOrder;
464
- });
465
- return () => {
466
- e.dispose();
467
- for (const entry of cache.values()) {
468
- entry.cleanup();
469
- entry.anchor.parentNode?.removeChild(entry.anchor);
470
- }
471
- cache.clear();
472
- startMarker.parentNode?.removeChild(startMarker);
473
- tailMarker.parentNode?.removeChild(tailMarker);
474
- };
475
- }
476
- /** Maximum number of displaced positions before falling back to full LIS. */
477
-
478
- /** Try small-k reorder; returns true if handled, false if LIS fallback needed. */
479
- function trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker) {
480
- if (n !== currentKeys.length) return false;
481
- const diffs = [];
482
- for (let i = 0; i < n; i++) if (newKeys[i] !== currentKeys[i]) {
483
- diffs.push(i);
484
- if (diffs.length > SMALL_K) return false;
485
- }
486
- if (diffs.length > 0) smallKPlace(liveParent, diffs, newKeys, cache, tailMarker);
487
- for (const i of diffs) {
488
- const cached = cache.get(newKeys[i]);
489
- if (cached) cached.pos = i;
490
- }
491
- return true;
492
- }
493
- function computeForLis(lis, n, newKeys, cache) {
494
- const {
495
- tails,
496
- tailIdx,
497
- pred
498
- } = lis;
499
- let lisLen = 0;
500
- for (let i = 0; i < n; i++) {
501
- const key = newKeys[i];
502
- const v = cache.get(key)?.pos ?? 0;
503
- let lo = 0;
504
- let hi = lisLen;
505
- while (lo < hi) {
506
- const mid = lo + hi >> 1;
507
- if (tails[mid] < v) lo = mid + 1;else hi = mid;
508
- }
509
- tails[lo] = v;
510
- tailIdx[lo] = i;
511
- if (lo > 0) pred[i] = tailIdx[lo - 1];
512
- if (lo === lisLen) lisLen++;
513
- }
514
- return lisLen;
515
- }
516
- function applyForMoves(n, newKeys, stay, cache, liveParent, tailMarker) {
517
- let cursor = tailMarker;
518
- for (let i = n - 1; i >= 0; i--) {
519
- const entry = cache.get(newKeys[i]);
520
- if (!entry) continue;
521
- if (!stay[i]) moveEntryBefore(liveParent, entry.anchor, cursor);
522
- cursor = entry.anchor;
523
- }
524
- }
525
- /** LIS-based reorder for mountFor. */
526
- function forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker) {
527
- const grown = growLisArrays(lis, n);
528
- grown.pred.fill(-1, 0, n);
529
- grown.stay.fill(0, 0, n);
530
- markStayingEntries(grown, computeForLis(grown, n, newKeys, cache));
531
- applyForMoves(n, newKeys, grown.stay, cache, liveParent, tailMarker);
532
- for (let i = 0; i < n; i++) {
533
- const cached = cache.get(newKeys[i]);
534
- if (cached) cached.pos = i;
535
- }
536
- return grown;
537
- }
538
- /**
539
- * Keyed reconciler that works directly on the source item array.
540
- *
541
- * Optimizations:
542
- * - Calls renderItem() only for NEW keys — 0 VNode allocations for reorders
543
- * - Small-k fast path: if <= SMALL_K positions changed, skips LIS
544
- * - Fast clear path: moves nodes to DocumentFragment for O(n) bulk detach
545
- * - Fresh render fast path: skips stale-check and reorder on first render
546
- */
547
- function mountFor(source, getKey, renderItem, parent, anchor, mountChild) {
548
- const startMarker = document.createComment("");
549
- const tailMarker = document.createComment("");
550
- parent.insertBefore(startMarker, anchor);
551
- parent.insertBefore(tailMarker, anchor);
552
- let cache = /* @__PURE__ */new Map();
553
- let currentKeys = [];
554
- let cleanupCount = 0;
555
- let anchorsRegistered = false;
556
- let lis = {
557
- tails: new Int32Array(16),
558
- tailIdx: new Int32Array(16),
559
- pred: new Int32Array(16),
560
- stay: new Uint8Array(16)
561
- };
562
- const warnForKey = (seen, key) => {
563
- if (!__DEV__$4 || !seen) return;
564
- if (key == null) console.warn("[Pyreon] <For> `by` function returned null/undefined. Keys must be strings or numbers. Check your `by` prop.");
565
- if (seen.has(key)) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
566
- seen.add(key);
567
- };
568
- /** Render item into container, update cache+cleanupCount. No anchor registration. */
569
- const renderInto = (item, key, pos, container, before) => {
570
- const result = renderItem(item);
571
- if (result.__isNative) {
572
- const native = result;
573
- container.insertBefore(native.el, before);
574
- cache.set(key, {
575
- anchor: native.el,
576
- cleanup: native.cleanup,
577
- pos
578
- });
579
- if (native.cleanup) cleanupCount++;
580
- return;
581
- }
582
- const priorLast = before ? before.previousSibling : container.lastChild;
583
- const cl = mountChild(result, container, before);
584
- const firstMounted = priorLast ? priorLast.nextSibling : container.firstChild;
585
- if (!firstMounted || firstMounted === before) {
586
- const ph = document.createComment("");
587
- container.insertBefore(ph, before);
588
- cache.set(key, {
589
- anchor: ph,
590
- cleanup: cl,
591
- pos
592
- });
593
- } else cache.set(key, {
594
- anchor: firstMounted,
595
- cleanup: cl,
596
- pos
597
- });
598
- cleanupCount++;
599
- };
600
- const handleFreshRender = (items, n, liveParent) => {
601
- const frag = document.createDocumentFragment();
602
- const keys = new Array(n);
603
- const _seenKeys = __DEV__$4 ? /* @__PURE__ */new Set() : null;
604
- for (let i = 0; i < n; i++) {
605
- const item = items[i];
606
- const key = getKey(item);
607
- warnForKey(_seenKeys, key);
608
- keys[i] = key;
609
- renderInto(item, key, i, frag, null);
610
- }
611
- liveParent.insertBefore(frag, tailMarker);
612
- anchorsRegistered = false;
613
- currentKeys = keys;
614
- };
615
- const collectNewKeys = (items, n) => {
616
- const newKeys = new Array(n);
617
- const _seenUpdate = __DEV__$4 ? /* @__PURE__ */new Set() : null;
618
- for (let i = 0; i < n; i++) {
619
- newKeys[i] = getKey(items[i]);
620
- warnForKey(_seenUpdate, newKeys[i]);
621
- }
622
- return newKeys;
623
- };
624
- const handleReplaceAll = (items, n, newKeys, liveParent) => {
625
- if (cleanupCount > 0) {
626
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
627
- }
628
- cache = /* @__PURE__ */new Map();
629
- cleanupCount = 0;
630
- const parentParent = liveParent.parentNode;
631
- const canSwap = parentParent && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker;
632
- const frag = document.createDocumentFragment();
633
- for (let i = 0; i < n; i++) renderInto(items[i], newKeys[i], i, frag, null);
634
- anchorsRegistered = false;
635
- if (canSwap) {
636
- const fresh = liveParent.cloneNode(false);
637
- fresh.appendChild(startMarker);
638
- fresh.appendChild(frag);
639
- fresh.appendChild(tailMarker);
640
- parentParent.replaceChild(fresh, liveParent);
641
- } else {
642
- clearBetween(startMarker, tailMarker);
643
- liveParent.insertBefore(frag, tailMarker);
644
- }
645
- currentKeys = newKeys;
646
- };
647
- const removeStaleForEntries = newKeySet => {
648
- for (const [key, entry] of cache) {
649
- if (newKeySet.has(key)) continue;
650
- if (entry.cleanup) {
651
- entry.cleanup();
652
- cleanupCount--;
653
- }
654
- entry.anchor.parentNode?.removeChild(entry.anchor);
655
- cache.delete(key);
656
- }
657
- };
658
- const mountNewForEntries = (items, n, newKeys, liveParent) => {
659
- for (let i = 0; i < n; i++) {
660
- const key = newKeys[i];
661
- if (cache.has(key)) continue;
662
- renderInto(items[i], key, i, liveParent, tailMarker);
663
- const entry = cache.get(key);
664
- if (entry) _forAnchors.add(entry.anchor);
665
- }
666
- };
667
- const handleFastClear = liveParent => {
668
- if (cache.size === 0) return;
669
- if (cleanupCount > 0) {
670
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
671
- }
672
- const pp = liveParent.parentNode;
673
- if (pp && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker) {
674
- const fresh = liveParent.cloneNode(false);
675
- fresh.appendChild(startMarker);
676
- fresh.appendChild(tailMarker);
677
- pp.replaceChild(fresh, liveParent);
678
- } else clearBetween(startMarker, tailMarker);
679
- cache = /* @__PURE__ */new Map();
680
- cleanupCount = 0;
681
- currentKeys = [];
682
- };
683
- const hasAnyKeptKey = (n, newKeys) => {
684
- for (let i = 0; i < n; i++) if (cache.has(newKeys[i])) return true;
685
- return false;
686
- };
687
- const handleIncrementalUpdate = (items, n, newKeys, liveParent) => {
688
- removeStaleForEntries(new Set(newKeys));
689
- mountNewForEntries(items, n, newKeys, liveParent);
690
- if (!anchorsRegistered) {
691
- for (const entry of cache.values()) _forAnchors.add(entry.anchor);
692
- anchorsRegistered = true;
693
- }
694
- if (trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker)) {
695
- currentKeys = newKeys;
696
- return;
697
- }
698
- lis = forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker);
699
- currentKeys = newKeys;
700
- };
701
- const e = effect(() => {
702
- const liveParent = startMarker.parentNode;
703
- if (!liveParent) return;
704
- const items = source();
705
- const n = items.length;
706
- if (n === 0) {
707
- handleFastClear(liveParent);
708
- return;
709
- }
710
- if (currentKeys.length === 0) {
711
- handleFreshRender(items, n, liveParent);
712
- return;
713
- }
714
- const newKeys = collectNewKeys(items, n);
715
- if (!hasAnyKeptKey(n, newKeys)) {
716
- handleReplaceAll(items, n, newKeys, liveParent);
717
- return;
718
- }
719
- handleIncrementalUpdate(items, n, newKeys, liveParent);
720
- });
721
- return () => {
722
- e.dispose();
723
- for (const entry of cache.values()) {
724
- if (cleanupCount > 0 && entry.cleanup) entry.cleanup();
725
- entry.anchor.parentNode?.removeChild(entry.anchor);
726
- }
727
- cache = /* @__PURE__ */new Map();
728
- cleanupCount = 0;
729
- startMarker.parentNode?.removeChild(startMarker);
730
- tailMarker.parentNode?.removeChild(tailMarker);
731
- };
732
- }
71
+ * Hydrate a server-rendered container with a Pyreon VNode tree.
72
+ *
73
+ * Reuses existing DOM elements for static structure, attaches event listeners
74
+ * and reactive effects without re-rendering. Falls back to fresh mount for
75
+ * dynamic content (reactive conditionals, For lists).
76
+ *
77
+ * @example
78
+ * // Server:
79
+ * const html = await renderToString(h(App, null))
80
+ *
81
+ * // Client:
82
+ * const unmount = hydrateRoot(document.getElementById("app")!, h(App, null))
83
+ */
84
+ declare function hydrateRoot(container: Element, vnode: VNodeChild): () => void;
85
+ //#endregion
86
+ //#region src/hydration-debug.d.ts
733
87
  /**
734
- * Small-k reorder: directly place the k displaced entries without LIS.
735
- */
736
- function smallKPlace(parent, diffs, newKeys, cache, tailMarker) {
737
- const diffSet = new Set(diffs);
738
- let cursor = tailMarker;
739
- let prevDiffIdx = newKeys.length;
740
- for (let d = diffs.length - 1; d >= 0; d--) {
741
- const i = diffs[d];
742
- let nextNonDiff = -1;
743
- for (let j = i + 1; j < prevDiffIdx; j++) if (!diffSet.has(j)) {
744
- nextNonDiff = j;
745
- break;
746
- }
747
- if (nextNonDiff >= 0) {
748
- const nc = cache.get(newKeys[nextNonDiff])?.anchor;
749
- if (nc) cursor = nc;
750
- }
751
- const entry = cache.get(newKeys[i]);
752
- if (!entry) {
753
- prevDiffIdx = i;
754
- continue;
755
- }
756
- moveEntryBefore(parent, entry.anchor, cursor);
757
- cursor = entry.anchor;
758
- prevDiffIdx = i;
759
- }
88
+ * Hydration mismatch warnings.
89
+ *
90
+ * Enabled automatically in development (NODE_ENV !== "production").
91
+ * Can be toggled manually for testing or verbose production debugging.
92
+ *
93
+ * @example
94
+ * import { enableHydrationWarnings } from "@pyreon/runtime-dom"
95
+ * enableHydrationWarnings()
96
+ */
97
+ declare function enableHydrationWarnings(): void;
98
+ declare function disableHydrationWarnings(): void;
99
+ //#endregion
100
+ //#region src/keep-alive.d.ts
101
+ interface KeepAliveProps extends Props {
102
+ /**
103
+ * Accessor that returns true when this slot's children should be visible.
104
+ * When false, children are CSS-hidden but remain mounted — effects and
105
+ * signals stay alive.
106
+ * Defaults to true (always visible / always mounted).
107
+ */
108
+ active?: () => boolean;
109
+ children?: VNodeChild;
760
110
  }
761
111
  /**
762
- * Move startNode and all siblings belonging to this entry to just before `before`.
763
- * Stops at the next entry anchor (identified via WeakSet) or the tail marker.
764
- *
765
- * Fast path: if the next sibling is already a boundary (another entry or tail),
766
- * this entry is a single node skip the toMove array entirely.
767
- */
768
- function moveEntryBefore(parent, startNode, before) {
769
- const next = startNode.nextSibling;
770
- if (!next || next === before || next.parentNode === parent && (_forAnchors.has(next) || _keyedAnchors.has(next))) {
771
- parent.insertBefore(startNode, before);
772
- return;
773
- }
774
- const toMove = [startNode];
775
- let cur = next;
776
- while (cur && cur !== before) {
777
- const nextNode = cur.nextSibling;
778
- toMove.push(cur);
779
- cur = nextNode;
780
- if (cur && cur.parentNode === parent && (cur === before || _forAnchors.has(cur) || _keyedAnchors.has(cur))) break;
781
- }
782
- for (const node of toMove) parent.insertBefore(node, before);
783
- }
784
-
112
+ * KeepAlive mounts its children once and keeps them alive even when hidden.
113
+ *
114
+ * Unlike conditional rendering (which destroys and recreates component state),
115
+ * KeepAlive CSS-hides the children while preserving all reactive state,
116
+ * scroll position, form values, and in-flight async operations.
117
+ *
118
+ * Children are mounted imperatively on first activation and are never unmounted
119
+ * while the KeepAlive itself is mounted.
120
+ *
121
+ * Multi-slot pattern (one KeepAlive per route):
122
+ * @example
123
+ * h(Fragment, null, [
124
+ * h(KeepAlive, { active: () => route() === "/a" }, h(RouteA, null)),
125
+ * h(KeepAlive, { active: () => route() === "/b" }, h(RouteB, null)),
126
+ * ])
127
+ *
128
+ * With JSX:
129
+ * @example
130
+ * <>
131
+ * <KeepAlive active={() => route() === "/a"}><RouteA /></KeepAlive>
132
+ * <KeepAlive active={() => route() === "/b"}><RouteB /></KeepAlive>
133
+ * </>
134
+ */
135
+ declare function KeepAlive(props: KeepAliveProps): VNodeChild;
785
136
  //#endregion
786
- //#region src/props.ts
787
-
788
- /**
789
- * Set a custom HTML sanitizer used by `innerHTML` and `sanitizeHtml()`.
790
- * Overrides both the Sanitizer API and the built-in fallback.
791
- *
792
- * @example
793
- * // With DOMPurify:
794
- * import DOMPurify from "dompurify"
795
- * setSanitizer((html) => DOMPurify.sanitize(html))
796
- *
797
- * // With sanitize-html:
798
- * import sanitize from "sanitize-html"
799
- * setSanitizer((html) => sanitize(html))
800
- *
801
- * // Reset to built-in:
802
- * setSanitizer(null)
803
- */
804
- function setSanitizer(fn) {
805
- _customSanitizer = fn;
806
- }
137
+ //#region src/mount.d.ts
138
+ type Cleanup$1 = () => void;
807
139
  /**
808
- * Fallback tag-stripping sanitizer for environments without the Sanitizer API.
809
- * Removes all tags not in SAFE_TAGS, strips event handler attributes,
810
- * and blocks javascript:/data: URLs in href/src/action attributes.
811
- */
812
- function fallbackSanitize(html) {
813
- const doc = new DOMParser().parseFromString(html, "text/html");
814
- sanitizeNode(doc.body);
815
- return doc.body.innerHTML;
816
- }
817
- /** Strip unsafe attributes from a single element. */
818
- function stripUnsafeAttrs(el) {
819
- const attrs = Array.from(el.attributes);
820
- for (const attr of attrs) if (UNSAFE_ATTR_RE.test(attr.name)) el.removeAttribute(attr.name);else if (URL_ATTRS.has(attr.name) && UNSAFE_URL_RE.test(attr.value)) el.removeAttribute(attr.name);
821
- }
822
- function sanitizeNode(node) {
823
- const children = Array.from(node.childNodes);
824
- for (const child of children) {
825
- if (child.nodeType !== 1) continue;
826
- const el = child;
827
- const tag = el.tagName.toLowerCase();
828
- if (!SAFE_TAGS.has(tag)) {
829
- const text = document.createTextNode(el.textContent);
830
- node.replaceChild(text, el);
831
- continue;
832
- }
833
- stripUnsafeAttrs(el);
834
- sanitizeNode(el);
835
- }
836
- }
140
+ * Mount a single child into `parent`, inserting before `anchor` (null = append).
141
+ * Returns a cleanup that removes the node(s) and disposes all reactive effects.
142
+ *
143
+ * This function is the hot path — all child types are handled inline to avoid
144
+ * function call overhead in tight render loops (1000+ calls per list render).
145
+ */
146
+ declare function mountChild(child: VNodeChild | VNodeChild[] | (() => VNodeChild | VNodeChild[]), parent: Node, anchor?: Node | null): Cleanup$1;
147
+ //#endregion
148
+ //#region src/props.d.ts
149
+ type Cleanup = () => void;
837
150
  /**
838
- * Sanitize an HTML string using the browser Sanitizer API (Chrome 105+).
839
- * Falls back to a tag-allowlist sanitizer that strips unsafe elements and attributes.
840
- */
841
- function sanitizeHtml(html) {
842
- if (_customSanitizer) return _customSanitizer(html);
843
- return fallbackSanitize(html);
844
- }
151
+ * Directive function signature.
152
+ * Receives the element and an `addCleanup` callback to register teardown logic.
153
+ *
154
+ * @example
155
+ * const nFocus: Directive = (el) => { el.focus() }
156
+ *
157
+ * // With reactive value (via closure):
158
+ * const nTooltip = (text: () => string): Directive => (el, addCleanup) => {
159
+ * const e = effect(() => { el.title = text() })
160
+ * addCleanup(() => e.dispose())
161
+ * }
162
+ *
163
+ * // Usage:
164
+ * h("input", { "n-focus": nFocus })
165
+ * h("div", { "n-tooltip": nTooltip(() => label()) })
166
+ */
167
+ type Directive = (el: HTMLElement, addCleanup: (fn: Cleanup) => void) => void;
168
+ type SanitizeFn = (html: string) => string;
845
169
  /**
846
- * Apply all props to a DOM element.
847
- * Returns a single chained cleanup (or null if no props need teardown).
848
- * Uses for-in instead of Object.keys() to avoid allocating a keys array.
849
- */
850
- function applyProps(el, props) {
851
- let first = null;
852
- let cleanups = null;
853
- for (const key in props) {
854
- if (key === "key" || key === "ref") continue;
855
- const c = applyProp(el, key, props[key]);
856
- if (c) if (!first) first = c;else if (!cleanups) cleanups = [first, c];else cleanups.push(c);
857
- }
858
- if (cleanups) return () => {
859
- for (const c of cleanups) c();
860
- };
861
- return first;
862
- }
170
+ * Set a custom HTML sanitizer used by `innerHTML` and `sanitizeHtml()`.
171
+ * Overrides both the Sanitizer API and the built-in fallback.
172
+ *
173
+ * @example
174
+ * // With DOMPurify:
175
+ * import DOMPurify from "dompurify"
176
+ * setSanitizer((html) => DOMPurify.sanitize(html))
177
+ *
178
+ * // With sanitize-html:
179
+ * import sanitize from "sanitize-html"
180
+ * setSanitizer((html) => sanitize(html))
181
+ *
182
+ * // Reset to built-in:
183
+ * setSanitizer(null)
184
+ */
185
+ declare function setSanitizer(fn: SanitizeFn | null): void;
863
186
  /**
864
- * Apply a single prop.
865
- *
866
- * - `onXxx` → addEventListener
867
- * - `() => value` (non-event function) → reactive via effect
868
- * - anything else → static attribute / DOM property
869
- */
187
+ * Sanitize an HTML string using the browser Sanitizer API (Chrome 105+).
188
+ * Falls back to a tag-allowlist sanitizer that strips unsafe elements and attributes.
189
+ */
190
+ declare function sanitizeHtml(html: string): string;
870
191
  /**
871
- * Bind an event handler (onClick "click") with batching + delegation support.
872
- */
873
- function applyEventProp(el, key, value) {
874
- if (__DEV__$3 && typeof value !== "function") {
875
- console.warn(`[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). Expected a function. Did you mean ${key}={() => ...}?`);
876
- return null;
877
- }
878
- const eventName = key[2]?.toLowerCase() + key.slice(3);
879
- const handler = value;
880
- if (DELEGATED_EVENTS.has(eventName)) {
881
- const prop = delegatedPropName(eventName);
882
- el[prop] = e => batch(() => handler(e));
883
- return () => {
884
- el[prop] = void 0;
885
- };
886
- }
887
- const batched = e => batch(() => handler(e));
888
- el.addEventListener(eventName, batched);
889
- return () => el.removeEventListener(eventName, batched);
890
- }
891
- function applyProp(el, key, value) {
892
- if (EVENT_RE.test(key)) return applyEventProp(el, key, value);
893
- if (key === "innerHTML") {
894
- if (typeof el.setHTML === "function") el.setHTML(value);else el.innerHTML = sanitizeHtml(value);
895
- return null;
896
- }
897
- if (key === "dangerouslySetInnerHTML") {
898
- if (__DEV__$3) console.warn("[Pyreon] dangerouslySetInnerHTML bypasses sanitization. Ensure the HTML is trusted.");
899
- el.innerHTML = value.__html;
900
- return null;
901
- }
902
- if (key === "n-show") return renderEffect(() => {
903
- const visible = value();
904
- el.style.display = visible ? "" : "none";
905
- });
906
- if (key.startsWith("n-")) {
907
- const directive = value;
908
- const cleanups = [];
909
- directive(el, fn => cleanups.push(fn));
910
- return cleanups.length > 0 ? () => {
911
- for (const fn of cleanups) fn();
912
- } : null;
913
- }
914
- if (typeof value === "function") return renderEffect(() => setStaticProp(el, key, value()));
915
- setStaticProp(el, key, value);
916
- return null;
917
- }
918
- /** Apply a style prop (string or object). */
919
- function applyStyleProp(el, value) {
920
- if (typeof value === "string") el.style.cssText = value;else if (value != null && typeof value === "object") {
921
- const obj = value;
922
- for (const k in obj) {
923
- const css = normalizeStyleValue(k, obj[k]);
924
- el.style.setProperty(k.startsWith("--") ? k : toKebabCase(k), css);
925
- }
926
- }
927
- }
928
- function setStaticProp(el, key, value) {
929
- if (URL_ATTRS.has(key) && typeof value === "string" && UNSAFE_URL_RE.test(value)) {
930
- if (__DEV__$3) console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`);
931
- return;
932
- }
933
- if (key === "class" || key === "className") {
934
- el.setAttribute("class", value == null ? "" : String(value));
935
- return;
936
- }
937
- if (key === "style") {
938
- applyStyleProp(el, value);
939
- return;
940
- }
941
- if (value == null) {
942
- el.removeAttribute(key);
943
- return;
944
- }
945
- if (typeof value === "boolean") {
946
- if (value) el.setAttribute(key, "");else el.removeAttribute(key);
947
- return;
948
- }
949
- if (key in el) {
950
- el[key] = value;
951
- return;
952
- }
953
- el.setAttribute(key, String(value));
954
- }
955
-
192
+ * Apply all props to a DOM element.
193
+ * Returns a single chained cleanup (or null if no props need teardown).
194
+ * Uses for-in instead of Object.keys() to avoid allocating a keys array.
195
+ */
196
+ declare function applyProps(el: Element, props: Props): Cleanup | null;
197
+ declare function applyProp(el: Element, key: string, value: unknown): Cleanup | null;
956
198
  //#endregion
957
- //#region src/mount.ts
958
-
199
+ //#region src/template.d.ts
959
200
  /**
960
- * Mount a single child into `parent`, inserting before `anchor` (null = append).
961
- * Returns a cleanup that removes the node(s) and disposes all reactive effects.
962
- *
963
- * This function is the hot path all child types are handled inline to avoid
964
- * function call overhead in tight render loops (1000+ calls per list render).
965
- */
966
- function mountChild(child, parent, anchor = null) {
967
- if (typeof child === "function") {
968
- const sample = runUntracked(() => child());
969
- if (isKeyedArray(sample)) {
970
- const prevDepth = _elementDepth;
971
- _elementDepth = 0;
972
- const cleanup = mountKeyedList(child, parent, anchor, (v, p, a) => mountChild(v, p, a));
973
- _elementDepth = prevDepth;
974
- return cleanup;
975
- }
976
- if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
977
- const text = document.createTextNode(sample == null || sample === false ? "" : String(sample));
978
- parent.insertBefore(text, anchor);
979
- const dispose = renderEffect(() => {
980
- const v = child();
981
- text.data = v == null || v === false ? "" : String(v);
982
- });
983
- if (_elementDepth > 0) return dispose;
984
- return () => {
985
- dispose();
986
- const p = text.parentNode;
987
- if (p && p.isConnected !== false) p.removeChild(text);
988
- };
989
- }
990
- const prevDepth = _elementDepth;
991
- _elementDepth = 0;
992
- const cleanup = mountReactive(child, parent, anchor, mountChild);
993
- _elementDepth = prevDepth;
994
- return cleanup;
995
- }
996
- if (Array.isArray(child)) return mountChildren(child, parent, anchor);
997
- if (child == null || child === false) return noop$1;
998
- if (typeof child !== "object") {
999
- parent.insertBefore(document.createTextNode(String(child)), anchor);
1000
- return noop$1;
1001
- }
1002
- if (child.__isNative) {
1003
- const native = child;
1004
- parent.insertBefore(native.el, anchor);
1005
- if (!native.cleanup) {
1006
- if (_elementDepth > 0) return noop$1;
1007
- return () => {
1008
- const p = native.el.parentNode;
1009
- if (p && p.isConnected !== false) p.removeChild(native.el);
1010
- };
1011
- }
1012
- if (_elementDepth > 0) return native.cleanup;
1013
- return () => {
1014
- native.cleanup?.();
1015
- const p = native.el.parentNode;
1016
- if (p && p.isConnected !== false) p.removeChild(native.el);
1017
- };
1018
- }
1019
- const vnode = child;
1020
- if (vnode.type === Fragment) return mountChildren(vnode.children, parent, anchor);
1021
- if (vnode.type === ForSymbol) {
1022
- const {
1023
- each,
1024
- by,
1025
- children
1026
- } = vnode.props;
1027
- const prevDepth = _elementDepth;
1028
- _elementDepth = 0;
1029
- const cleanup = mountFor(each, by, children, parent, anchor, mountChild);
1030
- _elementDepth = prevDepth;
1031
- return cleanup;
1032
- }
1033
- if (vnode.type === PortalSymbol) {
1034
- const {
1035
- target,
1036
- children
1037
- } = vnode.props;
1038
- if (__DEV__$2 && !target) {
1039
- console.warn("[Pyreon] <Portal> received a falsy `target`. Provide a valid DOM element.");
1040
- return noop$1;
1041
- }
1042
- if (__DEV__$2 && !(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.`);
1043
- return mountChild(children, target, null);
1044
- }
1045
- if (typeof vnode.type === "function") return mountComponent(vnode, parent, anchor);
1046
- return mountElement(vnode, parent, anchor);
1047
- }
1048
- function mountElement(vnode, parent, anchor) {
1049
- const el = document.createElement(vnode.type);
1050
- if (__DEV__$2 && vnode.children.length > 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.`);
1051
- const props = vnode.props;
1052
- const propCleanup = props !== EMPTY_PROPS ? applyProps(el, props) : null;
1053
- _elementDepth++;
1054
- const childCleanup = mountChildren(vnode.children, el, null);
1055
- _elementDepth--;
1056
- parent.insertBefore(el, anchor);
1057
- const ref = props.ref;
1058
- if (ref) if (typeof ref === "function") ref(el);else ref.current = el;
1059
- if (!propCleanup && childCleanup === noop$1 && !ref) {
1060
- if (_elementDepth > 0) return noop$1;
1061
- return () => {
1062
- const p = el.parentNode;
1063
- if (p && p.isConnected !== false) p.removeChild(el);
1064
- };
1065
- }
1066
- if (_elementDepth > 0) {
1067
- if (!ref && !propCleanup) return childCleanup;
1068
- if (!ref && propCleanup) return () => {
1069
- propCleanup();
1070
- childCleanup();
1071
- };
1072
- const refToClean = ref;
1073
- return () => {
1074
- if (refToClean && typeof refToClean === "object") refToClean.current = null;
1075
- if (propCleanup) propCleanup();
1076
- childCleanup();
1077
- };
1078
- }
1079
- return () => {
1080
- if (ref && typeof ref === "object") ref.current = null;
1081
- if (propCleanup) propCleanup();
1082
- childCleanup();
1083
- const p = el.parentNode;
1084
- if (p && p.isConnected !== false) p.removeChild(el);
1085
- };
1086
- }
1087
- function mountComponent(vnode, parent, anchor) {
1088
- const scope = effectScope();
1089
- setCurrentScope(scope);
1090
- let hooks;
1091
- let output;
1092
- const componentName = vnode.type.name || "Anonymous";
1093
- const compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`;
1094
- const parentId = _mountingStack[_mountingStack.length - 1] ?? null;
1095
- _mountingStack.push(compId);
1096
- const mergedProps = vnode.children.length > 0 && vnode.props.children === void 0 ? {
1097
- ...vnode.props,
1098
- children: vnode.children.length === 1 ? vnode.children[0] : vnode.children
1099
- } : vnode.props;
1100
- try {
1101
- const result = runWithHooks(vnode.type, mergedProps);
1102
- hooks = result.hooks;
1103
- output = result.vnode;
1104
- } catch (err) {
1105
- _mountingStack.pop();
1106
- setCurrentScope(null);
1107
- scope.stop();
1108
- reportError({
1109
- component: componentName,
1110
- phase: "setup",
1111
- error: err,
1112
- timestamp: Date.now(),
1113
- props: vnode.props
1114
- });
1115
- dispatchToErrorBoundary(err);
1116
- return noop$1;
1117
- } finally {
1118
- setCurrentScope(null);
1119
- }
1120
- if (__DEV__$2 && output != null && typeof output === "object") {
1121
- 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.`);else if (!("type" in output)) console.warn(`[Pyreon] Component <${componentName}> returned an invalid value. Components must return a VNode, string, null, or function.`);
1122
- }
1123
- for (const fn of hooks.update) scope.addUpdateHook(fn);
1124
- let subtreeCleanup = noop$1;
1125
- try {
1126
- subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop$1;
1127
- } catch (err) {
1128
- _mountingStack.pop();
1129
- scope.stop();
1130
- if (!(propagateError(err, hooks) || dispatchToErrorBoundary(err))) reportError({
1131
- component: componentName,
1132
- phase: "render",
1133
- error: err,
1134
- timestamp: Date.now(),
1135
- props: vnode.props
1136
- });
1137
- return noop$1;
1138
- }
1139
- _mountingStack.pop();
1140
- registerComponent(compId, componentName, parent instanceof Element ? parent.firstElementChild : null, parentId);
1141
- const mountCleanups = [];
1142
- for (const fn of hooks.mount) try {
1143
- let cleanup;
1144
- scope.runInScope(() => {
1145
- cleanup = fn();
1146
- });
1147
- if (cleanup) mountCleanups.push(cleanup);
1148
- } catch (err) {
1149
- console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err);
1150
- reportError({
1151
- component: componentName,
1152
- phase: "mount",
1153
- error: err,
1154
- timestamp: Date.now()
1155
- });
1156
- }
1157
- return () => {
1158
- unregisterComponent(compId);
1159
- scope.stop();
1160
- subtreeCleanup();
1161
- for (const fn of hooks.unmount) try {
1162
- fn();
1163
- } catch (err) {
1164
- console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err);
1165
- reportError({
1166
- component: componentName,
1167
- phase: "unmount",
1168
- error: err,
1169
- timestamp: Date.now()
1170
- });
1171
- }
1172
- for (const fn of mountCleanups) fn();
1173
- };
1174
- }
1175
- function mountChildren(children, parent, anchor) {
1176
- if (children.length === 0) return noop$1;
1177
- if (children.length === 1) {
1178
- const c = children[0];
1179
- if (c !== void 0) {
1180
- if (anchor === null && (typeof c === "string" || typeof c === "number")) {
1181
- parent.textContent = String(c);
1182
- return noop$1;
1183
- }
1184
- return mountChild(c, parent, anchor);
1185
- }
1186
- }
1187
- if (children.length === 2) {
1188
- const c0 = children[0];
1189
- const c1 = children[1];
1190
- if (c0 !== void 0 && c1 !== void 0) {
1191
- const d0 = mountChild(c0, parent, anchor);
1192
- const d1 = mountChild(c1, parent, anchor);
1193
- if (d0 === noop$1 && d1 === noop$1) return noop$1;
1194
- if (d0 === noop$1) return d1;
1195
- if (d1 === noop$1) return d0;
1196
- return () => {
1197
- d0();
1198
- d1();
1199
- };
1200
- }
1201
- }
1202
- const cleanups = children.map(c => mountChild(c, parent, anchor));
1203
- return () => {
1204
- for (const fn of cleanups) fn();
1205
- };
1206
- }
1207
- /** Returns true if value is a non-empty array of VNodes that all carry keys. */
1208
- function isKeyedArray(value) {
1209
- if (!Array.isArray(value) || value.length === 0) return false;
1210
- return value.every(v => v !== null && typeof v === "object" && !Array.isArray(v) && v.key !== null && v.key !== void 0);
1211
- }
1212
-
1213
- //#endregion
1214
- //#region src/hydrate.ts
1215
-
1216
- /** Skip comment and whitespace-only text nodes, return first "real" node */
1217
- function firstReal(initialNode) {
1218
- let node = initialNode;
1219
- while (node) {
1220
- if (node.nodeType === Node.COMMENT_NODE) {
1221
- node = node.nextSibling;
1222
- continue;
1223
- }
1224
- if (node.nodeType === Node.TEXT_NODE && isWhitespaceOnly(node.data)) {
1225
- node = node.nextSibling;
1226
- continue;
1227
- }
1228
- return node;
1229
- }
1230
- return null;
1231
- }
1232
- /** Check if a string is whitespace-only without allocating a trimmed copy. */
1233
- function isWhitespaceOnly(s) {
1234
- for (let i = 0; i < s.length; i++) {
1235
- const c = s.charCodeAt(i);
1236
- if (c !== 32 && c !== 9 && c !== 10 && c !== 13 && c !== 12) return false;
1237
- }
1238
- return true;
1239
- }
1240
- /** Advance past a node, skipping whitespace-only text and comments */
1241
- function nextReal(node) {
1242
- return firstReal(node.nextSibling);
1243
- }
201
+ * Creates a row/item factory backed by HTML template cloning.
202
+ *
203
+ * - The HTML string is parsed exactly once via <template>.innerHTML.
204
+ * - Each call to the returned factory clones the root element via
205
+ * cloneNode(true) ~5-10x faster than createElement + setAttribute.
206
+ * - `bind` receives the cloned element and the item; it should wire up
207
+ * reactive effects and return a cleanup function.
208
+ * - Returns a NativeItem directly (no VNode wrapper) — saves 2 allocations
209
+ * per row vs the old VNode + props-object + children-array approach.
210
+ *
211
+ * @example
212
+ * const rowTemplate = createTemplate<Row>(
213
+ * "<tr><td></td><td></td></tr>",
214
+ * (el, row) => {
215
+ * const td1 = el.firstChild as HTMLElement
216
+ * const td2 = td1.nextSibling as HTMLElement
217
+ * td1.textContent = String(row.id)
218
+ * const text = td2.firstChild as Text
219
+ * text.data = row.label()
220
+ * const unsub = row.label.subscribe(() => { text.data = row.label() })
221
+ * return unsub
222
+ * }
223
+ * )
224
+ */
225
+ declare function createTemplate<T>(html: string, bind: (el: HTMLElement, item: T) => (() => void) | null): (item: T) => NativeItem;
1244
226
  /**
1245
- * Hydrate a single VNodeChild against the DOM subtree starting at `domNode`.
1246
- * Returns [cleanup, nextDomSibling].
1247
- */
1248
- /** Insert a comment marker before domNode (or append if domNode is null). */
1249
- function insertMarker(parent, domNode, text) {
1250
- const marker = document.createComment(text);
1251
- if (domNode) parent.insertBefore(marker, domNode);else parent.appendChild(marker);
1252
- return marker;
1253
- }
1254
- /** Hydrate a reactive accessor (function child). */
1255
- function hydrateReactiveChild(child, domNode, parent, anchor, path) {
1256
- const initial = runUntracked(child);
1257
- if (initial == null || initial === false) return [mountReactive(child, parent, insertMarker(parent, domNode, "pyreon"), mountChild), domNode];
1258
- if (typeof initial === "string" || typeof initial === "number" || typeof initial === "boolean") return hydrateReactiveText(child, domNode, parent, anchor, path);
1259
- return [mountReactive(child, parent, insertMarker(parent, domNode, "pyreon"), mountChild), domNode ? nextReal(domNode) : null];
1260
- }
1261
- /** Hydrate a reactive text binding against an existing text node. */
1262
- function hydrateReactiveText(child, domNode, parent, anchor, path) {
1263
- if (domNode?.nodeType === Node.TEXT_NODE) {
1264
- const textNode = domNode;
1265
- return [renderEffect(() => {
1266
- const v = child();
1267
- textNode.data = v == null ? "" : String(v);
1268
- }), nextReal(domNode)];
1269
- }
1270
- warnHydrationMismatch("text", "TextNode", domNode?.nodeType ?? "null", `${path} > reactive`);
1271
- return [mountChild(child, parent, anchor), domNode];
1272
- }
1273
- /** Hydrate a VNode (fragment, For, Portal, component, element). */
1274
- function hydrateVNode(vnode, domNode, parent, anchor, path) {
1275
- if (vnode.type === Fragment) return hydrateChildren(vnode.children, domNode, parent, anchor, path);
1276
- if (vnode.type === ForSymbol) return [mountChild(vnode, parent, insertMarker(parent, domNode, "pyreon-for")), null];
1277
- if (vnode.type === PortalSymbol) return [mountChild(vnode, parent, anchor), domNode];
1278
- if (typeof vnode.type === "function") return hydrateComponent(vnode, domNode, parent, anchor, path);
1279
- if (typeof vnode.type === "string") return hydrateElement(vnode, domNode, parent, anchor, path);
1280
- return [noop, domNode];
1281
- }
1282
- function hydrateChild(child, domNode, parent, anchor, path = "root") {
1283
- if (Array.isArray(child)) {
1284
- const cleanups = [];
1285
- let cursor = domNode;
1286
- for (const c of child) {
1287
- const [cleanup, next] = hydrateChild(c, cursor, parent, anchor, path);
1288
- cleanups.push(cleanup);
1289
- cursor = next;
1290
- }
1291
- return [() => {
1292
- for (const c of cleanups) c();
1293
- }, cursor];
1294
- }
1295
- if (child == null || child === false) return [noop, domNode];
1296
- if (typeof child === "function") return hydrateReactiveChild(child, domNode, parent, anchor, path);
1297
- if (typeof child === "string" || typeof child === "number") {
1298
- if (domNode?.nodeType === Node.TEXT_NODE) return [() => domNode.remove(), nextReal(domNode)];
1299
- warnHydrationMismatch("text", "TextNode", domNode?.nodeType ?? "null", `${path} > text`);
1300
- return [mountChild(child, parent, anchor), domNode];
1301
- }
1302
- return hydrateVNode(child, domNode, parent, anchor, path);
1303
- }
1304
- function hydrateElement(vnode, domNode, parent, anchor, path = "root") {
1305
- const elPath = `${path} > ${vnode.type}`;
1306
- if (domNode?.nodeType === Node.ELEMENT_NODE && domNode.tagName.toLowerCase() === vnode.type) {
1307
- const el = domNode;
1308
- const cleanups = [];
1309
- const propCleanup = applyProps(el, vnode.props);
1310
- if (propCleanup) cleanups.push(propCleanup);
1311
- const firstChild = firstReal(el.firstChild);
1312
- const [childCleanup] = hydrateChildren(vnode.children, firstChild, el, null, elPath);
1313
- cleanups.push(childCleanup);
1314
- const ref = vnode.props.ref;
1315
- if (ref) if (typeof ref === "function") ref(el);else ref.current = el;
1316
- const cleanup = () => {
1317
- if (ref && typeof ref === "object") ref.current = null;
1318
- for (const c of cleanups) c();
1319
- el.remove();
1320
- };
1321
- return [cleanup, nextReal(domNode)];
1322
- }
1323
- const actual = domNode?.nodeType === Node.ELEMENT_NODE ? domNode.tagName.toLowerCase() : domNode?.nodeType ?? "null";
1324
- warnHydrationMismatch("tag", vnode.type, actual, elPath);
1325
- return [mountChild(vnode, parent, anchor), domNode];
1326
- }
1327
- function hydrateChildren(children, domNode, parent, anchor, path = "root") {
1328
- if (children.length === 0) return [noop, domNode];
1329
- if (children.length === 1) return hydrateChild(children[0], domNode, parent, anchor, path);
1330
- const cleanups = [];
1331
- let cursor = domNode;
1332
- for (const child of children) {
1333
- const [cleanup, next] = hydrateChild(child, cursor, parent, anchor, path);
1334
- cleanups.push(cleanup);
1335
- cursor = next;
1336
- }
1337
- return [() => {
1338
- for (const c of cleanups) c();
1339
- }, cursor];
1340
- }
1341
- function hydrateComponent(vnode, domNode, parent, anchor, path = "root") {
1342
- const scope = effectScope();
1343
- setCurrentScope(scope);
1344
- let subtreeCleanup = noop;
1345
- const mountCleanups = [];
1346
- let nextDom = domNode;
1347
- const componentName = vnode.type.name || "Anonymous";
1348
- const mergedProps = vnode.children.length > 0 && vnode.props.children === void 0 ? {
1349
- ...vnode.props,
1350
- children: vnode.children.length === 1 ? vnode.children[0] : vnode.children
1351
- } : vnode.props;
1352
- let result;
1353
- try {
1354
- result = runWithHooks(vnode.type, mergedProps);
1355
- } catch (err) {
1356
- setCurrentScope(null);
1357
- scope.stop();
1358
- console.error(`[Pyreon] Error hydrating component <${componentName}>:`, err);
1359
- reportError({
1360
- component: componentName,
1361
- phase: "setup",
1362
- error: err,
1363
- timestamp: Date.now(),
1364
- props: vnode.props
1365
- });
1366
- dispatchToErrorBoundary(err);
1367
- return [noop, domNode];
1368
- }
1369
- setCurrentScope(null);
1370
- const {
1371
- vnode: output,
1372
- hooks
1373
- } = result;
1374
- for (const fn of hooks.update) scope.addUpdateHook(fn);
1375
- if (output != null) {
1376
- const [childCleanup, next] = hydrateChild(output, domNode, parent, anchor, path);
1377
- subtreeCleanup = childCleanup;
1378
- nextDom = next;
1379
- }
1380
- for (const fn of hooks.mount) try {
1381
- let c;
1382
- scope.runInScope(() => {
1383
- c = fn();
1384
- });
1385
- if (c) mountCleanups.push(c);
1386
- } catch (err) {
1387
- reportError({
1388
- component: componentName,
1389
- phase: "mount",
1390
- error: err,
1391
- timestamp: Date.now()
1392
- });
1393
- }
1394
- const cleanup = () => {
1395
- scope.stop();
1396
- subtreeCleanup();
1397
- for (const fn of hooks.unmount) fn();
1398
- for (const fn of mountCleanups) fn();
1399
- };
1400
- return [cleanup, nextDom];
1401
- }
227
+ * Compiler-emitted direct text binding for single-signal text nodes.
228
+ *
229
+ * When the compiler detects `{signal()}` as the only reactive expression
230
+ * in a text binding, it emits `_bindText(signal, textNode)` instead of
231
+ * `_bind(() => { textNode.data = signal() })`.
232
+ *
233
+ * This bypasses the effect system entirely:
234
+ * - No deps array allocation
235
+ * - No withTracking / setDepsCollector overhead
236
+ * - No `run` closure
237
+ * - Signal.subscribe is used directly (O(1) subscribe + unsubscribe)
238
+ *
239
+ * @param source - A signal (anything with `._v` and `.direct`)
240
+ * @param node - The Text node to update
241
+ */
242
+ declare function _bindText(source: {
243
+ _v: unknown;
244
+ direct: (fn: () => void) => () => void;
245
+ }, node: Text): () => void;
1402
246
  /**
1403
- * Hydrate a server-rendered container with a Pyreon VNode tree.
1404
- *
1405
- * Reuses existing DOM elements for static structure, attaches event listeners
1406
- * and reactive effects without re-rendering. Falls back to fresh mount for
1407
- * dynamic content (reactive conditionals, For lists).
1408
- *
1409
- * @example
1410
- * // Server:
1411
- * const html = await renderToString(h(App, null))
1412
- *
1413
- * // Client:
1414
- * const unmount = hydrateRoot(document.getElementById("app")!, h(App, null))
1415
- */
1416
- function hydrateRoot(container, vnode) {
1417
- setupDelegation(container);
1418
- const [cleanup] = hydrateChild(vnode, firstReal(container.firstChild), container, null);
1419
- return cleanup;
1420
- }
1421
-
1422
- //#endregion
1423
- //#region src/keep-alive.ts
247
+ * Compiler-emitted direct binding for single-signal reactive expressions.
248
+ *
249
+ * Like _bindText but for arbitrary DOM updates (attributes, className, style).
250
+ * When the compiler detects that a reactive expression depends on exactly one
251
+ * signal call, it emits `_bindDirect(signal, updater)` instead of
252
+ * `_bind(() => { updater() })`.
253
+ *
254
+ * Uses signal.direct() for zero-overhead registration:
255
+ * - Flat array instead of Set (no hashing)
256
+ * - Index-based disposal (no Set.delete)
257
+ * - No deps array, no withTracking, no run closure
258
+ *
259
+ * @param source - A signal (anything with `._v` and `.direct`)
260
+ * @param updater - Function that reads `source._v` and applies the DOM update
261
+ */
262
+ declare function _bindDirect(source: {
263
+ _v: unknown;
264
+ direct: (fn: () => void) => () => void;
265
+ }, updater: (value: unknown) => void): () => void;
1424
266
  /**
1425
- * KeepAlive mounts its children once and keeps them alive even when hidden.
1426
- *
1427
- * Unlike conditional rendering (which destroys and recreates component state),
1428
- * KeepAlive CSS-hides the children while preserving all reactive state,
1429
- * scroll position, form values, and in-flight async operations.
1430
- *
1431
- * Children are mounted imperatively on first activation and are never unmounted
1432
- * while the KeepAlive itself is mounted.
1433
- *
1434
- * Multi-slot pattern (one KeepAlive per route):
1435
- * @example
1436
- * h(Fragment, null, [
1437
- * h(KeepAlive, { active: () => route() === "/a" }, h(RouteA, null)),
1438
- * h(KeepAlive, { active: () => route() === "/b" }, h(RouteB, null)),
1439
- * ])
1440
- *
1441
- * With JSX:
1442
- * @example
1443
- * <>
1444
- * <KeepAlive active={() => route() === "/a"}><RouteA /></KeepAlive>
1445
- * <KeepAlive active={() => route() === "/b"}><RouteB /></KeepAlive>
1446
- * </>
1447
- */
1448
- function KeepAlive(props) {
1449
- const containerRef = createRef();
1450
- let childCleanup = null;
1451
- let childMounted = false;
1452
- onMount(() => {
1453
- const container = containerRef.current;
1454
- const e = effect(() => {
1455
- const isActive = props.active?.() ?? true;
1456
- if (!childMounted) {
1457
- childCleanup = mountChild(props.children ?? null, container, null);
1458
- childMounted = true;
1459
- }
1460
- container.style.display = isActive ? "" : "none";
1461
- });
1462
- return () => {
1463
- e.dispose();
1464
- childCleanup?.();
1465
- };
1466
- });
1467
- return h("div", {
1468
- ref: containerRef,
1469
- style: "display: contents"
1470
- });
1471
- }
1472
-
267
+ * Compiler-emitted template instantiation.
268
+ *
269
+ * Parses `html` into a <template> element once (cached), then cloneNode(true)
270
+ * for each call. The `bind` function wires up dynamic attributes, text content,
271
+ * and event listeners on the cloned element tree. Returns a NativeItem that
272
+ * mountChild can insert directly — no VNode allocation.
273
+ *
274
+ * This is the runtime half of the compiler's template optimisation. The compiler
275
+ * detects static JSX element trees and emits `_tpl(html, bindFn)` instead of
276
+ * nested `h()` calls. Benefits:
277
+ * - cloneNode(true) is ~5-10x faster than sequential createElement + setAttribute
278
+ * - Zero VNode / props-object / children-array allocations per instance
279
+ * - Static attributes are baked into the HTML string (no runtime prop application)
280
+ *
281
+ * @example
282
+ * // Compiler output for: <div class="box"><span>{text()}</span></div>
283
+ * _tpl('<div class="box"><span></span></div>', (__root) => {
284
+ * const __e0 = __root.children[0];
285
+ * const __d0 = _re(() => { __e0.textContent = text(); });
286
+ * return () => { __d0(); };
287
+ * })
288
+ */
289
+ declare function _tpl(html: string, bind: (el: HTMLElement) => (() => void) | null): NativeItem;
1473
290
  //#endregion
1474
- //#region src/template.ts
1475
- /**
1476
- * Creates a row/item factory backed by HTML template cloning.
1477
- *
1478
- * - The HTML string is parsed exactly once via <template>.innerHTML.
1479
- * - Each call to the returned factory clones the root element via
1480
- * cloneNode(true) — ~5-10x faster than createElement + setAttribute.
1481
- * - `bind` receives the cloned element and the item; it should wire up
1482
- * reactive effects and return a cleanup function.
1483
- * - Returns a NativeItem directly (no VNode wrapper) saves 2 allocations
1484
- * per row vs the old VNode + props-object + children-array approach.
1485
- *
1486
- * @example
1487
- * const rowTemplate = createTemplate<Row>(
1488
- * "<tr><td></td><td></td></tr>",
1489
- * (el, row) => {
1490
- * const td1 = el.firstChild as HTMLElement
1491
- * const td2 = td1.nextSibling as HTMLElement
1492
- * td1.textContent = String(row.id)
1493
- * const text = td2.firstChild as Text
1494
- * text.data = row.label()
1495
- * const unsub = row.label.subscribe(() => { text.data = row.label() })
1496
- * return unsub
1497
- * }
1498
- * )
1499
- */
1500
- function createTemplate(html, bind) {
1501
- const tmpl = document.createElement("template");
1502
- tmpl.innerHTML = html;
1503
- const proto = tmpl.content.firstElementChild;
1504
- return item => {
1505
- const el = proto.cloneNode(true);
1506
- return {
1507
- __isNative: true,
1508
- el,
1509
- cleanup: bind(el, item)
1510
- };
1511
- };
291
+ //#region src/transition.d.ts
292
+ interface TransitionProps {
293
+ /**
294
+ * CSS class name prefix.
295
+ * "fade" fade-enter-from, fade-enter-active, fade-enter-to, fade-leave-from,
296
+ * Default: "pyreon"
297
+ */
298
+ name?: string;
299
+ /** Reactive boolean controlling whether the child is shown. */
300
+ show: () => boolean;
301
+ /**
302
+ * If true, runs the enter transition on the initial mount (instead of
303
+ * appearing immediately). Default: false.
304
+ */
305
+ appear?: boolean;
306
+ enterFrom?: string;
307
+ enterActive?: string;
308
+ enterTo?: string;
309
+ leaveFrom?: string;
310
+ leaveActive?: string;
311
+ leaveTo?: string;
312
+ onBeforeEnter?: (el: HTMLElement) => void;
313
+ onAfterEnter?: (el: HTMLElement) => void;
314
+ onBeforeLeave?: (el: HTMLElement) => void;
315
+ onAfterLeave?: (el: HTMLElement) => void;
316
+ /**
317
+ * The single child element to animate.
318
+ * Must be a direct DOM element VNode (not a component) for class injection to work.
319
+ */
320
+ children?: VNodeChild;
1512
321
  }
1513
322
  /**
1514
- * Compiler-emitted direct text binding for single-signal text nodes.
1515
- *
1516
- * When the compiler detects `{signal()}` as the only reactive expression
1517
- * in a text binding, it emits `_bindText(signal, textNode)` instead of
1518
- * `_bind(() => { textNode.data = signal() })`.
1519
- *
1520
- * This bypasses the effect system entirely:
1521
- * - No deps array allocation
1522
- * - No withTracking / setDepsCollector overhead
1523
- * - No `run` closure
1524
- * - Signal.subscribe is used directly (O(1) subscribe + unsubscribe)
1525
- *
1526
- * @param source - A signal (anything with `._v` and `.direct`)
1527
- * @param node - The Text node to update
1528
- */
1529
- function _bindText(source, node) {
1530
- const update = () => {
1531
- const v = source._v;
1532
- node.data = v == null || v === false ? "" : String(v);
1533
- };
1534
- update();
1535
- return source.direct(update);
1536
- }
1537
- /**
1538
- * Compiler-emitted direct binding for single-signal reactive expressions.
1539
- *
1540
- * Like _bindText but for arbitrary DOM updates (attributes, className, style).
1541
- * When the compiler detects that a reactive expression depends on exactly one
1542
- * signal call, it emits `_bindDirect(signal, updater)` instead of
1543
- * `_bind(() => { updater() })`.
1544
- *
1545
- * Uses signal.direct() for zero-overhead registration:
1546
- * - Flat array instead of Set (no hashing)
1547
- * - Index-based disposal (no Set.delete)
1548
- * - No deps array, no withTracking, no run closure
1549
- *
1550
- * @param source - A signal (anything with `._v` and `.direct`)
1551
- * @param updater - Function that reads `source._v` and applies the DOM update
1552
- */
1553
- function _bindDirect(source, updater) {
1554
- updater(source._v);
1555
- return source.direct(() => updater(source._v));
1556
- }
1557
- /**
1558
- * Compiler-emitted template instantiation.
1559
- *
1560
- * Parses `html` into a <template> element once (cached), then cloneNode(true)
1561
- * for each call. The `bind` function wires up dynamic attributes, text content,
1562
- * and event listeners on the cloned element tree. Returns a NativeItem that
1563
- * mountChild can insert directly — no VNode allocation.
1564
- *
1565
- * This is the runtime half of the compiler's template optimisation. The compiler
1566
- * detects static JSX element trees and emits `_tpl(html, bindFn)` instead of
1567
- * nested `h()` calls. Benefits:
1568
- * - cloneNode(true) is ~5-10x faster than sequential createElement + setAttribute
1569
- * - Zero VNode / props-object / children-array allocations per instance
1570
- * - Static attributes are baked into the HTML string (no runtime prop application)
1571
- *
1572
- * @example
1573
- * // Compiler output for: <div class="box"><span>{text()}</span></div>
1574
- * _tpl('<div class="box"><span></span></div>', (__root) => {
1575
- * const __e0 = __root.children[0];
1576
- * const __d0 = _re(() => { __e0.textContent = text(); });
1577
- * return () => { __d0(); };
1578
- * })
1579
- */
1580
- function _tpl(html, bind) {
1581
- let tpl = _tplCache.get(html);
1582
- if (!tpl) {
1583
- tpl = document.createElement("template");
1584
- tpl.innerHTML = html;
1585
- _tplCache.set(html, tpl);
1586
- }
1587
- const el = tpl.content.firstElementChild?.cloneNode(true);
1588
- return {
1589
- __isNative: true,
1590
- el,
1591
- cleanup: bind(el)
1592
- };
1593
- }
1594
-
323
+ * Transition adds CSS enter/leave animation classes to a single child element,
324
+ * controlled by the reactive `show` prop.
325
+ *
326
+ * Class lifecycle:
327
+ * Enter: {name}-enter-from → (next frame) {name}-enter-active + {name}-enter-to cleanup
328
+ * Leave: {name}-leave-from → (next frame) → {name}-leave-active + {name}-leave-to → unmount
329
+ *
330
+ * The child element stays in the DOM during the leave animation and is removed only
331
+ * after the CSS transition / animation completes.
332
+ *
333
+ * @example
334
+ * const visible = signal(false)
335
+ *
336
+ * h(Transition, { name: "fade", show: () => visible() },
337
+ * h("div", { class: "modal" }, "content")
338
+ * )
339
+ *
340
+ * // CSS:
341
+ * // .fade-enter-from, .fade-leave-to { opacity: 0; }
342
+ * // .fade-enter-active, .fade-leave-active { transition: opacity 300ms ease; }
343
+ */
344
+ declare function Transition(props: TransitionProps): VNodeChild;
1595
345
  //#endregion
1596
- //#region src/transition.ts
1597
-
1598
- /**
1599
- * Transition — adds CSS enter/leave animation classes to a single child element,
1600
- * controlled by the reactive `show` prop.
1601
- *
1602
- * Class lifecycle:
1603
- * Enter: {name}-enter-from (next frame) → {name}-enter-active + {name}-enter-to → cleanup
1604
- * Leave: {name}-leave-from (next frame) → {name}-leave-active + {name}-leave-to → unmount
1605
- *
1606
- * The child element stays in the DOM during the leave animation and is removed only
1607
- * after the CSS transition / animation completes.
1608
- *
1609
- * @example
1610
- * const visible = signal(false)
1611
- *
1612
- * h(Transition, { name: "fade", show: () => visible() },
1613
- * h("div", { class: "modal" }, "content")
1614
- * )
1615
- *
1616
- * // CSS:
1617
- * // .fade-enter-from, .fade-leave-to { opacity: 0; }
1618
- * // .fade-enter-active, .fade-leave-active { transition: opacity 300ms ease; }
1619
- */
1620
- function Transition(props) {
1621
- const n = props.name ?? "pyreon";
1622
- const cls = {
1623
- ef: props.enterFrom ?? `${n}-enter-from`,
1624
- ea: props.enterActive ?? `${n}-enter-active`,
1625
- et: props.enterTo ?? `${n}-enter-to`,
1626
- lf: props.leaveFrom ?? `${n}-leave-from`,
1627
- la: props.leaveActive ?? `${n}-leave-active`,
1628
- lt: props.leaveTo ?? `${n}-leave-to`
1629
- };
1630
- const ref = createRef();
1631
- const isMounted = signal(runUntracked(props.show));
1632
- let pendingLeaveCancel = null;
1633
- let initialized = false;
1634
- const applyEnter = el => {
1635
- pendingLeaveCancel?.();
1636
- pendingLeaveCancel = null;
1637
- props.onBeforeEnter?.(el);
1638
- el.classList.remove(cls.lf, cls.la, cls.lt);
1639
- el.classList.add(cls.ef, cls.ea);
1640
- requestAnimationFrame(() => {
1641
- el.classList.remove(cls.ef);
1642
- el.classList.add(cls.et);
1643
- const done = () => {
1644
- el.removeEventListener("transitionend", done);
1645
- el.removeEventListener("animationend", done);
1646
- el.classList.remove(cls.ea, cls.et);
1647
- props.onAfterEnter?.(el);
1648
- };
1649
- el.addEventListener("transitionend", done, {
1650
- once: true
1651
- });
1652
- el.addEventListener("animationend", done, {
1653
- once: true
1654
- });
1655
- });
1656
- };
1657
- const applyLeave = el => {
1658
- props.onBeforeLeave?.(el);
1659
- el.classList.remove(cls.ef, cls.ea, cls.et);
1660
- el.classList.add(cls.lf, cls.la);
1661
- requestAnimationFrame(() => {
1662
- el.classList.remove(cls.lf);
1663
- el.classList.add(cls.lt);
1664
- const done = () => {
1665
- el.removeEventListener("transitionend", done);
1666
- el.removeEventListener("animationend", done);
1667
- el.classList.remove(cls.la, cls.lt);
1668
- pendingLeaveCancel = null;
1669
- isMounted.set(false);
1670
- props.onAfterLeave?.(el);
1671
- };
1672
- pendingLeaveCancel = () => {
1673
- el.removeEventListener("transitionend", done);
1674
- el.removeEventListener("animationend", done);
1675
- el.classList.remove(cls.lf, cls.la, cls.lt);
1676
- };
1677
- el.addEventListener("transitionend", done, {
1678
- once: true
1679
- });
1680
- el.addEventListener("animationend", done, {
1681
- once: true
1682
- });
1683
- });
1684
- };
1685
- const handleVisibilityChange = visible => {
1686
- if (visible) {
1687
- if (!isMounted.peek()) isMounted.set(true);
1688
- queueMicrotask(() => applyEnter(ref.current));
1689
- return;
1690
- }
1691
- if (!isMounted.peek()) return;
1692
- const el = ref.current;
1693
- if (!el) {
1694
- isMounted.set(false);
1695
- return;
1696
- }
1697
- applyLeave(el);
1698
- };
1699
- effect(() => {
1700
- const visible = props.show();
1701
- if (!initialized) {
1702
- initialized = true;
1703
- if (visible && props.appear) queueMicrotask(() => applyEnter(ref.current));
1704
- return;
1705
- }
1706
- handleVisibilityChange(visible);
1707
- });
1708
- onUnmount(() => {
1709
- pendingLeaveCancel?.();
1710
- pendingLeaveCancel = null;
1711
- });
1712
- const rawChild = props.children;
1713
- const emptyFragment = h(Fragment, null);
1714
- return () => {
1715
- if (!isMounted()) return emptyFragment;
1716
- if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) return rawChild ?? null;
1717
- const vnode = rawChild;
1718
- if (typeof vnode.type !== "string") {
1719
- if (__DEV__$1) console.warn("[Pyreon] Transition child is a component. Wrap it in a DOM element for enter/leave animations to work.");
1720
- return vnode;
1721
- }
1722
- return {
1723
- ...vnode,
1724
- props: {
1725
- ...vnode.props,
1726
- ref
1727
- }
1728
- };
1729
- };
346
+ //#region src/transition-group.d.ts
347
+ interface TransitionGroupProps<T = unknown> {
348
+ /** Wrapper element tag. Default: "div" */
349
+ tag?: string;
350
+ /** CSS class prefix. Default: "pyreon" */
351
+ name?: string;
352
+ /** Animate items on initial mount. Default: false */
353
+ appear?: boolean;
354
+ enterFrom?: string;
355
+ enterActive?: string;
356
+ enterTo?: string;
357
+ leaveFrom?: string;
358
+ leaveActive?: string;
359
+ leaveTo?: string;
360
+ /** Class applied during FLIP move animation. Default: "{name}-move" */
361
+ moveClass?: string;
362
+ /** Reactive list source */
363
+ items: () => T[];
364
+ /** Stable key extractor */
365
+ keyFn: (item: T, index: number) => string | number;
366
+ /**
367
+ * Render a single DOM-element VNode for each item.
368
+ * Must return a VNode whose `type` is a string (e.g. "div", "li") so
369
+ * the component can inject a ref and read the underlying DOM node.
370
+ */
371
+ render: (item: T, index: number) => VNode;
372
+ onBeforeEnter?: (el: HTMLElement) => void;
373
+ onAfterEnter?: (el: HTMLElement) => void;
374
+ onBeforeLeave?: (el: HTMLElement) => void;
375
+ onAfterLeave?: (el: HTMLElement) => void;
1730
376
  }
1731
-
1732
- //#endregion
1733
- //#region src/transition-group.ts
1734
377
  /**
1735
- * TransitionGroup — animates a keyed reactive list with CSS enter/leave and
1736
- * FLIP move animations.
1737
- *
1738
- * Class lifecycle:
1739
- * Enter: {name}-enter-from → {name}-enter-active + {name}-enter-to → cleanup
1740
- * Leave: {name}-leave-from → {name}-leave-active + {name}-leave-to → item removed
1741
- * Move: {name}-move (applied when an item shifts position)
1742
- *
1743
- * @example
1744
- * const items = signal([{ id: 1 }, { id: 2 }])
1745
- *
1746
- * h(TransitionGroup, {
1747
- * tag: "ul",
1748
- * name: "list",
1749
- * items,
1750
- * keyFn: (item) => item.id,
1751
- * render: (item) => h("li", { class: "item" }, item.id),
1752
- * })
1753
- *
1754
- * // CSS:
1755
- * // .list-enter-from, .list-leave-to { opacity: 0; transform: translateY(-10px); }
1756
- * // .list-enter-active, .list-leave-active { transition: all 300ms ease; }
1757
- * // .list-move { transition: transform 300ms ease; }
1758
- */
1759
- function TransitionGroup(props) {
1760
- const tag = props.tag ?? "div";
1761
- const n = props.name ?? "pyreon";
1762
- const cls = {
1763
- ef: props.enterFrom ?? `${n}-enter-from`,
1764
- ea: props.enterActive ?? `${n}-enter-active`,
1765
- et: props.enterTo ?? `${n}-enter-to`,
1766
- lf: props.leaveFrom ?? `${n}-leave-from`,
1767
- la: props.leaveActive ?? `${n}-leave-active`,
1768
- lt: props.leaveTo ?? `${n}-leave-to`,
1769
- mv: props.moveClass ?? `${n}-move`
1770
- };
1771
- const containerRef = createRef();
1772
- const entries = /* @__PURE__ */new Map();
1773
- const ready = signal(false);
1774
- let firstRun = true;
1775
- const applyEnter = el => {
1776
- props.onBeforeEnter?.(el);
1777
- el.classList.remove(cls.lf, cls.la, cls.lt);
1778
- el.classList.add(cls.ef, cls.ea);
1779
- requestAnimationFrame(() => {
1780
- el.classList.remove(cls.ef);
1781
- el.classList.add(cls.et);
1782
- const done = () => {
1783
- el.removeEventListener("transitionend", done);
1784
- el.removeEventListener("animationend", done);
1785
- el.classList.remove(cls.ea, cls.et);
1786
- props.onAfterEnter?.(el);
1787
- };
1788
- el.addEventListener("transitionend", done, {
1789
- once: true
1790
- });
1791
- el.addEventListener("animationend", done, {
1792
- once: true
1793
- });
1794
- });
1795
- };
1796
- const applyLeave = (el, onDone) => {
1797
- props.onBeforeLeave?.(el);
1798
- el.classList.remove(cls.ef, cls.ea, cls.et);
1799
- el.classList.add(cls.lf, cls.la);
1800
- requestAnimationFrame(() => {
1801
- el.classList.remove(cls.lf);
1802
- el.classList.add(cls.lt);
1803
- const done = () => {
1804
- el.removeEventListener("transitionend", done);
1805
- el.removeEventListener("animationend", done);
1806
- el.classList.remove(cls.la, cls.lt);
1807
- props.onAfterLeave?.(el);
1808
- onDone();
1809
- };
1810
- el.addEventListener("transitionend", done, {
1811
- once: true
1812
- });
1813
- el.addEventListener("animationend", done, {
1814
- once: true
1815
- });
1816
- });
1817
- };
1818
- /** Start leave animation for removed items. */
1819
- const processLeaves = newKeys => {
1820
- for (const [key, entry] of entries) {
1821
- if (newKeys.has(key) || entry.leaving) continue;
1822
- entry.leaving = true;
1823
- const el = entry.ref.current;
1824
- if (el) applyLeave(el, () => {
1825
- entry.cleanup();
1826
- entries.delete(key);
1827
- });else {
1828
- entry.cleanup();
1829
- entries.delete(key);
1830
- }
1831
- }
1832
- };
1833
- /** Mount new items and return the list of newly created entries. */
1834
- const mountNewItems = (items, container) => {
1835
- const newEntries = [];
1836
- for (let i = 0; i < items.length; i++) {
1837
- const item = items[i];
1838
- const key = props.keyFn(item, i);
1839
- if (entries.has(key)) continue;
1840
- const itemRef = createRef();
1841
- const rawVNode = runUntracked(() => props.render(item, i));
1842
- const entry = {
1843
- key,
1844
- ref: itemRef,
1845
- cleanup: mountChild(typeof rawVNode.type === "string" ? {
1846
- ...rawVNode,
1847
- props: {
1848
- ...rawVNode.props,
1849
- ref: itemRef
1850
- }
1851
- } : rawVNode, container, null),
1852
- leaving: false
1853
- };
1854
- entries.set(key, entry);
1855
- newEntries.push(entry);
1856
- }
1857
- return newEntries;
1858
- };
1859
- const startMoveAnimation = el => {
1860
- requestAnimationFrame(() => {
1861
- el.classList.add(cls.mv);
1862
- el.style.transform = "";
1863
- el.style.transition = "";
1864
- const done = () => {
1865
- el.removeEventListener("transitionend", done);
1866
- el.removeEventListener("animationend", done);
1867
- el.classList.remove(cls.mv);
1868
- };
1869
- el.addEventListener("transitionend", done, {
1870
- once: true
1871
- });
1872
- el.addEventListener("animationend", done, {
1873
- once: true
1874
- });
1875
- });
1876
- };
1877
- const flipEntry = (entry, oldPos) => {
1878
- if (!entry.ref.current) return;
1879
- const newPos = entry.ref.current.getBoundingClientRect();
1880
- const dx = oldPos.left - newPos.left;
1881
- const dy = oldPos.top - newPos.top;
1882
- if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return;
1883
- const el = entry.ref.current;
1884
- el.style.transform = `translate(${dx}px, ${dy}px)`;
1885
- el.style.transition = "none";
1886
- startMoveAnimation(el);
1887
- };
1888
- /** Apply FLIP move animations for items that shifted position. */
1889
- const applyFlipMoves = oldPositions => {
1890
- requestAnimationFrame(() => {
1891
- for (const [key, entry] of entries) {
1892
- if (entry.leaving) continue;
1893
- const oldPos = oldPositions.get(key);
1894
- if (!oldPos) continue;
1895
- flipEntry(entry, oldPos);
1896
- }
1897
- });
1898
- };
1899
- const recordOldPositions = () => {
1900
- const oldPositions = /* @__PURE__ */new Map();
1901
- for (const [key, entry] of entries) if (!entry.leaving && entry.ref.current) oldPositions.set(key, entry.ref.current.getBoundingClientRect());
1902
- return oldPositions;
1903
- };
1904
- const reorderEntries = (items, container) => {
1905
- for (let i = 0; i < items.length; i++) {
1906
- const key = props.keyFn(items[i], i);
1907
- const entry = entries.get(key);
1908
- if (!entry || entry.leaving || !entry.ref.current) continue;
1909
- container.appendChild(entry.ref.current);
1910
- }
1911
- };
1912
- const animateNewEntries = newEntries => {
1913
- for (const entry of newEntries) queueMicrotask(() => {
1914
- if (entry.ref.current) applyEnter(entry.ref.current);
1915
- });
1916
- };
1917
- const e = effect(() => {
1918
- if (!ready()) return;
1919
- const container = containerRef.current;
1920
- if (!container) return;
1921
- const items = props.items();
1922
- const newKeys = new Set(items.map((item, i) => props.keyFn(item, i)));
1923
- const isFirst = firstRun;
1924
- firstRun = false;
1925
- const oldPositions = recordOldPositions();
1926
- processLeaves(newKeys);
1927
- const newEntries = mountNewItems(items, container);
1928
- reorderEntries(items, container);
1929
- if (!isFirst || props.appear) animateNewEntries(newEntries);
1930
- if (!isFirst && oldPositions.size > 0) applyFlipMoves(oldPositions);
1931
- });
1932
- onMount(() => {
1933
- ready.set(true);
1934
- });
1935
- onUnmount(() => {
1936
- e.dispose();
1937
- for (const entry of entries.values()) entry.cleanup();
1938
- entries.clear();
1939
- });
1940
- return h(tag, {
1941
- ref: containerRef
1942
- });
1943
- }
1944
-
378
+ * TransitionGroup — animates a keyed reactive list with CSS enter/leave and
379
+ * FLIP move animations.
380
+ *
381
+ * Class lifecycle:
382
+ * Enter: {name}-enter-from → {name}-enter-active + {name}-enter-to → cleanup
383
+ * Leave: {name}-leave-from → {name}-leave-active + {name}-leave-to → item removed
384
+ * Move: {name}-move (applied when an item shifts position)
385
+ *
386
+ * @example
387
+ * const items = signal([{ id: 1 }, { id: 2 }])
388
+ *
389
+ * h(TransitionGroup, {
390
+ * tag: "ul",
391
+ * name: "list",
392
+ * items,
393
+ * keyFn: (item) => item.id,
394
+ * render: (item) => h("li", { class: "item" }, item.id),
395
+ * })
396
+ *
397
+ * // CSS:
398
+ * // .list-enter-from, .list-leave-to { opacity: 0; transform: translateY(-10px); }
399
+ * // .list-enter-active, .list-leave-active { transition: all 300ms ease; }
400
+ * // .list-move { transition: transform 300ms ease; }
401
+ */
402
+ declare function TransitionGroup<T = unknown>(props: TransitionGroupProps<T>): VNodeChild;
1945
403
  //#endregion
1946
- //#region src/index.ts
1947
-
404
+ //#region src/index.d.ts
1948
405
  /**
1949
- * Mount a VNode tree into a container element.
1950
- * Clears the container first, then mounts the given child.
1951
- * Returns an `unmount` function that removes everything and disposes effects.
1952
- *
1953
- * @example
1954
- * const unmount = mount(h("div", null, "Hello Pyreon"), document.getElementById("app")!)
1955
- */
1956
- function mount(root, container) {
1957
- if (__DEV__ && container == null) throw new Error("[pyreon] mount() called with a null/undefined container. Make sure the element exists in the DOM, e.g. document.getElementById(\"app\")");
1958
- installDevTools();
1959
- setupDelegation(container);
1960
- container.innerHTML = "";
1961
- return mountChild(root, container, null);
1962
- }
406
+ * Mount a VNode tree into a container element.
407
+ * Clears the container first, then mounts the given child.
408
+ * Returns an `unmount` function that removes everything and disposes effects.
409
+ *
410
+ * @example
411
+ * const unmount = mount(h("div", null, "Hello Pyreon"), document.getElementById("app")!)
412
+ */
413
+ declare function mount(root: VNodeChild, container: Element): () => void;
1963
414
  /** Alias for `mount` */
1964
-
415
+ declare const render: typeof mount;
1965
416
  //#endregion
1966
- export { DELEGATED_EVENTS, KeepAlive, Transition, TransitionGroup, _bindDirect, _bindText, _tpl, applyProp, applyProps, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, render, sanitizeHtml, setSanitizer, setupDelegation };
1967
- //# sourceMappingURL=index.d.ts.map
417
+ export { DELEGATED_EVENTS, type DevtoolsComponentEntry, type Directive, KeepAlive, type KeepAliveProps, type PyreonDevtools, type SanitizeFn, Transition, TransitionGroup, type TransitionGroupProps, type TransitionProps, _bindDirect, _bindText, _tpl, applyProp, applyProps, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, render, sanitizeHtml, setSanitizer, setupDelegation };
418
+ //# sourceMappingURL=index2.d.ts.map