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