@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.
- package/lib/types/index.d.ts +384 -1933
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +3 -3
package/lib/types/index.d.ts
CHANGED
|
@@ -1,1967 +1,418 @@
|
|
|
1
|
-
import {
|
|
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/
|
|
55
|
-
|
|
56
|
-
function enableHydrationWarnings() {
|
|
57
|
-
_enabled = true;
|
|
58
|
-
}
|
|
59
|
-
function disableHydrationWarnings() {
|
|
60
|
-
_enabled = false;
|
|
61
|
-
}
|
|
33
|
+
//#region src/devtools.d.ts
|
|
62
34
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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/
|
|
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
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
*
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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/
|
|
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
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
|
|
812
|
-
function
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
*
|
|
839
|
-
*
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
*
|
|
865
|
-
*
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
*
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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/
|
|
958
|
-
|
|
199
|
+
//#region src/template.d.ts
|
|
959
200
|
/**
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
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/
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
*
|
|
1618
|
-
*
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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=
|
|
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
|