@pyreon/runtime-dom 0.5.5 → 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,1945 +1,418 @@
1
- import { batch, effect, effectScope, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
2
- import { EMPTY_PROPS, ForSymbol, Fragment, PortalSymbol, createRef, dispatchToErrorBoundary, h, onMount, onUnmount, propagateError, reportError, runWithHooks } 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 warnDuplicateKeys = (seen, key) => {
563
- if (!__DEV__$4 || !seen) return;
564
- if (seen.has(key)) console.warn(`[Pyreon] Duplicate key "${String(key)}" in <For> list. Keys must be unique.`);
565
- seen.add(key);
566
- };
567
- /** Render item into container, update cache+cleanupCount. No anchor registration. */
568
- const renderInto = (item, key, pos, container, before) => {
569
- const result = renderItem(item);
570
- if (result.__isNative) {
571
- const native = result;
572
- container.insertBefore(native.el, before);
573
- cache.set(key, {
574
- anchor: native.el,
575
- cleanup: native.cleanup,
576
- pos
577
- });
578
- if (native.cleanup) cleanupCount++;
579
- return;
580
- }
581
- const priorLast = before ? before.previousSibling : container.lastChild;
582
- const cl = mountChild(result, container, before);
583
- const firstMounted = priorLast ? priorLast.nextSibling : container.firstChild;
584
- if (!firstMounted || firstMounted === before) {
585
- const ph = document.createComment("");
586
- container.insertBefore(ph, before);
587
- cache.set(key, {
588
- anchor: ph,
589
- cleanup: cl,
590
- pos
591
- });
592
- } else cache.set(key, {
593
- anchor: firstMounted,
594
- cleanup: cl,
595
- pos
596
- });
597
- cleanupCount++;
598
- };
599
- const handleFreshRender = (items, n, liveParent) => {
600
- const frag = document.createDocumentFragment();
601
- const keys = new Array(n);
602
- const _seenKeys = __DEV__$4 ? /* @__PURE__ */new Set() : null;
603
- for (let i = 0; i < n; i++) {
604
- const item = items[i];
605
- const key = getKey(item);
606
- warnDuplicateKeys(_seenKeys, key);
607
- keys[i] = key;
608
- renderInto(item, key, i, frag, null);
609
- }
610
- liveParent.insertBefore(frag, tailMarker);
611
- anchorsRegistered = false;
612
- currentKeys = keys;
613
- };
614
- const collectNewKeys = (items, n) => {
615
- const newKeys = new Array(n);
616
- const _seenUpdate = __DEV__$4 ? /* @__PURE__ */new Set() : null;
617
- for (let i = 0; i < n; i++) {
618
- newKeys[i] = getKey(items[i]);
619
- warnDuplicateKeys(_seenUpdate, newKeys[i]);
620
- }
621
- return newKeys;
622
- };
623
- const handleReplaceAll = (items, n, newKeys, liveParent) => {
624
- if (cleanupCount > 0) {
625
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
626
- }
627
- cache = /* @__PURE__ */new Map();
628
- cleanupCount = 0;
629
- const parentParent = liveParent.parentNode;
630
- const canSwap = parentParent && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker;
631
- const frag = document.createDocumentFragment();
632
- for (let i = 0; i < n; i++) renderInto(items[i], newKeys[i], i, frag, null);
633
- anchorsRegistered = false;
634
- if (canSwap) {
635
- const fresh = liveParent.cloneNode(false);
636
- fresh.appendChild(startMarker);
637
- fresh.appendChild(frag);
638
- fresh.appendChild(tailMarker);
639
- parentParent.replaceChild(fresh, liveParent);
640
- } else {
641
- clearBetween(startMarker, tailMarker);
642
- liveParent.insertBefore(frag, tailMarker);
643
- }
644
- currentKeys = newKeys;
645
- };
646
- const removeStaleForEntries = newKeySet => {
647
- for (const [key, entry] of cache) {
648
- if (newKeySet.has(key)) continue;
649
- if (entry.cleanup) {
650
- entry.cleanup();
651
- cleanupCount--;
652
- }
653
- entry.anchor.parentNode?.removeChild(entry.anchor);
654
- cache.delete(key);
655
- }
656
- };
657
- const mountNewForEntries = (items, n, newKeys, liveParent) => {
658
- for (let i = 0; i < n; i++) {
659
- const key = newKeys[i];
660
- if (cache.has(key)) continue;
661
- renderInto(items[i], key, i, liveParent, tailMarker);
662
- const entry = cache.get(key);
663
- if (entry) _forAnchors.add(entry.anchor);
664
- }
665
- };
666
- const handleFastClear = liveParent => {
667
- if (cache.size === 0) return;
668
- if (cleanupCount > 0) {
669
- for (const entry of cache.values()) if (entry.cleanup) entry.cleanup();
670
- }
671
- const pp = liveParent.parentNode;
672
- if (pp && liveParent.firstChild === startMarker && liveParent.lastChild === tailMarker) {
673
- const fresh = liveParent.cloneNode(false);
674
- fresh.appendChild(startMarker);
675
- fresh.appendChild(tailMarker);
676
- pp.replaceChild(fresh, liveParent);
677
- } else clearBetween(startMarker, tailMarker);
678
- cache = /* @__PURE__ */new Map();
679
- cleanupCount = 0;
680
- currentKeys = [];
681
- };
682
- const hasAnyKeptKey = (n, newKeys) => {
683
- for (let i = 0; i < n; i++) if (cache.has(newKeys[i])) return true;
684
- return false;
685
- };
686
- const handleIncrementalUpdate = (items, n, newKeys, liveParent) => {
687
- removeStaleForEntries(new Set(newKeys));
688
- mountNewForEntries(items, n, newKeys, liveParent);
689
- if (!anchorsRegistered) {
690
- for (const entry of cache.values()) _forAnchors.add(entry.anchor);
691
- anchorsRegistered = true;
692
- }
693
- if (trySmallKReorder(n, newKeys, currentKeys, cache, liveParent, tailMarker)) {
694
- currentKeys = newKeys;
695
- return;
696
- }
697
- lis = forLisReorder(lis, n, newKeys, cache, liveParent, tailMarker);
698
- currentKeys = newKeys;
699
- };
700
- const e = effect(() => {
701
- const liveParent = startMarker.parentNode;
702
- if (!liveParent) return;
703
- const items = source();
704
- const n = items.length;
705
- if (n === 0) {
706
- handleFastClear(liveParent);
707
- return;
708
- }
709
- if (currentKeys.length === 0) {
710
- handleFreshRender(items, n, liveParent);
711
- return;
712
- }
713
- const newKeys = collectNewKeys(items, n);
714
- if (!hasAnyKeptKey(n, newKeys)) {
715
- handleReplaceAll(items, n, newKeys, liveParent);
716
- return;
717
- }
718
- handleIncrementalUpdate(items, n, newKeys, liveParent);
719
- });
720
- return () => {
721
- e.dispose();
722
- for (const entry of cache.values()) {
723
- if (cleanupCount > 0 && entry.cleanup) entry.cleanup();
724
- entry.anchor.parentNode?.removeChild(entry.anchor);
725
- }
726
- cache = /* @__PURE__ */new Map();
727
- cleanupCount = 0;
728
- startMarker.parentNode?.removeChild(startMarker);
729
- tailMarker.parentNode?.removeChild(tailMarker);
730
- };
731
- }
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
732
87
  /**
733
- * Small-k reorder: directly place the k displaced entries without LIS.
734
- */
735
- function smallKPlace(parent, diffs, newKeys, cache, tailMarker) {
736
- const diffSet = new Set(diffs);
737
- let cursor = tailMarker;
738
- let prevDiffIdx = newKeys.length;
739
- for (let d = diffs.length - 1; d >= 0; d--) {
740
- const i = diffs[d];
741
- let nextNonDiff = -1;
742
- for (let j = i + 1; j < prevDiffIdx; j++) if (!diffSet.has(j)) {
743
- nextNonDiff = j;
744
- break;
745
- }
746
- if (nextNonDiff >= 0) {
747
- const nc = cache.get(newKeys[nextNonDiff])?.anchor;
748
- if (nc) cursor = nc;
749
- }
750
- const entry = cache.get(newKeys[i]);
751
- if (!entry) {
752
- prevDiffIdx = i;
753
- continue;
754
- }
755
- moveEntryBefore(parent, entry.anchor, cursor);
756
- cursor = entry.anchor;
757
- prevDiffIdx = i;
758
- }
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;
759
110
  }
760
111
  /**
761
- * Move startNode and all siblings belonging to this entry to just before `before`.
762
- * Stops at the next entry anchor (identified via WeakSet) or the tail marker.
763
- *
764
- * Fast path: if the next sibling is already a boundary (another entry or tail),
765
- * this entry is a single node skip the toMove array entirely.
766
- */
767
- function moveEntryBefore(parent, startNode, before) {
768
- const next = startNode.nextSibling;
769
- if (!next || next === before || next.parentNode === parent && (_forAnchors.has(next) || _keyedAnchors.has(next))) {
770
- parent.insertBefore(startNode, before);
771
- return;
772
- }
773
- const toMove = [startNode];
774
- let cur = next;
775
- while (cur && cur !== before) {
776
- const nextNode = cur.nextSibling;
777
- toMove.push(cur);
778
- cur = nextNode;
779
- if (cur && cur.parentNode === parent && (cur === before || _forAnchors.has(cur) || _keyedAnchors.has(cur))) break;
780
- }
781
- for (const node of toMove) parent.insertBefore(node, before);
782
- }
783
-
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;
784
136
  //#endregion
785
- //#region src/props.ts
786
-
137
+ //#region src/mount.d.ts
138
+ type Cleanup$1 = () => void;
787
139
  /**
788
- * Set a custom HTML sanitizer used by `innerHTML` and `sanitizeHtml()`.
789
- * Overrides both the Sanitizer API and the built-in fallback.
790
- *
791
- * @example
792
- * // With DOMPurify:
793
- * import DOMPurify from "dompurify"
794
- * setSanitizer((html) => DOMPurify.sanitize(html))
795
- *
796
- * // With sanitize-html:
797
- * import sanitize from "sanitize-html"
798
- * setSanitizer((html) => sanitize(html))
799
- *
800
- * // Reset to built-in:
801
- * setSanitizer(null)
802
- */
803
- function setSanitizer(fn) {
804
- _customSanitizer = fn;
805
- }
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;
806
150
  /**
807
- * Fallback tag-stripping sanitizer for environments without the Sanitizer API.
808
- * Removes all tags not in SAFE_TAGS, strips event handler attributes,
809
- * and blocks javascript:/data: URLs in href/src/action attributes.
810
- */
811
- function fallbackSanitize(html) {
812
- const doc = new DOMParser().parseFromString(html, "text/html");
813
- sanitizeNode(doc.body);
814
- return doc.body.innerHTML;
815
- }
816
- /** Strip unsafe attributes from a single element. */
817
- function stripUnsafeAttrs(el) {
818
- const attrs = Array.from(el.attributes);
819
- 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);
820
- }
821
- function sanitizeNode(node) {
822
- const children = Array.from(node.childNodes);
823
- for (const child of children) {
824
- if (child.nodeType !== 1) continue;
825
- const el = child;
826
- const tag = el.tagName.toLowerCase();
827
- if (!SAFE_TAGS.has(tag)) {
828
- const text = document.createTextNode(el.textContent);
829
- node.replaceChild(text, el);
830
- continue;
831
- }
832
- stripUnsafeAttrs(el);
833
- sanitizeNode(el);
834
- }
835
- }
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;
836
169
  /**
837
- * Sanitize an HTML string using the browser Sanitizer API (Chrome 105+).
838
- * Falls back to a tag-allowlist sanitizer that strips unsafe elements and attributes.
839
- */
840
- function sanitizeHtml(html) {
841
- if (_customSanitizer) return _customSanitizer(html);
842
- return fallbackSanitize(html);
843
- }
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;
844
186
  /**
845
- * Apply all props to a DOM element.
846
- * Returns a single chained cleanup (or null if no props need teardown).
847
- * Uses for-in instead of Object.keys() to avoid allocating a keys array.
848
- */
849
- function applyProps(el, props) {
850
- let first = null;
851
- let cleanups = null;
852
- for (const key in props) {
853
- if (key === "key" || key === "ref") continue;
854
- const c = applyProp(el, key, props[key]);
855
- if (c) if (!first) first = c;else if (!cleanups) cleanups = [first, c];else cleanups.push(c);
856
- }
857
- if (cleanups) return () => {
858
- for (const c of cleanups) c();
859
- };
860
- return first;
861
- }
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;
862
191
  /**
863
- * Apply a single prop.
864
- *
865
- * - `onXxx` addEventListener
866
- * - `() => value` (non-event function) → reactive via effect
867
- * - anything else static attribute / DOM property
868
- */
869
- function applyProp(el, key, value) {
870
- if (EVENT_RE.test(key)) {
871
- const eventName = key[2]?.toLowerCase() + key.slice(3);
872
- const handler = value;
873
- if (DELEGATED_EVENTS.has(eventName)) {
874
- const prop = delegatedPropName(eventName);
875
- el[prop] = e => batch(() => handler(e));
876
- return () => {
877
- el[prop] = void 0;
878
- };
879
- }
880
- const batched = e => batch(() => handler(e));
881
- el.addEventListener(eventName, batched);
882
- return () => el.removeEventListener(eventName, batched);
883
- }
884
- if (key === "innerHTML") {
885
- if (typeof el.setHTML === "function") el.setHTML(value);else el.innerHTML = sanitizeHtml(value);
886
- return null;
887
- }
888
- if (key === "dangerouslySetInnerHTML") {
889
- if (__DEV__$3) console.warn("[Pyreon] dangerouslySetInnerHTML bypasses sanitization. Ensure the HTML is trusted.");
890
- el.innerHTML = value.__html;
891
- return null;
892
- }
893
- if (key === "n-show") return renderEffect(() => {
894
- const visible = value();
895
- el.style.display = visible ? "" : "none";
896
- });
897
- if (key.startsWith("n-")) {
898
- const directive = value;
899
- const cleanups = [];
900
- directive(el, fn => cleanups.push(fn));
901
- return cleanups.length > 0 ? () => {
902
- for (const fn of cleanups) fn();
903
- } : null;
904
- }
905
- if (typeof value === "function") return renderEffect(() => setStaticProp(el, key, value()));
906
- setStaticProp(el, key, value);
907
- return null;
908
- }
909
- /** Apply a style prop (string or object). */
910
- function applyStyleProp(el, value) {
911
- if (typeof value === "string") el.style.cssText = value;else if (value != null && typeof value === "object") Object.assign(el.style, value);
912
- }
913
- function setStaticProp(el, key, value) {
914
- if (URL_ATTRS.has(key) && typeof value === "string" && UNSAFE_URL_RE.test(value)) {
915
- if (__DEV__$3) console.warn(`[Pyreon] Blocked unsafe URL in "${key}" attribute: ${value}`);
916
- return;
917
- }
918
- if (key === "class" || key === "className") {
919
- el.setAttribute("class", value == null ? "" : String(value));
920
- return;
921
- }
922
- if (key === "style") {
923
- applyStyleProp(el, value);
924
- return;
925
- }
926
- if (value == null) {
927
- el.removeAttribute(key);
928
- return;
929
- }
930
- if (typeof value === "boolean") {
931
- if (value) el.setAttribute(key, "");else el.removeAttribute(key);
932
- return;
933
- }
934
- if (key in el) {
935
- el[key] = value;
936
- return;
937
- }
938
- el.setAttribute(key, String(value));
939
- }
940
-
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;
941
198
  //#endregion
942
- //#region src/mount.ts
943
-
199
+ //#region src/template.d.ts
944
200
  /**
945
- * Mount a single child into `parent`, inserting before `anchor` (null = append).
946
- * Returns a cleanup that removes the node(s) and disposes all reactive effects.
947
- *
948
- * This function is the hot path all child types are handled inline to avoid
949
- * function call overhead in tight render loops (1000+ calls per list render).
950
- */
951
- function mountChild(child, parent, anchor = null) {
952
- if (typeof child === "function") {
953
- const sample = runUntracked(() => child());
954
- if (isKeyedArray(sample)) {
955
- const prevDepth = _elementDepth;
956
- _elementDepth = 0;
957
- const cleanup = mountKeyedList(child, parent, anchor, (v, p, a) => mountChild(v, p, a));
958
- _elementDepth = prevDepth;
959
- return cleanup;
960
- }
961
- if (typeof sample === "string" || typeof sample === "number" || typeof sample === "boolean") {
962
- const text = document.createTextNode(sample == null || sample === false ? "" : String(sample));
963
- parent.insertBefore(text, anchor);
964
- const dispose = renderEffect(() => {
965
- const v = child();
966
- text.data = v == null || v === false ? "" : String(v);
967
- });
968
- if (_elementDepth > 0) return dispose;
969
- return () => {
970
- dispose();
971
- const p = text.parentNode;
972
- if (p && p.isConnected !== false) p.removeChild(text);
973
- };
974
- }
975
- const prevDepth = _elementDepth;
976
- _elementDepth = 0;
977
- const cleanup = mountReactive(child, parent, anchor, mountChild);
978
- _elementDepth = prevDepth;
979
- return cleanup;
980
- }
981
- if (Array.isArray(child)) return mountChildren(child, parent, anchor);
982
- if (child == null || child === false) return noop$1;
983
- if (typeof child !== "object") {
984
- parent.insertBefore(document.createTextNode(String(child)), anchor);
985
- return noop$1;
986
- }
987
- if (child.__isNative) {
988
- const native = child;
989
- parent.insertBefore(native.el, anchor);
990
- if (!native.cleanup) {
991
- if (_elementDepth > 0) return noop$1;
992
- return () => {
993
- const p = native.el.parentNode;
994
- if (p && p.isConnected !== false) p.removeChild(native.el);
995
- };
996
- }
997
- if (_elementDepth > 0) return native.cleanup;
998
- return () => {
999
- native.cleanup?.();
1000
- const p = native.el.parentNode;
1001
- if (p && p.isConnected !== false) p.removeChild(native.el);
1002
- };
1003
- }
1004
- const vnode = child;
1005
- if (vnode.type === Fragment) return mountChildren(vnode.children, parent, anchor);
1006
- if (vnode.type === ForSymbol) {
1007
- const {
1008
- each,
1009
- by,
1010
- children
1011
- } = vnode.props;
1012
- const prevDepth = _elementDepth;
1013
- _elementDepth = 0;
1014
- const cleanup = mountFor(each, by, children, parent, anchor, mountChild);
1015
- _elementDepth = prevDepth;
1016
- return cleanup;
1017
- }
1018
- if (vnode.type === PortalSymbol) {
1019
- const {
1020
- target,
1021
- children
1022
- } = vnode.props;
1023
- if (__DEV__$2 && !target) return noop$1;
1024
- return mountChild(children, target, null);
1025
- }
1026
- if (typeof vnode.type === "function") return mountComponent(vnode, parent, anchor);
1027
- return mountElement(vnode, parent, anchor);
1028
- }
1029
- function mountElement(vnode, parent, anchor) {
1030
- const el = document.createElement(vnode.type);
1031
- const props = vnode.props;
1032
- const propCleanup = props !== EMPTY_PROPS ? applyProps(el, props) : null;
1033
- _elementDepth++;
1034
- const childCleanup = mountChildren(vnode.children, el, null);
1035
- _elementDepth--;
1036
- parent.insertBefore(el, anchor);
1037
- const ref = props.ref;
1038
- if (ref) if (typeof ref === "function") ref(el);else ref.current = el;
1039
- if (!propCleanup && childCleanup === noop$1 && !ref) {
1040
- if (_elementDepth > 0) return noop$1;
1041
- return () => {
1042
- const p = el.parentNode;
1043
- if (p && p.isConnected !== false) p.removeChild(el);
1044
- };
1045
- }
1046
- if (_elementDepth > 0) {
1047
- if (!ref && !propCleanup) return childCleanup;
1048
- if (!ref && propCleanup) return () => {
1049
- propCleanup();
1050
- childCleanup();
1051
- };
1052
- const refToClean = ref;
1053
- return () => {
1054
- if (refToClean && typeof refToClean === "object") refToClean.current = null;
1055
- if (propCleanup) propCleanup();
1056
- childCleanup();
1057
- };
1058
- }
1059
- return () => {
1060
- if (ref && typeof ref === "object") ref.current = null;
1061
- if (propCleanup) propCleanup();
1062
- childCleanup();
1063
- const p = el.parentNode;
1064
- if (p && p.isConnected !== false) p.removeChild(el);
1065
- };
1066
- }
1067
- function mountComponent(vnode, parent, anchor) {
1068
- const scope = effectScope();
1069
- setCurrentScope(scope);
1070
- let hooks;
1071
- let output;
1072
- const componentName = vnode.type.name || "Anonymous";
1073
- const compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`;
1074
- const parentId = _mountingStack[_mountingStack.length - 1] ?? null;
1075
- _mountingStack.push(compId);
1076
- const mergedProps = vnode.children.length > 0 && vnode.props.children === void 0 ? {
1077
- ...vnode.props,
1078
- children: vnode.children.length === 1 ? vnode.children[0] : vnode.children
1079
- } : vnode.props;
1080
- try {
1081
- const result = runWithHooks(vnode.type, mergedProps);
1082
- hooks = result.hooks;
1083
- output = result.vnode;
1084
- } catch (err) {
1085
- _mountingStack.pop();
1086
- setCurrentScope(null);
1087
- scope.stop();
1088
- reportError({
1089
- component: componentName,
1090
- phase: "setup",
1091
- error: err,
1092
- timestamp: Date.now(),
1093
- props: vnode.props
1094
- });
1095
- dispatchToErrorBoundary(err);
1096
- return noop$1;
1097
- } finally {
1098
- setCurrentScope(null);
1099
- }
1100
- if (__DEV__$2 && output != null && typeof output === "object" && !("type" in output)) console.warn(`[Pyreon] Component <${componentName}> returned an invalid value. Components must return a VNode, string, null, or function.`);
1101
- for (const fn of hooks.update) scope.addUpdateHook(fn);
1102
- let subtreeCleanup = noop$1;
1103
- try {
1104
- subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop$1;
1105
- } catch (err) {
1106
- _mountingStack.pop();
1107
- scope.stop();
1108
- if (!(propagateError(err, hooks) || dispatchToErrorBoundary(err))) reportError({
1109
- component: componentName,
1110
- phase: "render",
1111
- error: err,
1112
- timestamp: Date.now(),
1113
- props: vnode.props
1114
- });
1115
- return noop$1;
1116
- }
1117
- _mountingStack.pop();
1118
- registerComponent(compId, componentName, parent instanceof Element ? parent.firstElementChild : null, parentId);
1119
- const mountCleanups = [];
1120
- for (const fn of hooks.mount) try {
1121
- let cleanup;
1122
- scope.runInScope(() => {
1123
- cleanup = fn();
1124
- });
1125
- if (cleanup) mountCleanups.push(cleanup);
1126
- } catch (err) {
1127
- console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err);
1128
- reportError({
1129
- component: componentName,
1130
- phase: "mount",
1131
- error: err,
1132
- timestamp: Date.now()
1133
- });
1134
- }
1135
- return () => {
1136
- unregisterComponent(compId);
1137
- scope.stop();
1138
- subtreeCleanup();
1139
- for (const fn of hooks.unmount) try {
1140
- fn();
1141
- } catch (err) {
1142
- console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err);
1143
- reportError({
1144
- component: componentName,
1145
- phase: "unmount",
1146
- error: err,
1147
- timestamp: Date.now()
1148
- });
1149
- }
1150
- for (const fn of mountCleanups) fn();
1151
- };
1152
- }
1153
- function mountChildren(children, parent, anchor) {
1154
- if (children.length === 0) return noop$1;
1155
- if (children.length === 1) {
1156
- const c = children[0];
1157
- if (c !== void 0) {
1158
- if (anchor === null && (typeof c === "string" || typeof c === "number")) {
1159
- parent.textContent = String(c);
1160
- return noop$1;
1161
- }
1162
- return mountChild(c, parent, anchor);
1163
- }
1164
- }
1165
- if (children.length === 2) {
1166
- const c0 = children[0];
1167
- const c1 = children[1];
1168
- if (c0 !== void 0 && c1 !== void 0) {
1169
- const d0 = mountChild(c0, parent, anchor);
1170
- const d1 = mountChild(c1, parent, anchor);
1171
- if (d0 === noop$1 && d1 === noop$1) return noop$1;
1172
- if (d0 === noop$1) return d1;
1173
- if (d1 === noop$1) return d0;
1174
- return () => {
1175
- d0();
1176
- d1();
1177
- };
1178
- }
1179
- }
1180
- const cleanups = children.map(c => mountChild(c, parent, anchor));
1181
- return () => {
1182
- for (const fn of cleanups) fn();
1183
- };
1184
- }
1185
- /** Returns true if value is a non-empty array of VNodes that all carry keys. */
1186
- function isKeyedArray(value) {
1187
- if (!Array.isArray(value) || value.length === 0) return false;
1188
- return value.every(v => v !== null && typeof v === "object" && !Array.isArray(v) && v.key !== null && v.key !== void 0);
1189
- }
1190
-
1191
- //#endregion
1192
- //#region src/hydrate.ts
1193
-
1194
- /** Skip comment and whitespace-only text nodes, return first "real" node */
1195
- function firstReal(initialNode) {
1196
- let node = initialNode;
1197
- while (node) {
1198
- if (node.nodeType === Node.COMMENT_NODE) {
1199
- node = node.nextSibling;
1200
- continue;
1201
- }
1202
- if (node.nodeType === Node.TEXT_NODE && isWhitespaceOnly(node.data)) {
1203
- node = node.nextSibling;
1204
- continue;
1205
- }
1206
- return node;
1207
- }
1208
- return null;
1209
- }
1210
- /** Check if a string is whitespace-only without allocating a trimmed copy. */
1211
- function isWhitespaceOnly(s) {
1212
- for (let i = 0; i < s.length; i++) {
1213
- const c = s.charCodeAt(i);
1214
- if (c !== 32 && c !== 9 && c !== 10 && c !== 13 && c !== 12) return false;
1215
- }
1216
- return true;
1217
- }
1218
- /** Advance past a node, skipping whitespace-only text and comments */
1219
- function nextReal(node) {
1220
- return firstReal(node.nextSibling);
1221
- }
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;
1222
226
  /**
1223
- * Hydrate a single VNodeChild against the DOM subtree starting at `domNode`.
1224
- * Returns [cleanup, nextDomSibling].
1225
- */
1226
- /** Insert a comment marker before domNode (or append if domNode is null). */
1227
- function insertMarker(parent, domNode, text) {
1228
- const marker = document.createComment(text);
1229
- if (domNode) parent.insertBefore(marker, domNode);else parent.appendChild(marker);
1230
- return marker;
1231
- }
1232
- /** Hydrate a reactive accessor (function child). */
1233
- function hydrateReactiveChild(child, domNode, parent, anchor, path) {
1234
- const initial = runUntracked(child);
1235
- if (initial == null || initial === false) return [mountReactive(child, parent, insertMarker(parent, domNode, "pyreon"), mountChild), domNode];
1236
- if (typeof initial === "string" || typeof initial === "number" || typeof initial === "boolean") return hydrateReactiveText(child, domNode, parent, anchor, path);
1237
- return [mountReactive(child, parent, insertMarker(parent, domNode, "pyreon"), mountChild), domNode ? nextReal(domNode) : null];
1238
- }
1239
- /** Hydrate a reactive text binding against an existing text node. */
1240
- function hydrateReactiveText(child, domNode, parent, anchor, path) {
1241
- if (domNode?.nodeType === Node.TEXT_NODE) {
1242
- const textNode = domNode;
1243
- return [renderEffect(() => {
1244
- const v = child();
1245
- textNode.data = v == null ? "" : String(v);
1246
- }), nextReal(domNode)];
1247
- }
1248
- warnHydrationMismatch("text", "TextNode", domNode?.nodeType ?? "null", `${path} > reactive`);
1249
- return [mountChild(child, parent, anchor), domNode];
1250
- }
1251
- /** Hydrate a VNode (fragment, For, Portal, component, element). */
1252
- function hydrateVNode(vnode, domNode, parent, anchor, path) {
1253
- if (vnode.type === Fragment) return hydrateChildren(vnode.children, domNode, parent, anchor, path);
1254
- if (vnode.type === ForSymbol) return [mountChild(vnode, parent, insertMarker(parent, domNode, "pyreon-for")), null];
1255
- if (vnode.type === PortalSymbol) return [mountChild(vnode, parent, anchor), domNode];
1256
- if (typeof vnode.type === "function") return hydrateComponent(vnode, domNode, parent, anchor, path);
1257
- if (typeof vnode.type === "string") return hydrateElement(vnode, domNode, parent, anchor, path);
1258
- return [noop, domNode];
1259
- }
1260
- function hydrateChild(child, domNode, parent, anchor, path = "root") {
1261
- if (Array.isArray(child)) {
1262
- const cleanups = [];
1263
- let cursor = domNode;
1264
- for (const c of child) {
1265
- const [cleanup, next] = hydrateChild(c, cursor, parent, anchor, path);
1266
- cleanups.push(cleanup);
1267
- cursor = next;
1268
- }
1269
- return [() => {
1270
- for (const c of cleanups) c();
1271
- }, cursor];
1272
- }
1273
- if (child == null || child === false) return [noop, domNode];
1274
- if (typeof child === "function") return hydrateReactiveChild(child, domNode, parent, anchor, path);
1275
- if (typeof child === "string" || typeof child === "number") {
1276
- if (domNode?.nodeType === Node.TEXT_NODE) return [() => domNode.remove(), nextReal(domNode)];
1277
- warnHydrationMismatch("text", "TextNode", domNode?.nodeType ?? "null", `${path} > text`);
1278
- return [mountChild(child, parent, anchor), domNode];
1279
- }
1280
- return hydrateVNode(child, domNode, parent, anchor, path);
1281
- }
1282
- function hydrateElement(vnode, domNode, parent, anchor, path = "root") {
1283
- const elPath = `${path} > ${vnode.type}`;
1284
- if (domNode?.nodeType === Node.ELEMENT_NODE && domNode.tagName.toLowerCase() === vnode.type) {
1285
- const el = domNode;
1286
- const cleanups = [];
1287
- const propCleanup = applyProps(el, vnode.props);
1288
- if (propCleanup) cleanups.push(propCleanup);
1289
- const firstChild = firstReal(el.firstChild);
1290
- const [childCleanup] = hydrateChildren(vnode.children, firstChild, el, null, elPath);
1291
- cleanups.push(childCleanup);
1292
- const ref = vnode.props.ref;
1293
- if (ref) if (typeof ref === "function") ref(el);else ref.current = el;
1294
- const cleanup = () => {
1295
- if (ref && typeof ref === "object") ref.current = null;
1296
- for (const c of cleanups) c();
1297
- el.remove();
1298
- };
1299
- return [cleanup, nextReal(domNode)];
1300
- }
1301
- const actual = domNode?.nodeType === Node.ELEMENT_NODE ? domNode.tagName.toLowerCase() : domNode?.nodeType ?? "null";
1302
- warnHydrationMismatch("tag", vnode.type, actual, elPath);
1303
- return [mountChild(vnode, parent, anchor), domNode];
1304
- }
1305
- function hydrateChildren(children, domNode, parent, anchor, path = "root") {
1306
- if (children.length === 0) return [noop, domNode];
1307
- if (children.length === 1) return hydrateChild(children[0], domNode, parent, anchor, path);
1308
- const cleanups = [];
1309
- let cursor = domNode;
1310
- for (const child of children) {
1311
- const [cleanup, next] = hydrateChild(child, cursor, parent, anchor, path);
1312
- cleanups.push(cleanup);
1313
- cursor = next;
1314
- }
1315
- return [() => {
1316
- for (const c of cleanups) c();
1317
- }, cursor];
1318
- }
1319
- function hydrateComponent(vnode, domNode, parent, anchor, path = "root") {
1320
- const scope = effectScope();
1321
- setCurrentScope(scope);
1322
- let subtreeCleanup = noop;
1323
- const mountCleanups = [];
1324
- let nextDom = domNode;
1325
- const componentName = vnode.type.name || "Anonymous";
1326
- const mergedProps = vnode.children.length > 0 && vnode.props.children === void 0 ? {
1327
- ...vnode.props,
1328
- children: vnode.children.length === 1 ? vnode.children[0] : vnode.children
1329
- } : vnode.props;
1330
- let result;
1331
- try {
1332
- result = runWithHooks(vnode.type, mergedProps);
1333
- } catch (err) {
1334
- setCurrentScope(null);
1335
- scope.stop();
1336
- console.error(`[Pyreon] Error hydrating component <${componentName}>:`, err);
1337
- reportError({
1338
- component: componentName,
1339
- phase: "setup",
1340
- error: err,
1341
- timestamp: Date.now(),
1342
- props: vnode.props
1343
- });
1344
- dispatchToErrorBoundary(err);
1345
- return [noop, domNode];
1346
- }
1347
- setCurrentScope(null);
1348
- const {
1349
- vnode: output,
1350
- hooks
1351
- } = result;
1352
- for (const fn of hooks.update) scope.addUpdateHook(fn);
1353
- if (output != null) {
1354
- const [childCleanup, next] = hydrateChild(output, domNode, parent, anchor, path);
1355
- subtreeCleanup = childCleanup;
1356
- nextDom = next;
1357
- }
1358
- for (const fn of hooks.mount) try {
1359
- let c;
1360
- scope.runInScope(() => {
1361
- c = fn();
1362
- });
1363
- if (c) mountCleanups.push(c);
1364
- } catch (err) {
1365
- reportError({
1366
- component: componentName,
1367
- phase: "mount",
1368
- error: err,
1369
- timestamp: Date.now()
1370
- });
1371
- }
1372
- const cleanup = () => {
1373
- scope.stop();
1374
- subtreeCleanup();
1375
- for (const fn of hooks.unmount) fn();
1376
- for (const fn of mountCleanups) fn();
1377
- };
1378
- return [cleanup, nextDom];
1379
- }
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;
1380
246
  /**
1381
- * Hydrate a server-rendered container with a Pyreon VNode tree.
1382
- *
1383
- * Reuses existing DOM elements for static structure, attaches event listeners
1384
- * and reactive effects without re-rendering. Falls back to fresh mount for
1385
- * dynamic content (reactive conditionals, For lists).
1386
- *
1387
- * @example
1388
- * // Server:
1389
- * const html = await renderToString(h(App, null))
1390
- *
1391
- * // Client:
1392
- * const unmount = hydrateRoot(document.getElementById("app")!, h(App, null))
1393
- */
1394
- function hydrateRoot(container, vnode) {
1395
- setupDelegation(container);
1396
- const [cleanup] = hydrateChild(vnode, firstReal(container.firstChild), container, null);
1397
- return cleanup;
1398
- }
1399
-
1400
- //#endregion
1401
- //#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;
1402
266
  /**
1403
- * KeepAlive mounts its children once and keeps them alive even when hidden.
1404
- *
1405
- * Unlike conditional rendering (which destroys and recreates component state),
1406
- * KeepAlive CSS-hides the children while preserving all reactive state,
1407
- * scroll position, form values, and in-flight async operations.
1408
- *
1409
- * Children are mounted imperatively on first activation and are never unmounted
1410
- * while the KeepAlive itself is mounted.
1411
- *
1412
- * Multi-slot pattern (one KeepAlive per route):
1413
- * @example
1414
- * h(Fragment, null, [
1415
- * h(KeepAlive, { active: () => route() === "/a" }, h(RouteA, null)),
1416
- * h(KeepAlive, { active: () => route() === "/b" }, h(RouteB, null)),
1417
- * ])
1418
- *
1419
- * With JSX:
1420
- * @example
1421
- * <>
1422
- * <KeepAlive active={() => route() === "/a"}><RouteA /></KeepAlive>
1423
- * <KeepAlive active={() => route() === "/b"}><RouteB /></KeepAlive>
1424
- * </>
1425
- */
1426
- function KeepAlive(props) {
1427
- const containerRef = createRef();
1428
- let childCleanup = null;
1429
- let childMounted = false;
1430
- onMount(() => {
1431
- const container = containerRef.current;
1432
- const e = effect(() => {
1433
- const isActive = props.active?.() ?? true;
1434
- if (!childMounted) {
1435
- childCleanup = mountChild(props.children ?? null, container, null);
1436
- childMounted = true;
1437
- }
1438
- container.style.display = isActive ? "" : "none";
1439
- });
1440
- return () => {
1441
- e.dispose();
1442
- childCleanup?.();
1443
- };
1444
- });
1445
- return h("div", {
1446
- ref: containerRef,
1447
- style: "display: contents"
1448
- });
1449
- }
1450
-
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;
1451
290
  //#endregion
1452
- //#region src/template.ts
1453
- /**
1454
- * Creates a row/item factory backed by HTML template cloning.
1455
- *
1456
- * - The HTML string is parsed exactly once via <template>.innerHTML.
1457
- * - Each call to the returned factory clones the root element via
1458
- * cloneNode(true) — ~5-10x faster than createElement + setAttribute.
1459
- * - `bind` receives the cloned element and the item; it should wire up
1460
- * reactive effects and return a cleanup function.
1461
- * - Returns a NativeItem directly (no VNode wrapper) saves 2 allocations
1462
- * per row vs the old VNode + props-object + children-array approach.
1463
- *
1464
- * @example
1465
- * const rowTemplate = createTemplate<Row>(
1466
- * "<tr><td></td><td></td></tr>",
1467
- * (el, row) => {
1468
- * const td1 = el.firstChild as HTMLElement
1469
- * const td2 = td1.nextSibling as HTMLElement
1470
- * td1.textContent = String(row.id)
1471
- * const text = td2.firstChild as Text
1472
- * text.data = row.label()
1473
- * const unsub = row.label.subscribe(() => { text.data = row.label() })
1474
- * return unsub
1475
- * }
1476
- * )
1477
- */
1478
- function createTemplate(html, bind) {
1479
- const tmpl = document.createElement("template");
1480
- tmpl.innerHTML = html;
1481
- const proto = tmpl.content.firstElementChild;
1482
- return item => {
1483
- const el = proto.cloneNode(true);
1484
- return {
1485
- __isNative: true,
1486
- el,
1487
- cleanup: bind(el, item)
1488
- };
1489
- };
1490
- }
1491
- /**
1492
- * Compiler-emitted direct text binding for single-signal text nodes.
1493
- *
1494
- * When the compiler detects `{signal()}` as the only reactive expression
1495
- * in a text binding, it emits `_bindText(signal, textNode)` instead of
1496
- * `_bind(() => { textNode.data = signal() })`.
1497
- *
1498
- * This bypasses the effect system entirely:
1499
- * - No deps array allocation
1500
- * - No withTracking / setDepsCollector overhead
1501
- * - No `run` closure
1502
- * - Signal.subscribe is used directly (O(1) subscribe + unsubscribe)
1503
- *
1504
- * @param source - A signal (anything with `._v` and `.direct`)
1505
- * @param node - The Text node to update
1506
- */
1507
- function _bindText(source, node) {
1508
- const update = () => {
1509
- const v = source._v;
1510
- node.data = v == null || v === false ? "" : String(v);
1511
- };
1512
- update();
1513
- return source.direct(update);
1514
- }
1515
- /**
1516
- * Compiler-emitted direct binding for single-signal reactive expressions.
1517
- *
1518
- * Like _bindText but for arbitrary DOM updates (attributes, className, style).
1519
- * When the compiler detects that a reactive expression depends on exactly one
1520
- * signal call, it emits `_bindDirect(signal, updater)` instead of
1521
- * `_bind(() => { updater() })`.
1522
- *
1523
- * Uses signal.direct() for zero-overhead registration:
1524
- * - Flat array instead of Set (no hashing)
1525
- * - Index-based disposal (no Set.delete)
1526
- * - No deps array, no withTracking, no run closure
1527
- *
1528
- * @param source - A signal (anything with `._v` and `.direct`)
1529
- * @param updater - Function that reads `source._v` and applies the DOM update
1530
- */
1531
- function _bindDirect(source, updater) {
1532
- updater(source._v);
1533
- return source.direct(() => updater(source._v));
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;
1534
321
  }
1535
322
  /**
1536
- * Compiler-emitted template instantiation.
1537
- *
1538
- * Parses `html` into a <template> element once (cached), then cloneNode(true)
1539
- * for each call. The `bind` function wires up dynamic attributes, text content,
1540
- * and event listeners on the cloned element tree. Returns a NativeItem that
1541
- * mountChild can insert directly no VNode allocation.
1542
- *
1543
- * This is the runtime half of the compiler's template optimisation. The compiler
1544
- * detects static JSX element trees and emits `_tpl(html, bindFn)` instead of
1545
- * nested `h()` calls. Benefits:
1546
- * - cloneNode(true) is ~5-10x faster than sequential createElement + setAttribute
1547
- * - Zero VNode / props-object / children-array allocations per instance
1548
- * - Static attributes are baked into the HTML string (no runtime prop application)
1549
- *
1550
- * @example
1551
- * // Compiler output for: <div class="box"><span>{text()}</span></div>
1552
- * _tpl('<div class="box"><span></span></div>', (__root) => {
1553
- * const __e0 = __root.children[0];
1554
- * const __d0 = _re(() => { __e0.textContent = text(); });
1555
- * return () => { __d0(); };
1556
- * })
1557
- */
1558
- function _tpl(html, bind) {
1559
- let tpl = _tplCache.get(html);
1560
- if (!tpl) {
1561
- tpl = document.createElement("template");
1562
- tpl.innerHTML = html;
1563
- _tplCache.set(html, tpl);
1564
- }
1565
- const el = tpl.content.firstElementChild?.cloneNode(true);
1566
- return {
1567
- __isNative: true,
1568
- el,
1569
- cleanup: bind(el)
1570
- };
1571
- }
1572
-
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;
1573
345
  //#endregion
1574
- //#region src/transition.ts
1575
-
1576
- /**
1577
- * Transition — adds CSS enter/leave animation classes to a single child element,
1578
- * controlled by the reactive `show` prop.
1579
- *
1580
- * Class lifecycle:
1581
- * Enter: {name}-enter-from (next frame) → {name}-enter-active + {name}-enter-to → cleanup
1582
- * Leave: {name}-leave-from (next frame) → {name}-leave-active + {name}-leave-to → unmount
1583
- *
1584
- * The child element stays in the DOM during the leave animation and is removed only
1585
- * after the CSS transition / animation completes.
1586
- *
1587
- * @example
1588
- * const visible = signal(false)
1589
- *
1590
- * h(Transition, { name: "fade", show: () => visible() },
1591
- * h("div", { class: "modal" }, "content")
1592
- * )
1593
- *
1594
- * // CSS:
1595
- * // .fade-enter-from, .fade-leave-to { opacity: 0; }
1596
- * // .fade-enter-active, .fade-leave-active { transition: opacity 300ms ease; }
1597
- */
1598
- function Transition(props) {
1599
- const n = props.name ?? "pyreon";
1600
- const cls = {
1601
- ef: props.enterFrom ?? `${n}-enter-from`,
1602
- ea: props.enterActive ?? `${n}-enter-active`,
1603
- et: props.enterTo ?? `${n}-enter-to`,
1604
- lf: props.leaveFrom ?? `${n}-leave-from`,
1605
- la: props.leaveActive ?? `${n}-leave-active`,
1606
- lt: props.leaveTo ?? `${n}-leave-to`
1607
- };
1608
- const ref = createRef();
1609
- const isMounted = signal(runUntracked(props.show));
1610
- let pendingLeaveCancel = null;
1611
- let initialized = false;
1612
- const applyEnter = el => {
1613
- pendingLeaveCancel?.();
1614
- pendingLeaveCancel = null;
1615
- props.onBeforeEnter?.(el);
1616
- el.classList.remove(cls.lf, cls.la, cls.lt);
1617
- el.classList.add(cls.ef, cls.ea);
1618
- requestAnimationFrame(() => {
1619
- el.classList.remove(cls.ef);
1620
- el.classList.add(cls.et);
1621
- const done = () => {
1622
- el.removeEventListener("transitionend", done);
1623
- el.removeEventListener("animationend", done);
1624
- el.classList.remove(cls.ea, cls.et);
1625
- props.onAfterEnter?.(el);
1626
- };
1627
- el.addEventListener("transitionend", done, {
1628
- once: true
1629
- });
1630
- el.addEventListener("animationend", done, {
1631
- once: true
1632
- });
1633
- });
1634
- };
1635
- const applyLeave = el => {
1636
- props.onBeforeLeave?.(el);
1637
- el.classList.remove(cls.ef, cls.ea, cls.et);
1638
- el.classList.add(cls.lf, cls.la);
1639
- requestAnimationFrame(() => {
1640
- el.classList.remove(cls.lf);
1641
- el.classList.add(cls.lt);
1642
- const done = () => {
1643
- el.removeEventListener("transitionend", done);
1644
- el.removeEventListener("animationend", done);
1645
- el.classList.remove(cls.la, cls.lt);
1646
- pendingLeaveCancel = null;
1647
- isMounted.set(false);
1648
- props.onAfterLeave?.(el);
1649
- };
1650
- pendingLeaveCancel = () => {
1651
- el.removeEventListener("transitionend", done);
1652
- el.removeEventListener("animationend", done);
1653
- el.classList.remove(cls.lf, cls.la, cls.lt);
1654
- };
1655
- el.addEventListener("transitionend", done, {
1656
- once: true
1657
- });
1658
- el.addEventListener("animationend", done, {
1659
- once: true
1660
- });
1661
- });
1662
- };
1663
- const handleVisibilityChange = visible => {
1664
- if (visible) {
1665
- if (!isMounted.peek()) isMounted.set(true);
1666
- queueMicrotask(() => applyEnter(ref.current));
1667
- return;
1668
- }
1669
- if (!isMounted.peek()) return;
1670
- const el = ref.current;
1671
- if (!el) {
1672
- isMounted.set(false);
1673
- return;
1674
- }
1675
- applyLeave(el);
1676
- };
1677
- effect(() => {
1678
- const visible = props.show();
1679
- if (!initialized) {
1680
- initialized = true;
1681
- if (visible && props.appear) queueMicrotask(() => applyEnter(ref.current));
1682
- return;
1683
- }
1684
- handleVisibilityChange(visible);
1685
- });
1686
- onUnmount(() => {
1687
- pendingLeaveCancel?.();
1688
- pendingLeaveCancel = null;
1689
- });
1690
- const rawChild = props.children;
1691
- const emptyFragment = h(Fragment, null);
1692
- return () => {
1693
- if (!isMounted()) return emptyFragment;
1694
- if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) return rawChild ?? null;
1695
- const vnode = rawChild;
1696
- if (typeof vnode.type !== "string") {
1697
- if (__DEV__$1) console.warn("[Pyreon] Transition child is a component. Wrap it in a DOM element for enter/leave animations to work.");
1698
- return vnode;
1699
- }
1700
- return {
1701
- ...vnode,
1702
- props: {
1703
- ...vnode.props,
1704
- ref
1705
- }
1706
- };
1707
- };
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;
1708
376
  }
1709
-
1710
- //#endregion
1711
- //#region src/transition-group.ts
1712
377
  /**
1713
- * TransitionGroup — animates a keyed reactive list with CSS enter/leave and
1714
- * FLIP move animations.
1715
- *
1716
- * Class lifecycle:
1717
- * Enter: {name}-enter-from → {name}-enter-active + {name}-enter-to → cleanup
1718
- * Leave: {name}-leave-from → {name}-leave-active + {name}-leave-to → item removed
1719
- * Move: {name}-move (applied when an item shifts position)
1720
- *
1721
- * @example
1722
- * const items = signal([{ id: 1 }, { id: 2 }])
1723
- *
1724
- * h(TransitionGroup, {
1725
- * tag: "ul",
1726
- * name: "list",
1727
- * items,
1728
- * keyFn: (item) => item.id,
1729
- * render: (item) => h("li", { class: "item" }, item.id),
1730
- * })
1731
- *
1732
- * // CSS:
1733
- * // .list-enter-from, .list-leave-to { opacity: 0; transform: translateY(-10px); }
1734
- * // .list-enter-active, .list-leave-active { transition: all 300ms ease; }
1735
- * // .list-move { transition: transform 300ms ease; }
1736
- */
1737
- function TransitionGroup(props) {
1738
- const tag = props.tag ?? "div";
1739
- const n = props.name ?? "pyreon";
1740
- const cls = {
1741
- ef: props.enterFrom ?? `${n}-enter-from`,
1742
- ea: props.enterActive ?? `${n}-enter-active`,
1743
- et: props.enterTo ?? `${n}-enter-to`,
1744
- lf: props.leaveFrom ?? `${n}-leave-from`,
1745
- la: props.leaveActive ?? `${n}-leave-active`,
1746
- lt: props.leaveTo ?? `${n}-leave-to`,
1747
- mv: props.moveClass ?? `${n}-move`
1748
- };
1749
- const containerRef = createRef();
1750
- const entries = /* @__PURE__ */new Map();
1751
- const ready = signal(false);
1752
- let firstRun = true;
1753
- const applyEnter = el => {
1754
- props.onBeforeEnter?.(el);
1755
- el.classList.remove(cls.lf, cls.la, cls.lt);
1756
- el.classList.add(cls.ef, cls.ea);
1757
- requestAnimationFrame(() => {
1758
- el.classList.remove(cls.ef);
1759
- el.classList.add(cls.et);
1760
- const done = () => {
1761
- el.removeEventListener("transitionend", done);
1762
- el.removeEventListener("animationend", done);
1763
- el.classList.remove(cls.ea, cls.et);
1764
- props.onAfterEnter?.(el);
1765
- };
1766
- el.addEventListener("transitionend", done, {
1767
- once: true
1768
- });
1769
- el.addEventListener("animationend", done, {
1770
- once: true
1771
- });
1772
- });
1773
- };
1774
- const applyLeave = (el, onDone) => {
1775
- props.onBeforeLeave?.(el);
1776
- el.classList.remove(cls.ef, cls.ea, cls.et);
1777
- el.classList.add(cls.lf, cls.la);
1778
- requestAnimationFrame(() => {
1779
- el.classList.remove(cls.lf);
1780
- el.classList.add(cls.lt);
1781
- const done = () => {
1782
- el.removeEventListener("transitionend", done);
1783
- el.removeEventListener("animationend", done);
1784
- el.classList.remove(cls.la, cls.lt);
1785
- props.onAfterLeave?.(el);
1786
- onDone();
1787
- };
1788
- el.addEventListener("transitionend", done, {
1789
- once: true
1790
- });
1791
- el.addEventListener("animationend", done, {
1792
- once: true
1793
- });
1794
- });
1795
- };
1796
- /** Start leave animation for removed items. */
1797
- const processLeaves = newKeys => {
1798
- for (const [key, entry] of entries) {
1799
- if (newKeys.has(key) || entry.leaving) continue;
1800
- entry.leaving = true;
1801
- const el = entry.ref.current;
1802
- if (el) applyLeave(el, () => {
1803
- entry.cleanup();
1804
- entries.delete(key);
1805
- });else {
1806
- entry.cleanup();
1807
- entries.delete(key);
1808
- }
1809
- }
1810
- };
1811
- /** Mount new items and return the list of newly created entries. */
1812
- const mountNewItems = (items, container) => {
1813
- const newEntries = [];
1814
- for (let i = 0; i < items.length; i++) {
1815
- const item = items[i];
1816
- const key = props.keyFn(item, i);
1817
- if (entries.has(key)) continue;
1818
- const itemRef = createRef();
1819
- const rawVNode = runUntracked(() => props.render(item, i));
1820
- const entry = {
1821
- key,
1822
- ref: itemRef,
1823
- cleanup: mountChild(typeof rawVNode.type === "string" ? {
1824
- ...rawVNode,
1825
- props: {
1826
- ...rawVNode.props,
1827
- ref: itemRef
1828
- }
1829
- } : rawVNode, container, null),
1830
- leaving: false
1831
- };
1832
- entries.set(key, entry);
1833
- newEntries.push(entry);
1834
- }
1835
- return newEntries;
1836
- };
1837
- const startMoveAnimation = el => {
1838
- requestAnimationFrame(() => {
1839
- el.classList.add(cls.mv);
1840
- el.style.transform = "";
1841
- el.style.transition = "";
1842
- const done = () => {
1843
- el.removeEventListener("transitionend", done);
1844
- el.removeEventListener("animationend", done);
1845
- el.classList.remove(cls.mv);
1846
- };
1847
- el.addEventListener("transitionend", done, {
1848
- once: true
1849
- });
1850
- el.addEventListener("animationend", done, {
1851
- once: true
1852
- });
1853
- });
1854
- };
1855
- const flipEntry = (entry, oldPos) => {
1856
- if (!entry.ref.current) return;
1857
- const newPos = entry.ref.current.getBoundingClientRect();
1858
- const dx = oldPos.left - newPos.left;
1859
- const dy = oldPos.top - newPos.top;
1860
- if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return;
1861
- const el = entry.ref.current;
1862
- el.style.transform = `translate(${dx}px, ${dy}px)`;
1863
- el.style.transition = "none";
1864
- startMoveAnimation(el);
1865
- };
1866
- /** Apply FLIP move animations for items that shifted position. */
1867
- const applyFlipMoves = oldPositions => {
1868
- requestAnimationFrame(() => {
1869
- for (const [key, entry] of entries) {
1870
- if (entry.leaving) continue;
1871
- const oldPos = oldPositions.get(key);
1872
- if (!oldPos) continue;
1873
- flipEntry(entry, oldPos);
1874
- }
1875
- });
1876
- };
1877
- const recordOldPositions = () => {
1878
- const oldPositions = /* @__PURE__ */new Map();
1879
- for (const [key, entry] of entries) if (!entry.leaving && entry.ref.current) oldPositions.set(key, entry.ref.current.getBoundingClientRect());
1880
- return oldPositions;
1881
- };
1882
- const reorderEntries = (items, container) => {
1883
- for (let i = 0; i < items.length; i++) {
1884
- const key = props.keyFn(items[i], i);
1885
- const entry = entries.get(key);
1886
- if (!entry || entry.leaving || !entry.ref.current) continue;
1887
- container.appendChild(entry.ref.current);
1888
- }
1889
- };
1890
- const animateNewEntries = newEntries => {
1891
- for (const entry of newEntries) queueMicrotask(() => {
1892
- if (entry.ref.current) applyEnter(entry.ref.current);
1893
- });
1894
- };
1895
- const e = effect(() => {
1896
- if (!ready()) return;
1897
- const container = containerRef.current;
1898
- if (!container) return;
1899
- const items = props.items();
1900
- const newKeys = new Set(items.map((item, i) => props.keyFn(item, i)));
1901
- const isFirst = firstRun;
1902
- firstRun = false;
1903
- const oldPositions = recordOldPositions();
1904
- processLeaves(newKeys);
1905
- const newEntries = mountNewItems(items, container);
1906
- reorderEntries(items, container);
1907
- if (!isFirst || props.appear) animateNewEntries(newEntries);
1908
- if (!isFirst && oldPositions.size > 0) applyFlipMoves(oldPositions);
1909
- });
1910
- onMount(() => {
1911
- ready.set(true);
1912
- });
1913
- onUnmount(() => {
1914
- e.dispose();
1915
- for (const entry of entries.values()) entry.cleanup();
1916
- entries.clear();
1917
- });
1918
- return h(tag, {
1919
- ref: containerRef
1920
- });
1921
- }
1922
-
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;
1923
403
  //#endregion
1924
- //#region src/index.ts
1925
-
404
+ //#region src/index.d.ts
1926
405
  /**
1927
- * Mount a VNode tree into a container element.
1928
- * Clears the container first, then mounts the given child.
1929
- * Returns an `unmount` function that removes everything and disposes effects.
1930
- *
1931
- * @example
1932
- * const unmount = mount(h("div", null, "Hello Pyreon"), document.getElementById("app")!)
1933
- */
1934
- function mount(root, container) {
1935
- 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\")");
1936
- installDevTools();
1937
- setupDelegation(container);
1938
- container.innerHTML = "";
1939
- return mountChild(root, container, null);
1940
- }
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;
1941
414
  /** Alias for `mount` */
1942
-
415
+ declare const render: typeof mount;
1943
416
  //#endregion
1944
- export { DELEGATED_EVENTS, KeepAlive, Transition, TransitionGroup, _bindDirect, _bindText, _tpl, applyProp, applyProps, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, render, sanitizeHtml, setSanitizer, setupDelegation };
1945
- //# 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