what-core 0.5.6 → 0.6.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/dist/render.js CHANGED
@@ -1,118 +1,1193 @@
1
- // What Framework - Fine-Grained Rendering Primitives
2
- // Solid-style rendering: components run once, signals create individual DOM effects.
3
- // No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
4
-
5
- import { effect, untrack, createRoot, signal } from './reactive.js';
6
-
7
- // --- template(html) ---
8
- // Pre-parse HTML string into a <template> element. Returns a factory function
9
- // that clones the DOM tree via cloneNode(true) — 2-5x faster than createElement chains.
10
-
11
- export function template(html) {
12
- const t = document.createElement('template');
13
- t.innerHTML = html.trim();
14
- return () => t.content.firstChild.cloneNode(true);
1
+ // packages/core/src/reactive.js
2
+ var __DEV__ = typeof process !== "undefined" ? true : true;
3
+ var __devtools = null;
4
+ var currentEffect = null;
5
+ var currentRoot = null;
6
+ var currentOwner = null;
7
+ var insideComputed = false;
8
+ var batchDepth = 0;
9
+ var pendingEffects = [];
10
+ var pendingNeedSort = false;
11
+ var subSetOwner = /* @__PURE__ */ new WeakMap();
12
+ var NEEDS_UPSTREAM = /* @__PURE__ */ Symbol("needs_upstream");
13
+ function signal(initial, debugName) {
14
+ let value = initial;
15
+ const subs = /* @__PURE__ */ new Set();
16
+ function sig(...args) {
17
+ if (args.length === 0) {
18
+ if (currentEffect) {
19
+ subs.add(currentEffect);
20
+ currentEffect.deps.push(subs);
21
+ }
22
+ return value;
23
+ }
24
+ if (__DEV__ && insideComputed) {
25
+ console.warn(
26
+ "[what] Signal.set() called inside a computed function. This may cause infinite loops. Use effect() instead." + (debugName ? ` (signal: ${debugName})` : "")
27
+ );
28
+ }
29
+ const nextVal = typeof args[0] === "function" ? args[0](value) : args[0];
30
+ if (Object.is(value, nextVal)) return;
31
+ value = nextVal;
32
+ if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
33
+ if (subs.size > 0) notify(subs);
34
+ }
35
+ sig.set = (next) => {
36
+ if (__DEV__ && insideComputed) {
37
+ console.warn(
38
+ "[what] Signal.set() called inside a computed function. This may cause infinite loops. Use effect() instead." + (debugName ? ` (signal: ${debugName})` : "")
39
+ );
40
+ }
41
+ const nextVal = typeof next === "function" ? next(value) : next;
42
+ if (Object.is(value, nextVal)) return;
43
+ value = nextVal;
44
+ if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
45
+ if (subs.size > 0) notify(subs);
46
+ };
47
+ sig.peek = () => value;
48
+ sig.subscribe = (fn) => {
49
+ return effect(() => fn(sig()));
50
+ };
51
+ sig._signal = true;
52
+ if (__DEV__) {
53
+ sig._subs = subs;
54
+ if (debugName) sig._debugName = debugName;
55
+ }
56
+ if (__DEV__ && __devtools) __devtools.onSignalCreate(sig);
57
+ return sig;
58
+ }
59
+ function _updateLevel(e) {
60
+ let maxDepLevel = 0;
61
+ const deps = e.deps;
62
+ for (let i = 0; i < deps.length; i++) {
63
+ const owner = subSetOwner.get(deps[i]);
64
+ if (owner) {
65
+ const depLevel = owner._level;
66
+ if (depLevel > maxDepLevel) maxDepLevel = depLevel;
67
+ }
68
+ }
69
+ e._level = maxDepLevel + 1;
70
+ }
71
+ function effect(fn, opts) {
72
+ const e = _createEffect(fn);
73
+ e._level = 1;
74
+ const prev = currentEffect;
75
+ currentEffect = e;
76
+ try {
77
+ const result = e.fn();
78
+ if (typeof result === "function") e._cleanup = result;
79
+ } finally {
80
+ currentEffect = prev;
81
+ }
82
+ _updateLevel(e);
83
+ if (opts?.stable) e._stable = true;
84
+ const dispose = () => _disposeEffect(e);
85
+ if (currentRoot) {
86
+ currentRoot.disposals.push(dispose);
87
+ }
88
+ return dispose;
89
+ }
90
+ function _createEffect(fn, lazy) {
91
+ const e = {
92
+ fn,
93
+ deps: [],
94
+ // array of subscriber sets (cheaper than Set for typical 1-3 deps)
95
+ lazy: lazy || false,
96
+ _onNotify: null,
97
+ disposed: false,
98
+ _pending: false,
99
+ _stable: false,
100
+ // stable effects skip cleanup/re-subscribe on re-run
101
+ _level: 0,
102
+ // topological depth: signals=0, computed/effects=max(deps)+1
103
+ _computed: false,
104
+ // true for computed inner effects
105
+ _computedSubs: null,
106
+ // reference to the computed's subscriber set
107
+ _isDirty: null,
108
+ // function to check if computed is dirty (set by computed())
109
+ _markDirty: null
110
+ // function to mark computed dirty (set by computed())
111
+ };
112
+ if (__DEV__ && __devtools) __devtools.onEffectCreate(e);
113
+ return e;
114
+ }
115
+ function _runEffect(e) {
116
+ if (e.disposed) return;
117
+ if (e._stable) {
118
+ if (e._cleanup) {
119
+ try {
120
+ e._cleanup();
121
+ } catch (err) {
122
+ if (__DEV__) console.warn("[what] Error in effect cleanup:", err);
123
+ }
124
+ e._cleanup = null;
125
+ }
126
+ const prev2 = currentEffect;
127
+ currentEffect = null;
128
+ try {
129
+ const result = e.fn();
130
+ if (typeof result === "function") e._cleanup = result;
131
+ } catch (err) {
132
+ if (__devtools?.onError) __devtools.onError(err, { type: "effect", effect: e });
133
+ if (__DEV__) console.warn("[what] Error in stable effect:", err);
134
+ } finally {
135
+ currentEffect = prev2;
136
+ }
137
+ if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
138
+ return;
139
+ }
140
+ cleanup(e);
141
+ if (e._cleanup) {
142
+ try {
143
+ e._cleanup();
144
+ } catch (err) {
145
+ if (__devtools?.onError) __devtools.onError(err, { type: "effect-cleanup", effect: e });
146
+ if (__DEV__) console.warn("[what] Error in effect cleanup:", err);
147
+ }
148
+ e._cleanup = null;
149
+ }
150
+ const prev = currentEffect;
151
+ currentEffect = e;
152
+ try {
153
+ const result = e.fn();
154
+ if (typeof result === "function") {
155
+ e._cleanup = result;
156
+ }
157
+ } catch (err) {
158
+ if (err === NEEDS_UPSTREAM) throw err;
159
+ if (__devtools?.onError) __devtools.onError(err, { type: "effect", effect: e });
160
+ throw err;
161
+ } finally {
162
+ currentEffect = prev;
163
+ }
164
+ if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
165
+ }
166
+ function _disposeEffect(e) {
167
+ e.disposed = true;
168
+ if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
169
+ cleanup(e);
170
+ if (e._cleanup) {
171
+ try {
172
+ e._cleanup();
173
+ } catch (err) {
174
+ if (__DEV__) console.warn("[what] Error in effect cleanup on dispose:", err);
175
+ }
176
+ e._cleanup = null;
177
+ }
178
+ }
179
+ function cleanup(e) {
180
+ const deps = e.deps;
181
+ for (let i = 0; i < deps.length; i++) deps[i].delete(e);
182
+ deps.length = 0;
183
+ }
184
+ var notifyDepth = 0;
185
+ var notifyQueue = null;
186
+ var notifyQueueLen = 0;
187
+ function notify(subs) {
188
+ if (notifyDepth === 0) {
189
+ notifyDepth = 1;
190
+ try {
191
+ for (const e of subs) {
192
+ if (e.disposed) continue;
193
+ if (e._onNotify) {
194
+ e._onNotify();
195
+ } else if (batchDepth === 0 && e._stable) {
196
+ const prev = currentEffect;
197
+ currentEffect = null;
198
+ try {
199
+ const result = e.fn();
200
+ if (typeof result === "function") {
201
+ if (e._cleanup) try {
202
+ e._cleanup();
203
+ } catch (err) {
204
+ }
205
+ e._cleanup = result;
206
+ }
207
+ } catch (err) {
208
+ if (__devtools?.onError) __devtools.onError(err, { type: "effect", effect: e });
209
+ if (__DEV__) console.warn("[what] Error in stable effect:", err);
210
+ } finally {
211
+ currentEffect = prev;
212
+ }
213
+ } else if (!e._pending) {
214
+ e._pending = true;
215
+ const level = e._level;
216
+ const len = pendingEffects.length;
217
+ if (len > 0 && pendingEffects[len - 1]._level > level) {
218
+ pendingNeedSort = true;
219
+ }
220
+ pendingEffects.push(e);
221
+ }
222
+ }
223
+ if (notifyQueueLen > 0) {
224
+ let qi = 0;
225
+ while (qi < notifyQueueLen) {
226
+ const queuedSubs = notifyQueue[qi];
227
+ notifyQueue[qi] = null;
228
+ qi++;
229
+ for (const e of queuedSubs) {
230
+ if (e.disposed) continue;
231
+ if (e._onNotify) {
232
+ e._onNotify();
233
+ } else if (batchDepth === 0 && e._stable) {
234
+ const prev = currentEffect;
235
+ currentEffect = null;
236
+ try {
237
+ const result = e.fn();
238
+ if (typeof result === "function") {
239
+ if (e._cleanup) try {
240
+ e._cleanup();
241
+ } catch (err) {
242
+ }
243
+ e._cleanup = result;
244
+ }
245
+ } catch (err) {
246
+ if (__devtools?.onError) __devtools.onError(err, { type: "effect", effect: e });
247
+ if (__DEV__) console.warn("[what] Error in stable effect:", err);
248
+ } finally {
249
+ currentEffect = prev;
250
+ }
251
+ } else if (!e._pending) {
252
+ e._pending = true;
253
+ const level = e._level;
254
+ const len = pendingEffects.length;
255
+ if (len > 0 && pendingEffects[len - 1]._level > level) {
256
+ pendingNeedSort = true;
257
+ }
258
+ pendingEffects.push(e);
259
+ }
260
+ }
261
+ }
262
+ notifyQueueLen = 0;
263
+ }
264
+ } finally {
265
+ notifyDepth = 0;
266
+ }
267
+ if (batchDepth === 0 && pendingEffects.length > 0) scheduleMicrotask();
268
+ } else {
269
+ if (notifyQueue === null) notifyQueue = [];
270
+ if (notifyQueueLen >= notifyQueue.length) {
271
+ notifyQueue.push(subs);
272
+ } else {
273
+ notifyQueue[notifyQueueLen] = subs;
274
+ }
275
+ notifyQueueLen++;
276
+ }
277
+ }
278
+ var microtaskScheduled = false;
279
+ function scheduleMicrotask() {
280
+ if (!microtaskScheduled) {
281
+ microtaskScheduled = true;
282
+ queueMicrotask(() => {
283
+ microtaskScheduled = false;
284
+ flush();
285
+ });
286
+ }
287
+ }
288
+ function flush() {
289
+ let iterations = 0;
290
+ while (pendingEffects.length > 0 && iterations < 25) {
291
+ const batch2 = pendingEffects;
292
+ pendingEffects = [];
293
+ if (batch2.length > 1 && pendingNeedSort) {
294
+ batch2.sort((a, b) => a._level - b._level);
295
+ }
296
+ pendingNeedSort = false;
297
+ for (let i = 0; i < batch2.length; i++) {
298
+ const e = batch2[i];
299
+ e._pending = false;
300
+ if (!e.disposed && !e._onNotify) {
301
+ const prevDepsLen = e.deps.length;
302
+ _runEffect(e);
303
+ if (!e._computed && e.deps.length !== prevDepsLen) {
304
+ _updateLevel(e);
305
+ }
306
+ }
307
+ }
308
+ iterations++;
309
+ }
310
+ if (iterations >= 25) {
311
+ if (__DEV__) {
312
+ const remaining = pendingEffects.slice(0, 3);
313
+ const effectNames = remaining.map((e) => e.fn?.name || e.fn?.toString().slice(0, 60) || "(anonymous)");
314
+ console.warn(
315
+ `[what] Possible infinite effect loop detected (25 iterations). Likely cause: an effect writes to a signal it also reads, creating a cycle. Use untrack() to read signals without subscribing. Looping effects: ${effectNames.join(", ")}`
316
+ );
317
+ } else {
318
+ console.warn("[what] Possible infinite effect loop detected");
319
+ }
320
+ for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;
321
+ pendingEffects.length = 0;
322
+ }
323
+ }
324
+ function untrack(fn) {
325
+ const prev = currentEffect;
326
+ currentEffect = null;
327
+ try {
328
+ return fn();
329
+ } finally {
330
+ currentEffect = prev;
331
+ }
332
+ }
333
+ function createRoot(fn) {
334
+ const prevRoot = currentRoot;
335
+ const prevOwner = currentOwner;
336
+ const root = {
337
+ disposals: [],
338
+ owner: currentOwner,
339
+ // parent owner for ownership tree
340
+ children: [],
341
+ // child roots (ownership tree)
342
+ _disposed: false
343
+ };
344
+ if (currentOwner) {
345
+ currentOwner.children.push(root);
346
+ }
347
+ currentRoot = root;
348
+ currentOwner = root;
349
+ try {
350
+ const dispose = () => {
351
+ if (root._disposed) return;
352
+ root._disposed = true;
353
+ for (let i = root.children.length - 1; i >= 0; i--) {
354
+ _disposeRoot(root.children[i]);
355
+ }
356
+ root.children.length = 0;
357
+ for (let i = root.disposals.length - 1; i >= 0; i--) {
358
+ root.disposals[i]();
359
+ }
360
+ root.disposals.length = 0;
361
+ if (root.owner) {
362
+ const idx = root.owner.children.indexOf(root);
363
+ if (idx >= 0) root.owner.children.splice(idx, 1);
364
+ }
365
+ };
366
+ return fn(dispose);
367
+ } finally {
368
+ currentRoot = prevRoot;
369
+ currentOwner = prevOwner;
370
+ }
371
+ }
372
+ function _disposeRoot(root) {
373
+ if (root._disposed) return;
374
+ root._disposed = true;
375
+ for (let i = root.children.length - 1; i >= 0; i--) {
376
+ _disposeRoot(root.children[i]);
377
+ }
378
+ root.children.length = 0;
379
+ for (let i = root.disposals.length - 1; i >= 0; i--) {
380
+ root.disposals[i]();
381
+ }
382
+ root.disposals.length = 0;
15
383
  }
16
384
 
17
- // --- insert(parent, child, marker?) ---
18
- // Reactive child insertion. Handles all child types:
19
- // - string/number → text node
20
- // - function → effect that updates text node reactively
21
- // - DOM node → append directly
22
- // - array → insert each element
23
-
24
- export function insert(parent, child, marker) {
25
- if (child == null || typeof child === 'boolean') return;
26
-
27
- if (typeof child === 'string' || typeof child === 'number') {
28
- const textNode = document.createTextNode(String(child));
29
- parent.insertBefore(textNode, marker || null);
30
- return textNode;
385
+ // packages/core/src/components.js
386
+ var _getCurrentComponent = null;
387
+ function _injectGetCurrentComponent(fn) {
388
+ _getCurrentComponent = fn;
389
+ }
390
+ function reportError(error, startCtx) {
391
+ let ctx = startCtx || _getCurrentComponent?.();
392
+ while (ctx) {
393
+ if (ctx._errorBoundary) {
394
+ ctx._errorBoundary(error);
395
+ return true;
396
+ }
397
+ ctx = ctx._parentCtx;
31
398
  }
399
+ return false;
400
+ }
32
401
 
33
- if (typeof child === 'function') {
34
- // Reactive expression — create micro-effect
35
- let currentNode = document.createTextNode('');
36
- parent.insertBefore(currentNode, marker || null);
402
+ // packages/core/src/helpers.js
403
+ var _getCurrentComponentRef = null;
404
+ function _setComponentRef(fn) {
405
+ _getCurrentComponentRef = fn;
406
+ }
37
407
 
38
- effect(() => {
39
- const value = child();
40
- if (value instanceof Node) {
41
- // Function returned a DOM node — replace text node with it
42
- if (currentNode !== value) {
43
- parent.replaceChild(value, currentNode);
44
- currentNode = value;
408
+ // packages/core/src/dom.js
409
+ var SVG_ELEMENTS = /* @__PURE__ */ new Set([
410
+ "svg",
411
+ "path",
412
+ "circle",
413
+ "rect",
414
+ "line",
415
+ "polyline",
416
+ "polygon",
417
+ "ellipse",
418
+ "g",
419
+ "defs",
420
+ "use",
421
+ "symbol",
422
+ "clipPath",
423
+ "mask",
424
+ "pattern",
425
+ "image",
426
+ "text",
427
+ "tspan",
428
+ "textPath",
429
+ "foreignObject",
430
+ "linearGradient",
431
+ "radialGradient",
432
+ "stop",
433
+ "marker",
434
+ "animate",
435
+ "animateTransform",
436
+ "animateMotion",
437
+ "set",
438
+ "filter",
439
+ "feBlend",
440
+ "feColorMatrix",
441
+ "feComponentTransfer",
442
+ "feComposite",
443
+ "feConvolveMatrix",
444
+ "feDiffuseLighting",
445
+ "feDisplacementMap",
446
+ "feFlood",
447
+ "feGaussianBlur",
448
+ "feImage",
449
+ "feMerge",
450
+ "feMergeNode",
451
+ "feMorphology",
452
+ "feOffset",
453
+ "feSpecularLighting",
454
+ "feTile",
455
+ "feTurbulence"
456
+ ]);
457
+ var SVG_NS = "http://www.w3.org/2000/svg";
458
+ var mountedComponents = /* @__PURE__ */ new Set();
459
+ var _commentCtxMap = /* @__PURE__ */ new WeakMap();
460
+ function isDomNode(value) {
461
+ if (!value || typeof value !== "object") return false;
462
+ if (typeof Node !== "undefined" && value instanceof Node) return true;
463
+ return typeof value.nodeType === "number" && typeof value.nodeName === "string";
464
+ }
465
+ function isVNode(value) {
466
+ return !!value && typeof value === "object" && (value._vnode === true || "tag" in value);
467
+ }
468
+ function disposeComponent(ctx) {
469
+ if (ctx.disposed) return;
470
+ ctx.disposed = true;
471
+ if (ctx.cleanups) {
472
+ for (const cleanup2 of ctx.cleanups) {
473
+ try {
474
+ cleanup2();
475
+ } catch (e) {
476
+ console.error("[what] cleanup error:", e);
477
+ }
478
+ }
479
+ }
480
+ if (ctx.effects) {
481
+ for (const dispose of ctx.effects) {
482
+ try {
483
+ dispose();
484
+ } catch (e) {
485
+ }
486
+ }
487
+ }
488
+ if (ctx.hooks) {
489
+ for (const hook of ctx.hooks) {
490
+ if (hook && typeof hook.cleanup === "function") {
491
+ try {
492
+ hook.cleanup();
493
+ } catch (e) {
494
+ console.error("[what] hook cleanup error:", e);
45
495
  }
46
- } else if (Array.isArray(value)) {
47
- // Function returned array — handle dynamic lists
48
- _insertArray(parent, value, currentNode, marker);
49
- } else {
50
- // Primitive update text content
51
- const text = value == null || typeof value === 'boolean' ? '' : String(value);
52
- if (currentNode.nodeType === 3) {
53
- if (currentNode.textContent !== text) currentNode.textContent = text;
54
- } else {
55
- const textNode = document.createTextNode(text);
56
- parent.replaceChild(textNode, currentNode);
57
- currentNode = textNode;
496
+ }
497
+ }
498
+ }
499
+ if (ctx._cleanupCallbacks) {
500
+ for (const fn of ctx._cleanupCallbacks) {
501
+ try {
502
+ fn();
503
+ } catch (e) {
504
+ console.error("[what] onCleanup error:", e);
505
+ }
506
+ }
507
+ }
508
+ if (__DEV__ && __devtools?.onComponentUnmount) __devtools.onComponentUnmount(ctx);
509
+ mountedComponents.delete(ctx);
510
+ }
511
+ function disposeTree(node) {
512
+ if (!node) return;
513
+ if (node._componentCtx) {
514
+ disposeComponent(node._componentCtx);
515
+ }
516
+ const commentCtx = _commentCtxMap.get(node);
517
+ if (commentCtx) {
518
+ disposeComponent(commentCtx);
519
+ }
520
+ if (node._dispose) {
521
+ try {
522
+ node._dispose();
523
+ } catch (e) {
524
+ }
525
+ }
526
+ if (node._propEffects) {
527
+ for (const key in node._propEffects) {
528
+ try {
529
+ node._propEffects[key]();
530
+ } catch (e) {
531
+ }
532
+ }
533
+ }
534
+ if (node.childNodes) {
535
+ for (const child of node.childNodes) {
536
+ disposeTree(child);
537
+ }
538
+ }
539
+ }
540
+ function createDOM(vnode, parent, isSvg) {
541
+ if (vnode == null || vnode === false || vnode === true) {
542
+ return document.createComment("");
543
+ }
544
+ if (typeof vnode === "string" || typeof vnode === "number") {
545
+ return document.createTextNode(String(vnode));
546
+ }
547
+ if (isDomNode(vnode)) {
548
+ return vnode;
549
+ }
550
+ if (typeof vnode === "function") {
551
+ const startMarker = document.createComment("fn");
552
+ const endMarker = document.createComment("/fn");
553
+ let currentNodes = [];
554
+ const frag = document.createDocumentFragment();
555
+ frag.appendChild(startMarker);
556
+ frag.appendChild(endMarker);
557
+ const dispose = effect(() => {
558
+ const val = vnode();
559
+ const vnodes = val == null || val === false || val === true ? [] : Array.isArray(val) ? val : [val];
560
+ const realParent = endMarker.parentNode;
561
+ if (!realParent) return;
562
+ for (const old of currentNodes) {
563
+ disposeTree(old);
564
+ if (old.parentNode === realParent) realParent.removeChild(old);
565
+ }
566
+ currentNodes = [];
567
+ for (const v of vnodes) {
568
+ const node = createDOM(v, realParent, parent?._isSvg);
569
+ if (node) {
570
+ if (node.nodeType === 11) {
571
+ const children = Array.from(node.childNodes);
572
+ realParent.insertBefore(node, endMarker);
573
+ for (const child of children) currentNodes.push(child);
574
+ } else {
575
+ realParent.insertBefore(node, endMarker);
576
+ currentNodes.push(node);
577
+ }
58
578
  }
59
579
  }
60
580
  });
61
-
62
- return currentNode;
581
+ startMarker._dispose = dispose;
582
+ endMarker._dispose = dispose;
583
+ return frag;
63
584
  }
64
-
65
- if (child instanceof Node) {
66
- parent.insertBefore(child, marker || null);
67
- return child;
585
+ if (Array.isArray(vnode)) {
586
+ const frag = document.createDocumentFragment();
587
+ for (const child of vnode) {
588
+ const node = createDOM(child, parent, isSvg);
589
+ if (node) frag.appendChild(node);
590
+ }
591
+ return frag;
68
592
  }
69
-
70
- if (Array.isArray(child)) {
71
- const nodes = [];
72
- for (let i = 0; i < child.length; i++) {
73
- const node = insert(parent, child[i], marker);
74
- if (node) nodes.push(node);
593
+ if (isVNode(vnode) && typeof vnode.tag === "function") {
594
+ return createComponent(vnode, parent, isSvg);
595
+ }
596
+ if (isVNode(vnode)) {
597
+ return createElementFromVNode(vnode, parent, isSvg);
598
+ }
599
+ return document.createTextNode(String(vnode));
600
+ }
601
+ var componentStack = [];
602
+ function getCurrentComponent() {
603
+ return componentStack[componentStack.length - 1];
604
+ }
605
+ _injectGetCurrentComponent(getCurrentComponent);
606
+ _setComponentRef(getCurrentComponent);
607
+ function getComponentStack() {
608
+ return componentStack;
609
+ }
610
+ function createComponent(vnode, parent, isSvg) {
611
+ let { tag: Component, props, children } = vnode;
612
+ if (typeof Component === "function" && (Component.prototype?.isReactComponent || Component.prototype?.render)) {
613
+ const ClassComp = Component;
614
+ Component = function ClassComponentBridge(props2) {
615
+ const instance = new ClassComp(props2);
616
+ return instance.render();
617
+ };
618
+ Component.displayName = ClassComp.displayName || ClassComp.name || "ClassComponent";
619
+ }
620
+ if (Component === "__errorBoundary" || vnode.tag === "__errorBoundary") {
621
+ return createErrorBoundary(vnode, parent);
622
+ }
623
+ if (Component === "__suspense" || vnode.tag === "__suspense") {
624
+ return createSuspenseBoundary(vnode, parent);
625
+ }
626
+ if (Component === "__portal" || vnode.tag === "__portal") {
627
+ return createPortalDOM(vnode, parent);
628
+ }
629
+ const ctx = {
630
+ hooks: [],
631
+ hookIndex: 0,
632
+ effects: [],
633
+ cleanups: [],
634
+ mounted: false,
635
+ disposed: false,
636
+ Component,
637
+ _parentCtx: componentStack[componentStack.length - 1] || null,
638
+ _errorBoundary: (() => {
639
+ let p = componentStack[componentStack.length - 1];
640
+ while (p) {
641
+ if (p._errorBoundary) return p._errorBoundary;
642
+ p = p._parentCtx;
643
+ }
644
+ return null;
645
+ })()
646
+ };
647
+ const startComment = document.createComment("c:start");
648
+ const endComment = document.createComment("c:end");
649
+ _commentCtxMap.set(startComment, ctx);
650
+ ctx._startComment = startComment;
651
+ ctx._endComment = endComment;
652
+ const container = document.createDocumentFragment();
653
+ container._componentCtx = ctx;
654
+ ctx._wrapper = startComment;
655
+ mountedComponents.add(ctx);
656
+ if (__DEV__ && __devtools?.onComponentMount) __devtools.onComponentMount(ctx);
657
+ const propsChildren = children.length === 0 ? void 0 : children.length === 1 ? children[0] : children;
658
+ const propsSignal = signal({ ...props, children: propsChildren });
659
+ ctx._propsSignal = propsSignal;
660
+ const reactiveProps = new Proxy({}, {
661
+ get(_, key) {
662
+ const current = propsSignal();
663
+ return current[key];
664
+ },
665
+ has(_, key) {
666
+ const current = propsSignal();
667
+ return key in current;
668
+ },
669
+ ownKeys() {
670
+ const current = propsSignal();
671
+ return Reflect.ownKeys(current);
672
+ },
673
+ getOwnPropertyDescriptor(_, key) {
674
+ const current = propsSignal();
675
+ if (key in current) {
676
+ return { value: current[key], writable: false, enumerable: true, configurable: true };
677
+ }
678
+ return void 0;
75
679
  }
76
- return nodes;
680
+ });
681
+ componentStack.push(ctx);
682
+ let result;
683
+ try {
684
+ result = Component(reactiveProps);
685
+ } catch (error) {
686
+ componentStack.pop();
687
+ if (!reportError(error, ctx)) {
688
+ console.error("[what] Uncaught error in component:", Component.name || "Anonymous", error);
689
+ throw error;
690
+ }
691
+ container.appendChild(startComment);
692
+ container.appendChild(endComment);
693
+ return container;
694
+ }
695
+ componentStack.pop();
696
+ ctx.mounted = true;
697
+ if (ctx._mountCallbacks) {
698
+ queueMicrotask(() => {
699
+ if (ctx.disposed) return;
700
+ for (const fn of ctx._mountCallbacks) {
701
+ try {
702
+ fn();
703
+ } catch (e) {
704
+ console.error("[what] onMount error:", e);
705
+ }
706
+ }
707
+ });
77
708
  }
709
+ container.appendChild(startComment);
710
+ const vnodes = Array.isArray(result) ? result : [result];
711
+ for (const v of vnodes) {
712
+ const node = createDOM(v, container, isSvg);
713
+ if (node) container.appendChild(node);
714
+ }
715
+ container.appendChild(endComment);
716
+ return container;
78
717
  }
79
-
80
- function _insertArray(parent, arr, currentNode, marker) {
81
- // Simple case: replace placeholder with array nodes
82
- const frag = document.createDocumentFragment();
83
- for (let i = 0; i < arr.length; i++) {
84
- if (arr[i] instanceof Node) {
85
- frag.appendChild(arr[i]);
86
- } else if (arr[i] != null && typeof arr[i] !== 'boolean') {
87
- frag.appendChild(document.createTextNode(String(arr[i])));
718
+ function createErrorBoundary(vnode, parent) {
719
+ const { errorState, handleError, fallback, reset } = vnode.props;
720
+ const children = vnode.children;
721
+ const wrapper = document.createElement("span");
722
+ wrapper.style.display = "contents";
723
+ const boundaryCtx = {
724
+ hooks: [],
725
+ hookIndex: 0,
726
+ effects: [],
727
+ cleanups: [],
728
+ mounted: false,
729
+ disposed: false,
730
+ _parentCtx: componentStack[componentStack.length - 1] || null,
731
+ _errorBoundary: handleError
732
+ };
733
+ wrapper._componentCtx = boundaryCtx;
734
+ const dispose = effect(() => {
735
+ const error = errorState();
736
+ componentStack.push(boundaryCtx);
737
+ while (wrapper.firstChild) {
738
+ disposeTree(wrapper.firstChild);
739
+ wrapper.removeChild(wrapper.firstChild);
740
+ }
741
+ let vnodes;
742
+ if (error) {
743
+ vnodes = typeof fallback === "function" ? [fallback({ error, reset })] : [fallback];
744
+ } else {
745
+ vnodes = children;
746
+ }
747
+ vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
748
+ for (const v of vnodes) {
749
+ const node = createDOM(v, wrapper);
750
+ if (node) wrapper.appendChild(node);
751
+ }
752
+ componentStack.pop();
753
+ });
754
+ boundaryCtx.effects.push(dispose);
755
+ return wrapper;
756
+ }
757
+ function createSuspenseBoundary(vnode, parent) {
758
+ const { boundary, fallback, loading } = vnode.props;
759
+ const children = vnode.children;
760
+ const wrapper = document.createElement("span");
761
+ wrapper.style.display = "contents";
762
+ const boundaryCtx = {
763
+ hooks: [],
764
+ hookIndex: 0,
765
+ effects: [],
766
+ cleanups: [],
767
+ mounted: false,
768
+ disposed: false,
769
+ _parentCtx: componentStack[componentStack.length - 1] || null
770
+ };
771
+ wrapper._componentCtx = boundaryCtx;
772
+ const dispose = effect(() => {
773
+ const isLoading = loading();
774
+ const vnodes = isLoading ? [fallback] : children;
775
+ const normalized = Array.isArray(vnodes) ? vnodes : [vnodes];
776
+ componentStack.push(boundaryCtx);
777
+ while (wrapper.firstChild) {
778
+ disposeTree(wrapper.firstChild);
779
+ wrapper.removeChild(wrapper.firstChild);
780
+ }
781
+ for (const v of normalized) {
782
+ const node = createDOM(v, wrapper);
783
+ if (node) wrapper.appendChild(node);
784
+ }
785
+ componentStack.pop();
786
+ });
787
+ boundaryCtx.effects.push(dispose);
788
+ return wrapper;
789
+ }
790
+ function createPortalDOM(vnode, parent) {
791
+ const { container } = vnode.props;
792
+ const children = vnode.children;
793
+ if (!container) {
794
+ console.warn("[what] Portal: target container not found");
795
+ return document.createComment("portal:empty");
796
+ }
797
+ const portalCtx = {
798
+ hooks: [],
799
+ hookIndex: 0,
800
+ effects: [],
801
+ cleanups: [],
802
+ mounted: false,
803
+ disposed: false,
804
+ _parentCtx: componentStack[componentStack.length - 1] || null
805
+ };
806
+ const placeholder = document.createComment("portal");
807
+ placeholder._componentCtx = portalCtx;
808
+ const portalNodes = [];
809
+ for (const child of children) {
810
+ const node = createDOM(child, container);
811
+ if (node) {
812
+ container.appendChild(node);
813
+ portalNodes.push(node);
88
814
  }
89
815
  }
90
- parent.replaceChild(frag, currentNode);
816
+ portalCtx._cleanupCallbacks = [() => {
817
+ for (const node of portalNodes) {
818
+ disposeTree(node);
819
+ if (node.parentNode) node.parentNode.removeChild(node);
820
+ }
821
+ }];
822
+ return placeholder;
823
+ }
824
+ function createElementFromVNode(vnode, parent, isSvg) {
825
+ const { tag, props, children } = vnode;
826
+ const svgContext = isSvg || SVG_ELEMENTS.has(tag);
827
+ const el = svgContext ? document.createElementNS(SVG_NS, tag) : document.createElement(tag);
828
+ if (props) {
829
+ applyProps(el, props, {}, svgContext);
830
+ }
831
+ for (const child of children) {
832
+ const node = createDOM(child, el, svgContext && tag !== "foreignObject");
833
+ if (node) el.appendChild(node);
834
+ }
835
+ el._vnode = vnode;
836
+ return el;
837
+ }
838
+ function applyProps(el, newProps, oldProps, isSvg) {
839
+ newProps = newProps || {};
840
+ oldProps = oldProps || {};
841
+ for (const key in newProps) {
842
+ if (key === "key" || key === "children") continue;
843
+ if (key === "ref") {
844
+ if (typeof newProps.ref === "function") newProps.ref(el);
845
+ else if (newProps.ref) newProps.ref.current = el;
846
+ continue;
847
+ }
848
+ setProp(el, key, newProps[key], isSvg);
849
+ }
850
+ }
851
+ function setProp(el, key, value, isSvg) {
852
+ if (typeof value === "function" && !(key.startsWith("on") && key.length > 2) && key !== "ref") {
853
+ if (!el._propEffects) el._propEffects = {};
854
+ if (el._propEffects[key]) {
855
+ try {
856
+ el._propEffects[key]();
857
+ } catch (e) {
858
+ }
859
+ }
860
+ el._propEffects[key] = effect(() => {
861
+ const resolved = value();
862
+ setProp(el, key, resolved, isSvg);
863
+ });
864
+ return;
865
+ }
866
+ if (key.startsWith("on") && key.length > 2) {
867
+ let eventName = key.slice(2);
868
+ let useCapture = false;
869
+ if (eventName.endsWith("Capture")) {
870
+ eventName = eventName.slice(0, -7);
871
+ useCapture = true;
872
+ }
873
+ const event = eventName.toLowerCase();
874
+ const storageKey = useCapture ? event + "_capture" : event;
875
+ const old = el._events?.[storageKey];
876
+ if (old && old._original === value) return;
877
+ if (old) el.removeEventListener(event, old, useCapture);
878
+ if (value == null) return;
879
+ if (!el._events) el._events = {};
880
+ const wrappedHandler = (e) => {
881
+ if (!e.nativeEvent) e.nativeEvent = e;
882
+ return untrack(() => value(e));
883
+ };
884
+ wrappedHandler._original = value;
885
+ el._events[storageKey] = wrappedHandler;
886
+ const eventOpts = value._eventOpts;
887
+ el.addEventListener(event, wrappedHandler, eventOpts || useCapture || void 0);
888
+ return;
889
+ }
890
+ if (key === "className" || key === "class") {
891
+ if (isSvg) {
892
+ el.setAttribute("class", value || "");
893
+ } else {
894
+ el.className = value || "";
895
+ }
896
+ return;
897
+ }
898
+ if (key === "style") {
899
+ if (typeof value === "string") {
900
+ el.style.cssText = value;
901
+ el._prevStyle = null;
902
+ } else if (typeof value === "object") {
903
+ const oldStyle = el._prevStyle || {};
904
+ for (const prop in oldStyle) {
905
+ if (!(prop in value)) el.style[prop] = "";
906
+ }
907
+ for (const prop in value) {
908
+ el.style[prop] = value[prop] ?? "";
909
+ }
910
+ el._prevStyle = { ...value };
911
+ }
912
+ return;
913
+ }
914
+ if (key === "dangerouslySetInnerHTML") {
915
+ el.innerHTML = value?.__html ?? "";
916
+ return;
917
+ }
918
+ if (key === "innerHTML") {
919
+ if (value == null) return;
920
+ if (value && typeof value === "object" && "__html" in value) {
921
+ el.innerHTML = value.__html ?? "";
922
+ } else {
923
+ if (__DEV__) {
924
+ console.warn(
925
+ "[what] innerHTML received a raw string. This is a security risk (XSS). Use innerHTML={{ __html: trustedString }} or dangerouslySetInnerHTML={{ __html: trustedString }} instead."
926
+ );
927
+ }
928
+ return;
929
+ }
930
+ return;
931
+ }
932
+ if (typeof value === "boolean") {
933
+ if (value) el.setAttribute(key, "");
934
+ else el.removeAttribute(key);
935
+ return;
936
+ }
937
+ if (key.startsWith("data-") || key.startsWith("aria-")) {
938
+ el.setAttribute(key, value);
939
+ return;
940
+ }
941
+ if (isSvg) {
942
+ if (value === false || value == null) {
943
+ el.removeAttribute(key);
944
+ } else {
945
+ el.setAttribute(key, value === true ? "" : String(value));
946
+ }
947
+ return;
948
+ }
949
+ if (key in el) {
950
+ el[key] = value;
951
+ } else {
952
+ el.setAttribute(key, value);
953
+ }
91
954
  }
92
955
 
93
- // --- mapArray(source, mapFn, options?) ---
94
- // Reactive list rendering with per-item scopes.
95
- // Unkeyed: tracks items by reference. Keyed: tracks by key function.
96
- // With key + raw: mapFn receives (item, index) — raw item value. Items identified by key for
97
- // efficient DOM reuse/moves. Use when items have per-field signals (no wrapper needed).
98
- // With key (no raw): mapFn receives (itemAccessor, index) — accessor is a signal getter.
99
- // When item reference changes but key persists, the signal updates in place.
100
- // Without key: mapFn receives (item, index) — raw item value. New reference = new row.
101
-
102
- export function mapArray(source, mapFn, options) {
956
+ // packages/core/src/render.js
957
+ function _$createComponent(Component, props, children) {
958
+ if (children && children.length > 0) {
959
+ const mergedChildren = children.length === 1 ? children[0] : children;
960
+ props = props ? { ...props, children: mergedChildren } : { children: mergedChildren };
961
+ }
962
+ return createDOM({ tag: Component, props: props || {}, children: children || [], key: null, _vnode: true });
963
+ }
964
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "formAction"]);
965
+ function isSafeUrl(url) {
966
+ if (typeof url !== "string") return true;
967
+ const normalized = url.trim().replace(/[\s\x00-\x1f]/g, "").toLowerCase();
968
+ if (normalized.startsWith("javascript:")) return false;
969
+ if (normalized.startsWith("data:")) return false;
970
+ if (normalized.startsWith("vbscript:")) return false;
971
+ return true;
972
+ }
973
+ var TABLE_WRAPPERS = {
974
+ tr: { depth: 2, wrap: "<table><tbody>", unwrap: "</tbody></table>" },
975
+ td: { depth: 3, wrap: "<table><tbody><tr>", unwrap: "</tr></tbody></table>" },
976
+ th: { depth: 3, wrap: "<table><tbody><tr>", unwrap: "</tr></tbody></table>" },
977
+ thead: { depth: 1, wrap: "<table>", unwrap: "</table>" },
978
+ tbody: { depth: 1, wrap: "<table>", unwrap: "</table>" },
979
+ tfoot: { depth: 1, wrap: "<table>", unwrap: "</table>" },
980
+ colgroup: { depth: 1, wrap: "<table>", unwrap: "</table>" },
981
+ col: { depth: 1, wrap: "<table>", unwrap: "</table>" },
982
+ caption: { depth: 1, wrap: "<table>", unwrap: "</table>" }
983
+ };
984
+ var SVG_ELEMENTS2 = /* @__PURE__ */ new Set([
985
+ "svg",
986
+ "path",
987
+ "circle",
988
+ "rect",
989
+ "line",
990
+ "polyline",
991
+ "polygon",
992
+ "ellipse",
993
+ "g",
994
+ "defs",
995
+ "use",
996
+ "text",
997
+ "tspan",
998
+ "foreignObject",
999
+ "clipPath",
1000
+ "mask",
1001
+ "pattern",
1002
+ "linearGradient",
1003
+ "radialGradient",
1004
+ "stop",
1005
+ "marker",
1006
+ "symbol",
1007
+ "image",
1008
+ "animate",
1009
+ "animateTransform",
1010
+ "animateMotion",
1011
+ "set",
1012
+ "filter",
1013
+ "feGaussianBlur",
1014
+ "feOffset",
1015
+ "feMerge",
1016
+ "feMergeNode",
1017
+ "feBlend",
1018
+ "feColorMatrix",
1019
+ "feComponentTransfer",
1020
+ "feComposite",
1021
+ "feConvolveMatrix",
1022
+ "feDiffuseLighting",
1023
+ "feDisplacementMap",
1024
+ "feFlood",
1025
+ "feImage",
1026
+ "feMorphology",
1027
+ "feSpecularLighting",
1028
+ "feTile",
1029
+ "feTurbulence",
1030
+ "feDistantLight",
1031
+ "fePointLight",
1032
+ "feSpotLight"
1033
+ ]);
1034
+ function getLeadingTag(html) {
1035
+ const m = html.match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
1036
+ return m ? m[1] : "";
1037
+ }
1038
+ function _$templateImpl(html) {
1039
+ const trimmed = html.trim();
1040
+ const tag = getLeadingTag(trimmed);
1041
+ if (SVG_ELEMENTS2.has(tag)) {
1042
+ return svgTemplate(trimmed);
1043
+ }
1044
+ const tableInfo = TABLE_WRAPPERS[tag];
1045
+ if (tableInfo) {
1046
+ const t2 = document.createElement("template");
1047
+ t2.innerHTML = tableInfo.wrap + trimmed + tableInfo.unwrap;
1048
+ return () => {
1049
+ let node = t2.content.firstChild;
1050
+ for (let i = 0; i < tableInfo.depth; i++) {
1051
+ node = node.firstChild;
1052
+ }
1053
+ return node.cloneNode(true);
1054
+ };
1055
+ }
1056
+ const t = document.createElement("template");
1057
+ t.innerHTML = trimmed;
1058
+ return () => t.content.firstChild.cloneNode(true);
1059
+ }
1060
+ var _templateWarned = false;
1061
+ function template(html) {
1062
+ if (__DEV__ && !_templateWarned) {
1063
+ _templateWarned = true;
1064
+ console.warn(
1065
+ "[what] template() is a compiler internal. Use JSX instead. Direct calls with user input can lead to XSS vulnerabilities."
1066
+ );
1067
+ }
1068
+ return _$templateImpl(html);
1069
+ }
1070
+ function svgTemplate(html) {
1071
+ const trimmed = html.trim();
1072
+ const tag = getLeadingTag(trimmed);
1073
+ if (tag === "svg") {
1074
+ const t2 = document.createElement("template");
1075
+ t2.innerHTML = trimmed;
1076
+ return () => t2.content.firstChild.cloneNode(true);
1077
+ }
1078
+ const t = document.createElement("template");
1079
+ t.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg">${trimmed}</svg>`;
1080
+ return () => t.content.firstChild.firstChild.cloneNode(true);
1081
+ }
1082
+ function insert(parent, child, marker) {
1083
+ if (typeof child === "function") {
1084
+ let current = null;
1085
+ effect(() => {
1086
+ current = reconcileInsert(parent, child(), current, marker || null);
1087
+ });
1088
+ return current;
1089
+ }
1090
+ return reconcileInsert(parent, child, null, marker || null);
1091
+ }
1092
+ function isDomNode2(value) {
1093
+ if (!value || typeof value !== "object") return false;
1094
+ if (typeof Node !== "undefined" && value instanceof Node) return true;
1095
+ return typeof value.nodeType === "number" && typeof value.nodeName === "string";
1096
+ }
1097
+ function isVNode2(value) {
1098
+ return !!value && typeof value === "object" && (value._vnode === true || "tag" in value);
1099
+ }
1100
+ function isSvgParent(parent) {
1101
+ return typeof SVGElement !== "undefined" && parent instanceof SVGElement && parent.tagName.toLowerCase() !== "foreignobject";
1102
+ }
1103
+ function asNodeArray(value) {
1104
+ if (value == null) return [];
1105
+ return Array.isArray(value) ? value : [value];
1106
+ }
1107
+ function valuesToNodes(value, parent, out) {
1108
+ if (value == null || typeof value === "boolean") return out;
1109
+ if (Array.isArray(value)) {
1110
+ for (let i = 0; i < value.length; i++) {
1111
+ valuesToNodes(value[i], parent, out);
1112
+ }
1113
+ return out;
1114
+ }
1115
+ if (typeof value === "string" || typeof value === "number") {
1116
+ out.push(document.createTextNode(String(value)));
1117
+ return out;
1118
+ }
1119
+ if (isDomNode2(value)) {
1120
+ out.push(value);
1121
+ return out;
1122
+ }
1123
+ if (isVNode2(value)) {
1124
+ out.push(createDOM(value, parent, isSvgParent(parent)));
1125
+ return out;
1126
+ }
1127
+ out.push(document.createTextNode(String(value)));
1128
+ return out;
1129
+ }
1130
+ function sameNodeArray(a, b) {
1131
+ if (a.length !== b.length) return false;
1132
+ for (let i = 0; i < a.length; i++) {
1133
+ if (a[i] !== b[i]) return false;
1134
+ }
1135
+ return true;
1136
+ }
1137
+ function reconcileInsert(parent, value, current, marker) {
1138
+ const targetMarker = marker || null;
1139
+ if (value == null || typeof value === "boolean") {
1140
+ const oldNodes2 = asNodeArray(current);
1141
+ for (let i = 0; i < oldNodes2.length; i++) {
1142
+ const oldNode = oldNodes2[i];
1143
+ if (oldNode.parentNode === parent) {
1144
+ disposeTree(oldNode);
1145
+ parent.removeChild(oldNode);
1146
+ }
1147
+ }
1148
+ return null;
1149
+ }
1150
+ if ((typeof value === "string" || typeof value === "number") && current && !Array.isArray(current) && current.nodeType === 3) {
1151
+ const text = String(value);
1152
+ if (current.textContent !== text) current.textContent = text;
1153
+ return current;
1154
+ }
1155
+ const newNodes = valuesToNodes(value, parent, []);
1156
+ const oldNodes = asNodeArray(current);
1157
+ if (sameNodeArray(oldNodes, newNodes)) {
1158
+ return current;
1159
+ }
1160
+ const keep = new Set(newNodes);
1161
+ for (let i = 0; i < oldNodes.length; i++) {
1162
+ const oldNode = oldNodes[i];
1163
+ if (!keep.has(oldNode) && oldNode.parentNode === parent) {
1164
+ disposeTree(oldNode);
1165
+ parent.removeChild(oldNode);
1166
+ }
1167
+ }
1168
+ let ref = targetMarker;
1169
+ for (let i = newNodes.length - 1; i >= 0; i--) {
1170
+ const node = newNodes[i];
1171
+ if (node.parentNode !== parent || node.nextSibling !== ref) {
1172
+ if (ref && ref.parentNode !== parent) ref = null;
1173
+ if (ref) parent.insertBefore(node, ref);
1174
+ else parent.appendChild(node);
1175
+ }
1176
+ ref = node;
1177
+ }
1178
+ if (newNodes.length === 0) return null;
1179
+ return newNodes.length === 1 ? newNodes[0] : newNodes;
1180
+ }
1181
+ function mapArray(source, mapFn, options) {
103
1182
  const keyFn = options?.key;
104
1183
  const raw = options?.raw || false;
105
-
106
1184
  return (parent, marker) => {
107
1185
  let items = [];
108
1186
  let mappedNodes = [];
109
1187
  let disposeFns = [];
110
- // Keyed mode state: key { itemSignal }. Null for raw/unkeyed modes.
111
- let keyedState = keyFn && !raw ? new Map() : null;
112
-
113
- const endMarker = document.createComment('/list');
1188
+ let keyedState = keyFn && !raw ? /* @__PURE__ */ new Map() : null;
1189
+ const endMarker = document.createComment("/list");
114
1190
  parent.insertBefore(endMarker, marker || null);
115
-
116
1191
  effect(() => {
117
1192
  const newItems = source() || [];
118
1193
  if (keyFn) {
@@ -122,33 +1197,31 @@ export function mapArray(source, mapFn, options) {
122
1197
  }
123
1198
  items = newItems.slice();
124
1199
  });
125
-
126
1200
  return endMarker;
127
1201
  };
128
1202
  }
129
-
130
1203
  function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns, mapFn) {
131
1204
  const newLen = newItems.length;
132
1205
  const oldLen = oldItems.length;
133
-
134
1206
  if (newLen === 0) {
135
- // Fast path: clear all
136
1207
  if (oldLen > 0) {
137
- for (let i = 0; i < oldLen; i++) disposeFns[i]?.();
138
- parent.textContent = '';
139
- parent.appendChild(endMarker);
1208
+ for (let i = 0; i < oldLen; i++) {
1209
+ disposeFns[i]?.();
1210
+ if (mappedNodes[i]?.parentNode === parent) {
1211
+ disposeTree(mappedNodes[i]);
1212
+ parent.removeChild(mappedNodes[i]);
1213
+ }
1214
+ }
140
1215
  mappedNodes.length = 0;
141
1216
  disposeFns.length = 0;
142
1217
  }
143
1218
  return;
144
1219
  }
145
-
146
1220
  if (oldLen === 0) {
147
- // Fast path: all new
148
1221
  const frag = document.createDocumentFragment();
149
1222
  for (let i = 0; i < newLen; i++) {
150
1223
  const item = newItems[i];
151
- const node = createRoot(dispose => {
1224
+ const node = createRoot((dispose) => {
152
1225
  disposeFns[i] = dispose;
153
1226
  return mapFn(item, i);
154
1227
  });
@@ -158,23 +1231,16 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
158
1231
  parent.insertBefore(frag, endMarker);
159
1232
  return;
160
1233
  }
161
-
162
- // --- Common prefix/suffix skip ---
163
1234
  let start = 0;
164
1235
  const minLen = Math.min(oldLen, newLen);
165
1236
  while (start < minLen && oldItems[start] === newItems[start]) start++;
166
-
167
- // If everything matches and same length, nothing changed
168
1237
  if (start === oldLen && start === newLen) return;
169
-
170
1238
  let oldEnd = oldLen - 1;
171
1239
  let newEnd = newLen - 1;
172
1240
  while (oldEnd >= start && newEnd >= start && oldItems[oldEnd] === newItems[newEnd]) {
173
1241
  oldEnd--;
174
1242
  newEnd--;
175
1243
  }
176
-
177
- // Copy prefix/suffix into output arrays
178
1244
  const newMapped = new Array(newLen);
179
1245
  const newDispose = new Array(newLen);
180
1246
  for (let i = 0; i < start; i++) {
@@ -182,30 +1248,24 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
182
1248
  newDispose[i] = disposeFns[i];
183
1249
  }
184
1250
  for (let i = newEnd + 1; i < newLen; i++) {
185
- // Suffix items: same item, possibly different index offset
186
1251
  const oldI = oldEnd + 1 + (i - newEnd - 1);
187
1252
  newMapped[i] = mappedNodes[oldI];
188
1253
  newDispose[i] = disposeFns[oldI];
189
1254
  }
190
-
191
- // Only reconcile the middle section: start..newEnd (new) vs start..oldEnd (old)
192
1255
  const midNewLen = newEnd - start + 1;
193
1256
  const midOldLen = oldEnd - start + 1;
194
-
195
1257
  if (midNewLen === 0) {
196
- // Only removals in the middle
197
1258
  for (let i = start; i <= oldEnd; i++) {
198
1259
  disposeFns[i]?.();
199
1260
  if (mappedNodes[i]?.parentNode) mappedNodes[i].parentNode.removeChild(mappedNodes[i]);
200
1261
  }
201
1262
  } else if (midOldLen === 0) {
202
- // Only insertions in the middle
203
1263
  const marker = start < newLen && newMapped[newEnd + 1] ? newMapped[newEnd + 1] : endMarker;
204
1264
  const frag = document.createDocumentFragment();
205
1265
  for (let i = start; i <= newEnd; i++) {
206
1266
  const item = newItems[i];
207
1267
  const idx = i;
208
- newMapped[i] = createRoot(dispose => {
1268
+ newMapped[i] = createRoot((dispose) => {
209
1269
  newDispose[idx] = dispose;
210
1270
  return mapFn(item, idx);
211
1271
  });
@@ -213,12 +1273,21 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
213
1273
  }
214
1274
  parent.insertBefore(frag, marker);
215
1275
  } else {
216
- // General case: reconcile middle section with LIS
217
- _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns,
218
- mapFn, start, oldEnd, newEnd, newMapped, newDispose);
1276
+ _reconcileMiddle(
1277
+ parent,
1278
+ endMarker,
1279
+ oldItems,
1280
+ newItems,
1281
+ mappedNodes,
1282
+ disposeFns,
1283
+ mapFn,
1284
+ start,
1285
+ oldEnd,
1286
+ newEnd,
1287
+ newMapped,
1288
+ newDispose
1289
+ );
219
1290
  }
220
-
221
- // Update arrays in place
222
1291
  mappedNodes.length = newLen;
223
1292
  disposeFns.length = newLen;
224
1293
  for (let i = 0; i < newLen; i++) {
@@ -226,46 +1295,32 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
226
1295
  disposeFns[i] = newDispose[i];
227
1296
  }
228
1297
  }
229
-
230
- function _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns,
231
- mapFn, start, oldEnd, newEnd, newMapped, newDispose) {
232
- // Build index map only for the middle section
233
- const oldIdxMap = new Map();
1298
+ function _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns, mapFn, start, oldEnd, newEnd, newMapped, newDispose) {
1299
+ const oldIdxMap = /* @__PURE__ */ new Map();
234
1300
  for (let i = start; i <= oldEnd; i++) {
235
1301
  oldIdxMap.set(oldItems[i], i);
236
1302
  }
237
-
238
- // Match old items to new positions, collect old indices for LIS
239
1303
  const midLen = newEnd - start + 1;
240
- const oldIndices = new Int32Array(midLen); // -1 = new item
1304
+ const oldIndices = new Int32Array(midLen);
241
1305
  oldIndices.fill(-1);
242
-
243
1306
  for (let i = start; i <= newEnd; i++) {
244
1307
  const oldIdx = oldIdxMap.get(newItems[i]);
245
- if (oldIdx !== undefined) {
1308
+ if (oldIdx !== void 0) {
246
1309
  oldIdxMap.delete(newItems[i]);
247
1310
  newMapped[i] = mappedNodes[oldIdx];
248
1311
  newDispose[i] = disposeFns[oldIdx];
249
1312
  oldIndices[i - start] = oldIdx;
250
1313
  }
251
1314
  }
252
-
253
- // Dispose removed items
254
1315
  for (const [, oldIdx] of oldIdxMap) {
255
1316
  disposeFns[oldIdx]?.();
256
1317
  if (mappedNodes[oldIdx]?.parentNode) mappedNodes[oldIdx].parentNode.removeChild(mappedNodes[oldIdx]);
257
1318
  }
258
-
259
- // Compute LIS on old indices of reused items
260
- // Build the sequence of old indices for reused items only
261
1319
  const reusedCount = midLen - _countNeg1(oldIndices, midLen);
262
-
263
- // Use a bitfield (via Uint8Array) to mark LIS positions — avoids Set overhead
264
1320
  const inLIS = new Uint8Array(midLen);
265
-
266
1321
  if (reusedCount > 1) {
267
1322
  const seq = new Int32Array(reusedCount);
268
- const seqToMid = new Int32Array(reusedCount); // maps seq index → mid index
1323
+ const seqToMid = new Int32Array(reusedCount);
269
1324
  let k = 0;
270
1325
  for (let i = 0; i < midLen; i++) {
271
1326
  if (oldIndices[i] !== -1) {
@@ -279,56 +1334,46 @@ function _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, di
279
1334
  inLIS[seqToMid[lisResult[i]]] = 1;
280
1335
  }
281
1336
  } else if (reusedCount === 1) {
282
- // Single reused item is trivially in LIS
283
1337
  for (let i = 0; i < midLen; i++) {
284
- if (oldIndices[i] !== -1) { inLIS[i] = 1; break; }
1338
+ if (oldIndices[i] !== -1) {
1339
+ inLIS[i] = 1;
1340
+ break;
1341
+ }
285
1342
  }
286
1343
  }
287
-
288
- // Create new items
289
1344
  for (let i = start; i <= newEnd; i++) {
290
1345
  if (!newMapped[i]) {
291
1346
  const item = newItems[i];
292
1347
  const idx = i;
293
- newMapped[i] = createRoot(dispose => {
1348
+ newMapped[i] = createRoot((dispose) => {
294
1349
  newDispose[idx] = dispose;
295
1350
  return mapFn(item, idx);
296
1351
  });
297
1352
  }
298
1353
  }
299
-
300
- // Position: work backwards from the item after newEnd (suffix start or endMarker)
301
- let nextSibling = newEnd + 1 < newMapped.length && newMapped[newEnd + 1]
302
- ? newMapped[newEnd + 1] : endMarker;
303
-
1354
+ let nextSibling = newEnd + 1 < newMapped.length && newMapped[newEnd + 1] ? newMapped[newEnd + 1] : endMarker;
304
1355
  for (let i = newEnd; i >= start; i--) {
305
1356
  const mi = i - start;
306
1357
  if (oldIndices[mi] === -1 || !inLIS[mi]) {
307
- // New item or moved item insert
1358
+ if (nextSibling && nextSibling.parentNode !== parent) nextSibling = endMarker;
308
1359
  parent.insertBefore(newMapped[i], nextSibling);
309
1360
  }
310
1361
  nextSibling = newMapped[i];
311
1362
  }
312
1363
  }
313
-
314
1364
  function _countNeg1(arr, len) {
315
1365
  let c = 0;
316
1366
  for (let i = 0; i < len; i++) if (arr[i] === -1) c++;
317
1367
  return c;
318
1368
  }
319
-
320
- // Longest Increasing Subsequence — returns indices into the input array.
321
- // O(n log n) using patience sorting. Uses typed arrays for performance.
322
1369
  function _lis(arr, len) {
323
1370
  if (len === 0) return [];
324
1371
  if (len === 1) return [0];
325
-
326
- const tails = new Int32Array(len); // indices into arr
1372
+ const tails = new Int32Array(len);
327
1373
  const predecessors = new Int32Array(len);
328
1374
  let tailLen = 1;
329
1375
  tails[0] = 0;
330
1376
  predecessors[0] = -1;
331
-
332
1377
  for (let i = 1; i < len; i++) {
333
1378
  if (arr[i] > arr[tails[tailLen - 1]]) {
334
1379
  predecessors[i] = tails[tailLen - 1];
@@ -336,7 +1381,7 @@ function _lis(arr, len) {
336
1381
  } else {
337
1382
  let lo = 0, hi = tailLen - 1;
338
1383
  while (lo < hi) {
339
- const mid = (lo + hi) >> 1;
1384
+ const mid = lo + hi >> 1;
340
1385
  if (arr[tails[mid]] < arr[i]) lo = mid + 1;
341
1386
  else hi = mid;
342
1387
  }
@@ -344,7 +1389,6 @@ function _lis(arr, len) {
344
1389
  predecessors[i] = lo > 0 ? tails[lo - 1] : -1;
345
1390
  }
346
1391
  }
347
-
348
1392
  const result = new Array(tailLen);
349
1393
  let k = tails[tailLen - 1];
350
1394
  for (let i = tailLen - 1; i >= 0; i--) {
@@ -353,33 +1397,24 @@ function _lis(arr, len) {
353
1397
  }
354
1398
  return result;
355
1399
  }
356
-
357
- // --- reconcileKeyed ---
358
- // Keyed reconciliation: tracks items by key function, not by reference.
359
- // When a key persists but its item reference changes, the item signal updates
360
- // in place — no DOM node destruction/creation. Only effects reading the
361
- // item accessor re-run (e.g., textContent update for changed label).
362
-
363
1400
  function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns, mapFn, keyFn, keyedState) {
364
1401
  const newLen = newItems.length;
365
1402
  const oldLen = oldItems.length;
366
-
367
- // --- Fast path: clear all ---
368
1403
  if (newLen === 0) {
369
1404
  if (oldLen > 0) {
370
- // Skip individual disposal: per-row effects only subscribe to their item signal,
371
- // which is also being discarded. Both become unreachable → GC collects them.
372
- // Bulk DOM removal: clear parent, re-add marker.
373
- parent.textContent = '';
374
- parent.appendChild(endMarker);
1405
+ for (let i = 0; i < oldLen; i++) {
1406
+ disposeFns[i]?.();
1407
+ if (mappedNodes[i]?.parentNode === parent) {
1408
+ disposeTree(mappedNodes[i]);
1409
+ parent.removeChild(mappedNodes[i]);
1410
+ }
1411
+ }
375
1412
  mappedNodes.length = 0;
376
1413
  disposeFns.length = 0;
377
1414
  if (keyedState) keyedState.clear();
378
1415
  }
379
1416
  return;
380
1417
  }
381
-
382
- // --- Fast path: all new ---
383
1418
  if (oldLen === 0) {
384
1419
  const frag = document.createDocumentFragment();
385
1420
  for (let i = 0; i < newLen; i++) {
@@ -392,9 +1427,9 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
392
1427
  accessor = itemSig;
393
1428
  keyedState.set(key, { itemSig });
394
1429
  } else {
395
- accessor = item; // raw mode: pass item directly
1430
+ accessor = item;
396
1431
  }
397
- const node = createRoot(dispose => {
1432
+ const node = createRoot((dispose) => {
398
1433
  disposeFns[idx] = dispose;
399
1434
  return mapFn(accessor, idx);
400
1435
  });
@@ -404,26 +1439,27 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
404
1439
  parent.insertBefore(frag, endMarker);
405
1440
  return;
406
1441
  }
407
-
408
- // --- Common prefix: skip matching keys at the start ---
409
1442
  let start = 0;
410
1443
  const minLen = Math.min(oldLen, newLen);
411
1444
  while (start < minLen) {
412
- // Fast path: same reference → same key, no update needed
413
- if (oldItems[start] === newItems[start]) { start++; continue; }
1445
+ if (oldItems[start] === newItems[start]) {
1446
+ start++;
1447
+ continue;
1448
+ }
414
1449
  const oldKey = keyFn(oldItems[start]);
415
1450
  const newKey = keyFn(newItems[start]);
416
1451
  if (oldKey !== newKey) break;
417
- // Key matches but reference changed — update signal (non-raw mode only)
418
1452
  if (keyedState) keyedState.get(oldKey).itemSig.set(newItems[start]);
419
1453
  start++;
420
1454
  }
421
-
422
- // --- Common suffix: skip matching keys at the end ---
423
1455
  let oldEnd = oldLen - 1;
424
1456
  let newEnd = newLen - 1;
425
1457
  while (oldEnd >= start && newEnd >= start) {
426
- if (oldItems[oldEnd] === newItems[newEnd]) { oldEnd--; newEnd--; continue; }
1458
+ if (oldItems[oldEnd] === newItems[newEnd]) {
1459
+ oldEnd--;
1460
+ newEnd--;
1461
+ continue;
1462
+ }
427
1463
  const oldKey = keyFn(oldItems[oldEnd]);
428
1464
  const newKey = keyFn(newItems[newEnd]);
429
1465
  if (oldKey !== newKey) break;
@@ -431,14 +1467,9 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
431
1467
  oldEnd--;
432
1468
  newEnd--;
433
1469
  }
434
-
435
- // If everything matched, nothing to do
436
1470
  if (start > oldEnd && start > newEnd) {
437
- // Just copy existing mappings to output
438
1471
  return;
439
1472
  }
440
-
441
- // Copy prefix/suffix into output arrays
442
1473
  const newMapped = new Array(newLen);
443
1474
  const newDispose = new Array(newLen);
444
1475
  for (let i = 0; i < start; i++) {
@@ -450,11 +1481,8 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
450
1481
  newMapped[i] = mappedNodes[oldI];
451
1482
  newDispose[i] = disposeFns[oldI];
452
1483
  }
453
-
454
1484
  const midNewLen = newEnd - start + 1;
455
1485
  const midOldLen = oldEnd - start + 1;
456
-
457
- // --- Only additions in middle ---
458
1486
  if (midOldLen === 0) {
459
1487
  const marker = newEnd + 1 < newLen && newMapped[newEnd + 1] ? newMapped[newEnd + 1] : endMarker;
460
1488
  const frag = document.createDocumentFragment();
@@ -470,7 +1498,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
470
1498
  } else {
471
1499
  accessor = item;
472
1500
  }
473
- newMapped[i] = createRoot(dispose => {
1501
+ newMapped[i] = createRoot((dispose) => {
474
1502
  newDispose[idx] = dispose;
475
1503
  return mapFn(accessor, idx);
476
1504
  });
@@ -480,8 +1508,6 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
480
1508
  _copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
481
1509
  return;
482
1510
  }
483
-
484
- // --- Only removals in middle ---
485
1511
  if (midNewLen === 0) {
486
1512
  for (let i = start; i <= oldEnd; i++) {
487
1513
  disposeFns[i]?.();
@@ -491,41 +1517,30 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
491
1517
  _copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
492
1518
  return;
493
1519
  }
494
-
495
- // --- General case: reconcile middle section ---
496
- // Build old key → old index map for middle section only
497
- const oldKeyMap = new Map();
1520
+ const oldKeyMap = /* @__PURE__ */ new Map();
498
1521
  for (let i = start; i <= oldEnd; i++) {
499
1522
  oldKeyMap.set(keyFn(oldItems[i]), i);
500
1523
  }
501
-
502
1524
  const oldIndices = new Int32Array(midNewLen);
503
1525
  oldIndices.fill(-1);
504
-
505
- // Match by key
506
1526
  for (let i = start; i <= newEnd; i++) {
507
1527
  const key = keyFn(newItems[i]);
508
1528
  const oldIdx = oldKeyMap.get(key);
509
- if (oldIdx !== undefined) {
1529
+ if (oldIdx !== void 0) {
510
1530
  oldKeyMap.delete(key);
511
1531
  newMapped[i] = mappedNodes[oldIdx];
512
1532
  newDispose[i] = disposeFns[oldIdx];
513
1533
  oldIndices[i - start] = oldIdx;
514
- // Update item signal if reference changed (non-raw mode only)
515
1534
  if (keyedState && newItems[i] !== oldItems[oldIdx]) {
516
1535
  keyedState.get(key).itemSig.set(newItems[i]);
517
1536
  }
518
1537
  }
519
1538
  }
520
-
521
- // Dispose removed items
522
1539
  for (const [key, oldIdx] of oldKeyMap) {
523
1540
  disposeFns[oldIdx]?.();
524
1541
  if (mappedNodes[oldIdx]?.parentNode) parent.removeChild(mappedNodes[oldIdx]);
525
1542
  if (keyedState) keyedState.delete(key);
526
1543
  }
527
-
528
- // Create new items
529
1544
  for (let i = start; i <= newEnd; i++) {
530
1545
  if (!newMapped[i]) {
531
1546
  const item = newItems[i];
@@ -539,15 +1554,12 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
539
1554
  } else {
540
1555
  accessor = item;
541
1556
  }
542
- newMapped[i] = createRoot(dispose => {
1557
+ newMapped[i] = createRoot((dispose) => {
543
1558
  newDispose[idx] = dispose;
544
1559
  return mapFn(accessor, idx);
545
1560
  });
546
1561
  }
547
1562
  }
548
-
549
- // Position using LIS
550
- // First check: are reused items already in order? (common for update-in-place)
551
1563
  let reusedCount = 0;
552
1564
  let alreadySorted = true;
553
1565
  let lastOldIdx = -1;
@@ -558,11 +1570,8 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
558
1570
  lastOldIdx = oldIndices[i];
559
1571
  }
560
1572
  }
561
-
562
1573
  const inLIS = new Uint8Array(midNewLen);
563
-
564
1574
  if (alreadySorted) {
565
- // All reused items are in order — mark all as in LIS (no moves needed)
566
1575
  for (let i = 0; i < midNewLen; i++) {
567
1576
  if (oldIndices[i] !== -1) inLIS[i] = 1;
568
1577
  }
@@ -583,25 +1592,23 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
583
1592
  }
584
1593
  } else if (reusedCount === 1) {
585
1594
  for (let i = 0; i < midNewLen; i++) {
586
- if (oldIndices[i] !== -1) { inLIS[i] = 1; break; }
1595
+ if (oldIndices[i] !== -1) {
1596
+ inLIS[i] = 1;
1597
+ break;
1598
+ }
587
1599
  }
588
1600
  }
589
-
590
- // Position: work backwards, insert items not in LIS
591
- let nextSibling = newEnd + 1 < newMapped.length && newMapped[newEnd + 1]
592
- ? newMapped[newEnd + 1] : endMarker;
593
-
1601
+ let nextSibling = newEnd + 1 < newMapped.length && newMapped[newEnd + 1] ? newMapped[newEnd + 1] : endMarker;
594
1602
  for (let i = newEnd; i >= start; i--) {
595
1603
  const mi = i - start;
596
1604
  if (oldIndices[mi] === -1 || !inLIS[mi]) {
1605
+ if (nextSibling && nextSibling.parentNode !== parent) nextSibling = endMarker;
597
1606
  parent.insertBefore(newMapped[i], nextSibling);
598
1607
  }
599
1608
  nextSibling = newMapped[i];
600
1609
  }
601
-
602
1610
  _copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
603
1611
  }
604
-
605
1612
  function _copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen) {
606
1613
  mappedNodes.length = newLen;
607
1614
  disposeFns.length = newLen;
@@ -610,58 +1617,71 @@ function _copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen) {
610
1617
  disposeFns[i] = newDispose[i];
611
1618
  }
612
1619
  }
613
-
614
- // --- spread(el, props) ---
615
- // Fine-grained prop effects. Function props create individual effects.
616
- // Event props use direct assignment.
617
-
618
- export function spread(el, props) {
1620
+ function spread(el, props) {
619
1621
  for (const key in props) {
620
1622
  const value = props[key];
621
-
622
- if (key.startsWith('on') && key.length > 2) {
623
- // Event handler — direct assignment. Use $$name for delegated events.
1623
+ if (key.startsWith("on") && key.length > 2) {
624
1624
  const event = key.slice(2).toLowerCase();
625
1625
  el.addEventListener(event, value);
626
1626
  continue;
627
1627
  }
628
-
629
- if (typeof value === 'function' && !key.startsWith('on')) {
630
- // Reactive prop — create micro-effect
631
- if (key === 'class' || key === 'className') {
632
- effect(() => { el.className = value() || ''; });
633
- } else if (key === 'style' && typeof value() === 'object') {
1628
+ if (typeof value === "function" && !key.startsWith("on")) {
1629
+ if (key === "class" || key === "className") {
1630
+ effect(() => {
1631
+ el.className = value() || "";
1632
+ });
1633
+ } else if (key === "style" && typeof value() === "object") {
634
1634
  effect(() => {
635
1635
  const styles = value();
636
1636
  for (const prop in styles) {
637
- el.style[prop] = styles[prop] ?? '';
1637
+ el.style[prop] = styles[prop] ?? "";
638
1638
  }
639
1639
  });
640
1640
  } else {
641
- effect(() => { setPropDirect(el, key, value()); });
1641
+ effect(() => {
1642
+ setProp2(el, key, value());
1643
+ });
642
1644
  }
643
1645
  } else {
644
- // Static prop
645
- setPropDirect(el, key, value);
1646
+ setProp2(el, key, value);
646
1647
  }
647
1648
  }
648
1649
  }
649
-
650
- function setPropDirect(el, key, value) {
651
- if (key === 'class' || key === 'className') {
652
- el.className = value || '';
653
- } else if (key === 'style') {
654
- if (typeof value === 'string') {
1650
+ function setProp2(el, key, value) {
1651
+ if (URL_ATTRS.has(key) || URL_ATTRS.has(key.toLowerCase())) {
1652
+ if (!isSafeUrl(value)) {
1653
+ if (typeof console !== "undefined") {
1654
+ console.warn(`[what] Blocked unsafe URL in "${key}" attribute: ${value}`);
1655
+ }
1656
+ return;
1657
+ }
1658
+ }
1659
+ if (key === "class" || key === "className") {
1660
+ el.className = value || "";
1661
+ } else if (key === "dangerouslySetInnerHTML") {
1662
+ el.innerHTML = value?.__html ?? "";
1663
+ } else if (key === "innerHTML") {
1664
+ if (value && typeof value === "object" && "__html" in value) {
1665
+ el.innerHTML = value.__html ?? "";
1666
+ } else {
1667
+ if (typeof console !== "undefined" && value != null && value !== "") {
1668
+ console.warn(
1669
+ '[what] Plain string innerHTML is not allowed. Use { __html: "..." } or dangerouslySetInnerHTML={{ __html: "..." }} instead.'
1670
+ );
1671
+ }
1672
+ }
1673
+ } else if (key === "style") {
1674
+ if (typeof value === "string") {
655
1675
  el.style.cssText = value;
656
- } else if (typeof value === 'object') {
1676
+ } else if (typeof value === "object") {
657
1677
  for (const prop in value) {
658
- el.style[prop] = value[prop] ?? '';
1678
+ el.style[prop] = value[prop] ?? "";
659
1679
  }
660
1680
  }
661
- } else if (key.startsWith('data-') || key.startsWith('aria-')) {
1681
+ } else if (key.startsWith("data-") || key.startsWith("aria-")) {
662
1682
  el.setAttribute(key, value);
663
- } else if (typeof value === 'boolean') {
664
- if (value) el.setAttribute(key, '');
1683
+ } else if (typeof value === "boolean") {
1684
+ if (value) el.setAttribute(key, "");
665
1685
  else el.removeAttribute(key);
666
1686
  } else if (key in el) {
667
1687
  el[key] = value;
@@ -669,24 +1689,14 @@ function setPropDirect(el, key, value) {
669
1689
  el.setAttribute(key, value);
670
1690
  }
671
1691
  }
672
-
673
- // --- delegateEvents(eventNames) ---
674
- // Event delegation: common events handled at document level.
675
- // Handlers stored as el.$$click, el.$$input, etc.
676
- // Single listener per event type on document — reduces listener count from N to 1.
677
-
678
- const delegatedEvents = new Set();
679
-
680
- export function delegateEvents(eventNames) {
1692
+ var delegatedEvents = /* @__PURE__ */ new Set();
1693
+ function delegateEvents(eventNames) {
681
1694
  for (const name of eventNames) {
682
1695
  if (delegatedEvents.has(name)) continue;
683
1696
  delegatedEvents.add(name);
684
-
685
1697
  document.addEventListener(name, (e) => {
686
1698
  let node = e.target;
687
- const key = '$$' + name;
688
-
689
- // Walk up the DOM tree looking for handlers
1699
+ const key = "$$" + name;
690
1700
  while (node) {
691
1701
  const handler = node[key];
692
1702
  if (handler) {
@@ -698,19 +1708,238 @@ export function delegateEvents(eventNames) {
698
1708
  });
699
1709
  }
700
1710
  }
701
-
702
- // --- addEventListener helper for non-delegated events ---
703
- export function on(el, event, handler) {
1711
+ function on(el, event, handler) {
704
1712
  el.addEventListener(event, handler);
705
1713
  return () => el.removeEventListener(event, handler);
706
1714
  }
707
-
708
- // --- className helper for conditional classes ---
709
- export function classList(el, classes) {
1715
+ function classList(el, classes) {
710
1716
  effect(() => {
711
1717
  for (const name in classes) {
712
- const value = typeof classes[name] === 'function' ? classes[name]() : classes[name];
1718
+ const value = typeof classes[name] === "function" ? classes[name]() : classes[name];
713
1719
  el.classList.toggle(name, !!value);
714
1720
  }
715
1721
  });
716
1722
  }
1723
+ var _isHydrating = false;
1724
+ var _hydrationCursor = null;
1725
+ function isHydrating() {
1726
+ return _isHydrating;
1727
+ }
1728
+ function hydrate(vnode, container) {
1729
+ _isHydrating = true;
1730
+ _hydrationCursor = { parent: container, index: 0 };
1731
+ try {
1732
+ const result = hydrateNode(vnode, container);
1733
+ return result;
1734
+ } finally {
1735
+ _isHydrating = false;
1736
+ _hydrationCursor = null;
1737
+ }
1738
+ }
1739
+ function claimNode(parent) {
1740
+ const children = parent.childNodes;
1741
+ while (_hydrationCursor.index < children.length) {
1742
+ const node = children[_hydrationCursor.index];
1743
+ if (node.nodeType === 8) {
1744
+ const text = node.textContent;
1745
+ if (text === "$" || text === "/$" || text === "[]" || text === "/[]") {
1746
+ _hydrationCursor.index++;
1747
+ continue;
1748
+ }
1749
+ }
1750
+ _hydrationCursor.index++;
1751
+ return node;
1752
+ }
1753
+ return null;
1754
+ }
1755
+ function isDevMode() {
1756
+ return typeof process !== "undefined" && true;
1757
+ }
1758
+ function hydrateNode(vnode, parent) {
1759
+ if (vnode == null || typeof vnode === "boolean") {
1760
+ return null;
1761
+ }
1762
+ if (typeof vnode === "string" || typeof vnode === "number") {
1763
+ const existing = claimNode(parent);
1764
+ const text = String(vnode);
1765
+ if (existing && existing.nodeType === 3) {
1766
+ if (isDevMode() && existing.textContent !== text) {
1767
+ console.warn(
1768
+ `[what] Hydration mismatch: expected text "${text}", got "${existing.textContent}"`
1769
+ );
1770
+ existing.textContent = text;
1771
+ }
1772
+ return existing;
1773
+ }
1774
+ if (isDevMode()) {
1775
+ console.warn(
1776
+ `[what] Hydration mismatch: expected text node "${text}", got ${existing ? existing.nodeName : "nothing"}. Falling back to client render.`
1777
+ );
1778
+ }
1779
+ const textNode2 = document.createTextNode(text);
1780
+ if (existing) {
1781
+ parent.replaceChild(textNode2, existing);
1782
+ } else {
1783
+ parent.appendChild(textNode2);
1784
+ }
1785
+ return textNode2;
1786
+ }
1787
+ if (typeof vnode === "function") {
1788
+ const initialValue = vnode();
1789
+ let current = hydrateNode(initialValue, parent);
1790
+ effect(() => {
1791
+ const value = vnode();
1792
+ if (!_isHydrating) {
1793
+ current = reconcileInsert(parent, value, current, null);
1794
+ }
1795
+ });
1796
+ return current;
1797
+ }
1798
+ if (Array.isArray(vnode)) {
1799
+ const nodes = [];
1800
+ for (const child of vnode) {
1801
+ const node = hydrateNode(child, parent);
1802
+ if (node) nodes.push(node);
1803
+ }
1804
+ return nodes.length === 1 ? nodes[0] : nodes;
1805
+ }
1806
+ if (typeof vnode === "object" && vnode._vnode) {
1807
+ if (typeof vnode.tag === "function") {
1808
+ const componentStack2 = getComponentStack();
1809
+ const Component = vnode.tag;
1810
+ const props = vnode.props || {};
1811
+ const children = vnode.children || [];
1812
+ const ctx = {
1813
+ hooks: [],
1814
+ hookIndex: 0,
1815
+ effects: [],
1816
+ cleanups: [],
1817
+ mounted: false,
1818
+ disposed: false,
1819
+ Component,
1820
+ _parentCtx: componentStack2[componentStack2.length - 1] || null,
1821
+ _errorBoundary: null
1822
+ };
1823
+ componentStack2.push(ctx);
1824
+ let result;
1825
+ try {
1826
+ const propsChildren = children.length === 0 ? void 0 : children.length === 1 ? children[0] : children;
1827
+ result = Component({ ...props, children: propsChildren });
1828
+ } catch (error) {
1829
+ componentStack2.pop();
1830
+ console.error("[what] Error in component during hydration:", Component.name || "Anonymous", error);
1831
+ return null;
1832
+ }
1833
+ componentStack2.pop();
1834
+ ctx.mounted = true;
1835
+ if (ctx._mountCallbacks) {
1836
+ queueMicrotask(() => {
1837
+ if (ctx.disposed) return;
1838
+ for (const fn of ctx._mountCallbacks) {
1839
+ try {
1840
+ fn();
1841
+ } catch (e) {
1842
+ console.error("[what] onMount error:", e);
1843
+ }
1844
+ }
1845
+ });
1846
+ }
1847
+ return hydrateNode(result, parent);
1848
+ }
1849
+ const existing = claimNode(parent);
1850
+ const expectedTag = vnode.tag.toUpperCase();
1851
+ if (existing && existing.nodeType === 1 && existing.nodeName === expectedTag) {
1852
+ hydrateElementProps(existing, vnode.props || {});
1853
+ const savedCursor = _hydrationCursor;
1854
+ _hydrationCursor = { parent: existing, index: 0 };
1855
+ const rawInner = vnode.props?.dangerouslySetInnerHTML?.__html;
1856
+ if (rawInner == null) {
1857
+ for (const child of vnode.children) {
1858
+ hydrateNode(child, existing);
1859
+ }
1860
+ }
1861
+ _hydrationCursor = savedCursor;
1862
+ return existing;
1863
+ }
1864
+ if (isDevMode()) {
1865
+ console.warn(
1866
+ `[what] Hydration mismatch: expected <${vnode.tag}>, got ${existing ? existing.nodeName : "nothing"}. Falling back to client render.`
1867
+ );
1868
+ }
1869
+ const newEl = document.createElement(vnode.tag);
1870
+ for (const key in vnode.props || {}) {
1871
+ if (key === "children" || key === "key") continue;
1872
+ setProp2(newEl, key, vnode.props[key]);
1873
+ }
1874
+ for (const child of vnode.children) {
1875
+ reconcileInsert(newEl, child, null, null);
1876
+ }
1877
+ if (existing) {
1878
+ parent.replaceChild(newEl, existing);
1879
+ } else {
1880
+ parent.appendChild(newEl);
1881
+ }
1882
+ return newEl;
1883
+ }
1884
+ if (isDomNode2(vnode)) {
1885
+ return vnode;
1886
+ }
1887
+ const textNode = document.createTextNode(String(vnode));
1888
+ parent.appendChild(textNode);
1889
+ return textNode;
1890
+ }
1891
+ function hydrateElementProps(el, props) {
1892
+ for (const key in props) {
1893
+ if (key === "children" || key === "key" || key === "ref") continue;
1894
+ if (key === "dangerouslySetInnerHTML" || key === "innerHTML") continue;
1895
+ const value = props[key];
1896
+ if (key.startsWith("on") && key.length > 2) {
1897
+ const event = key.slice(2).toLowerCase();
1898
+ el.addEventListener(event, value);
1899
+ continue;
1900
+ }
1901
+ if (key.startsWith("$$")) {
1902
+ el[key] = value;
1903
+ continue;
1904
+ }
1905
+ if (typeof value === "function" && !key.startsWith("on")) {
1906
+ if (key === "class" || key === "className") {
1907
+ effect(() => {
1908
+ el.className = value() || "";
1909
+ });
1910
+ } else if (key === "style" && typeof value() === "object") {
1911
+ effect(() => {
1912
+ const styles = value();
1913
+ for (const prop in styles) {
1914
+ el.style[prop] = styles[prop] ?? "";
1915
+ }
1916
+ });
1917
+ } else {
1918
+ effect(() => {
1919
+ setProp2(el, key, value());
1920
+ });
1921
+ }
1922
+ continue;
1923
+ }
1924
+ if (key === "data-hk") continue;
1925
+ }
1926
+ }
1927
+ export {
1928
+ _$createComponent,
1929
+ _$templateImpl as _$template,
1930
+ template as _template,
1931
+ classList,
1932
+ delegateEvents,
1933
+ effect,
1934
+ hydrate,
1935
+ insert,
1936
+ isHydrating,
1937
+ mapArray,
1938
+ on,
1939
+ setProp2 as setProp,
1940
+ spread,
1941
+ svgTemplate,
1942
+ template,
1943
+ untrack
1944
+ };
1945
+ //# sourceMappingURL=render.js.map