@pyreon/runtime-dom 0.23.0 → 0.24.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.
@@ -364,7 +364,8 @@ function mountReactive(accessor, parent, anchor, mount) {
364
364
  hasCleanup = false;
365
365
  const value = accessor();
366
366
  if (value != null && value !== false) {
367
- const cleanup = runUntracked(() => restoreContextStack(contextSnapshot, () => mount(value, parent, marker)));
367
+ const liveParent = marker.parentNode ?? parent;
368
+ const cleanup = runUntracked(() => restoreContextStack(contextSnapshot, () => mount(value, liveParent, marker)));
368
369
  if (myGen === generation) {
369
370
  currentCleanup = cleanup;
370
371
  hasCleanup = true;
@@ -483,15 +484,15 @@ function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
483
484
  curPos.delete(key);
484
485
  }
485
486
  };
486
- const mountNewEntries = (newList) => {
487
+ const mountNewEntries = (newList, liveParent) => {
487
488
  for (const vnode of newList) {
488
489
  const key = vnode.key;
489
490
  if (key === null || key === void 0) continue;
490
491
  if (cache.has(key)) continue;
491
492
  const anchor = document.createComment("");
492
493
  _keyedAnchors.add(anchor);
493
- parent.insertBefore(anchor, tailMarker);
494
- const cleanup = mountVNode(vnode, parent, tailMarker);
494
+ liveParent.insertBefore(anchor, tailMarker);
495
+ const cleanup = mountVNode(vnode, liveParent, tailMarker);
495
496
  cache.set(key, {
496
497
  anchor,
497
498
  cleanup
@@ -502,6 +503,7 @@ function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
502
503
  const newList = accessor();
503
504
  const n = newList.length;
504
505
  runUntracked(() => {
506
+ const liveParent = tailMarker.parentNode ?? parent;
505
507
  if (n === 0 && cache.size > 0) {
506
508
  for (const entry of cache.values()) {
507
509
  _emitCleanup();
@@ -515,8 +517,8 @@ function mountKeyedList(accessor, parent, listAnchor, mountVNode) {
515
517
  }
516
518
  const { newKeyOrder, newKeySet } = collectKeyOrder(newList);
517
519
  removeStaleEntries(newKeySet);
518
- mountNewEntries(newList);
519
- if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker);
520
+ mountNewEntries(newList, liveParent);
521
+ if (currentKeyOrder.length > 0 && n > 0) lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, liveParent, tailMarker);
520
522
  curPos.clear();
521
523
  for (let i = 0; i < newKeyOrder.length; i++) {
522
524
  const k = newKeyOrder[i];
@@ -1654,4 +1656,4 @@ nativeCompat(KeepAlive);
1654
1656
 
1655
1657
  //#endregion
1656
1658
  export { applyProps as a, mountReactive as c, delegatedPropName as d, setupDelegation as f, applyProp as i, installDevTools as l, mountChild as n, sanitizeHtml as o, _bindEvent as r, setSanitizer as s, KeepAlive as t, DELEGATED_EVENTS as u };
1657
- //# sourceMappingURL=keep-alive-BM7bn3W9.js.map
1659
+ //# sourceMappingURL=keep-alive-DznjF_h1.js.map
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"b8baa038-1","name":"hydration-debug.ts"},{"uid":"b8baa038-3","name":"hydrate.ts"},{"uid":"b8baa038-5","name":"template.ts"},{"uid":"b8baa038-7","name":"transition-group.ts"},{"uid":"b8baa038-9","name":"index.ts"}]}]},{"name":"keep-alive-entry.js","uid":"b8baa038-11"},{"name":"transition-entry.js","children":[{"name":"src/transition.ts","uid":"b8baa038-13"}]},{"name":"_chunks/keep-alive-BM7bn3W9.js","children":[{"name":"src","children":[{"uid":"b8baa038-15","name":"delegate.ts"},{"uid":"b8baa038-17","name":"devtools.ts"},{"uid":"b8baa038-19","name":"nodes.ts"},{"uid":"b8baa038-21","name":"props.ts"},{"uid":"b8baa038-23","name":"mount.ts"},{"uid":"b8baa038-24","name":"keep-alive.ts"}]}]}],"isRoot":true},"nodeParts":{"b8baa038-1":{"renderedLength":1395,"gzipLength":718,"brotliLength":0,"metaUid":"b8baa038-0"},"b8baa038-3":{"renderedLength":8312,"gzipLength":2472,"brotliLength":0,"metaUid":"b8baa038-2"},"b8baa038-5":{"renderedLength":9693,"gzipLength":3748,"brotliLength":0,"metaUid":"b8baa038-4"},"b8baa038-7":{"renderedLength":8002,"gzipLength":2092,"brotliLength":0,"metaUid":"b8baa038-6"},"b8baa038-9":{"renderedLength":985,"gzipLength":549,"brotliLength":0,"metaUid":"b8baa038-8"},"b8baa038-11":{"id":"keep-alive-entry.js","gzipLength":93,"brotliLength":0,"renderedLength":89,"metaUid":"b8baa038-10"},"b8baa038-13":{"renderedLength":4925,"gzipLength":1407,"brotliLength":0,"metaUid":"b8baa038-12"},"b8baa038-15":{"renderedLength":2090,"gzipLength":1029,"brotliLength":0,"metaUid":"b8baa038-14"},"b8baa038-17":{"renderedLength":8163,"gzipLength":2538,"brotliLength":0,"metaUid":"b8baa038-16"},"b8baa038-19":{"renderedLength":17556,"gzipLength":4658,"brotliLength":0,"metaUid":"b8baa038-18"},"b8baa038-21":{"renderedLength":9054,"gzipLength":3495,"brotliLength":0,"metaUid":"b8baa038-20"},"b8baa038-23":{"renderedLength":12162,"gzipLength":3873,"brotliLength":0,"metaUid":"b8baa038-22"},"b8baa038-24":{"renderedLength":1518,"gzipLength":724,"brotliLength":0,"metaUid":"b8baa038-10"}},"nodeMetas":{"b8baa038-0":{"id":"/src/hydration-debug.ts","moduleParts":{"index.js":"b8baa038-1"},"imported":[],"importedBy":[{"uid":"b8baa038-8"},{"uid":"b8baa038-2"}]},"b8baa038-2":{"id":"/src/hydrate.ts","moduleParts":{"index.js":"b8baa038-3"},"imported":[{"uid":"b8baa038-26"},{"uid":"b8baa038-25"},{"uid":"b8baa038-14"},{"uid":"b8baa038-0"},{"uid":"b8baa038-22"},{"uid":"b8baa038-18"},{"uid":"b8baa038-20"}],"importedBy":[{"uid":"b8baa038-8"}]},"b8baa038-4":{"id":"/src/template.ts","moduleParts":{"index.js":"b8baa038-5"},"imported":[{"uid":"b8baa038-25"},{"uid":"b8baa038-22"},{"uid":"b8baa038-20"}],"importedBy":[{"uid":"b8baa038-8"}]},"b8baa038-6":{"id":"/src/transition-group.ts","moduleParts":{"index.js":"b8baa038-7"},"imported":[{"uid":"b8baa038-26"},{"uid":"b8baa038-25"},{"uid":"b8baa038-22"}],"importedBy":[{"uid":"b8baa038-8"}]},"b8baa038-8":{"id":"/src/index.ts","moduleParts":{"index.js":"b8baa038-9"},"imported":[{"uid":"b8baa038-14"},{"uid":"b8baa038-2"},{"uid":"b8baa038-0"},{"uid":"b8baa038-10"},{"uid":"b8baa038-22"},{"uid":"b8baa038-20"},{"uid":"b8baa038-4"},{"uid":"b8baa038-12"},{"uid":"b8baa038-6"},{"uid":"b8baa038-16"}],"importedBy":[],"isEntry":true},"b8baa038-10":{"id":"/src/keep-alive.ts","moduleParts":{"keep-alive-entry.js":"b8baa038-11","_chunks/keep-alive-BM7bn3W9.js":"b8baa038-24"},"imported":[{"uid":"b8baa038-26"},{"uid":"b8baa038-25"},{"uid":"b8baa038-22"}],"importedBy":[{"uid":"b8baa038-8"}],"isEntry":true},"b8baa038-12":{"id":"/src/transition.ts","moduleParts":{"transition-entry.js":"b8baa038-13"},"imported":[{"uid":"b8baa038-26"},{"uid":"b8baa038-25"}],"importedBy":[{"uid":"b8baa038-8"}],"isEntry":true},"b8baa038-14":{"id":"/src/delegate.ts","moduleParts":{"_chunks/keep-alive-BM7bn3W9.js":"b8baa038-15"},"imported":[{"uid":"b8baa038-25"}],"importedBy":[{"uid":"b8baa038-8"},{"uid":"b8baa038-2"},{"uid":"b8baa038-20"}]},"b8baa038-16":{"id":"/src/devtools.ts","moduleParts":{"_chunks/keep-alive-BM7bn3W9.js":"b8baa038-17"},"imported":[{"uid":"b8baa038-25"}],"importedBy":[{"uid":"b8baa038-8"},{"uid":"b8baa038-22"}]},"b8baa038-18":{"id":"/src/nodes.ts","moduleParts":{"_chunks/keep-alive-BM7bn3W9.js":"b8baa038-19"},"imported":[{"uid":"b8baa038-26"},{"uid":"b8baa038-25"}],"importedBy":[{"uid":"b8baa038-2"},{"uid":"b8baa038-22"}]},"b8baa038-20":{"id":"/src/props.ts","moduleParts":{"_chunks/keep-alive-BM7bn3W9.js":"b8baa038-21"},"imported":[{"uid":"b8baa038-26"},{"uid":"b8baa038-25"},{"uid":"b8baa038-14"}],"importedBy":[{"uid":"b8baa038-8"},{"uid":"b8baa038-2"},{"uid":"b8baa038-22"},{"uid":"b8baa038-4"}]},"b8baa038-22":{"id":"/src/mount.ts","moduleParts":{"_chunks/keep-alive-BM7bn3W9.js":"b8baa038-23"},"imported":[{"uid":"b8baa038-26"},{"uid":"b8baa038-25"},{"uid":"b8baa038-16"},{"uid":"b8baa038-18"},{"uid":"b8baa038-20"}],"importedBy":[{"uid":"b8baa038-8"},{"uid":"b8baa038-2"},{"uid":"b8baa038-10"},{"uid":"b8baa038-4"},{"uid":"b8baa038-6"}]},"b8baa038-25":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"b8baa038-14"},{"uid":"b8baa038-2"},{"uid":"b8baa038-10"},{"uid":"b8baa038-22"},{"uid":"b8baa038-20"},{"uid":"b8baa038-4"},{"uid":"b8baa038-12"},{"uid":"b8baa038-6"},{"uid":"b8baa038-16"},{"uid":"b8baa038-18"}]},"b8baa038-26":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"b8baa038-2"},{"uid":"b8baa038-10"},{"uid":"b8baa038-22"},{"uid":"b8baa038-20"},{"uid":"b8baa038-12"},{"uid":"b8baa038-6"},{"uid":"b8baa038-18"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"baaa0631-1","name":"hydration-debug.ts"},{"uid":"baaa0631-3","name":"hydrate.ts"},{"uid":"baaa0631-5","name":"template.ts"},{"uid":"baaa0631-7","name":"transition-group.ts"},{"uid":"baaa0631-9","name":"index.ts"}]}]},{"name":"keep-alive-entry.js","uid":"baaa0631-11"},{"name":"transition-entry.js","children":[{"name":"src/transition.ts","uid":"baaa0631-13"}]},{"name":"_chunks/keep-alive-DznjF_h1.js","children":[{"name":"src","children":[{"uid":"baaa0631-15","name":"delegate.ts"},{"uid":"baaa0631-17","name":"devtools.ts"},{"uid":"baaa0631-19","name":"nodes.ts"},{"uid":"baaa0631-21","name":"props.ts"},{"uid":"baaa0631-23","name":"mount.ts"},{"uid":"baaa0631-24","name":"keep-alive.ts"}]}]}],"isRoot":true},"nodeParts":{"baaa0631-1":{"renderedLength":1395,"gzipLength":718,"brotliLength":0,"metaUid":"baaa0631-0"},"baaa0631-3":{"renderedLength":8312,"gzipLength":2472,"brotliLength":0,"metaUid":"baaa0631-2"},"baaa0631-5":{"renderedLength":15703,"gzipLength":5438,"brotliLength":0,"metaUid":"baaa0631-4"},"baaa0631-7":{"renderedLength":8002,"gzipLength":2092,"brotliLength":0,"metaUid":"baaa0631-6"},"baaa0631-9":{"renderedLength":985,"gzipLength":549,"brotliLength":0,"metaUid":"baaa0631-8"},"baaa0631-11":{"id":"keep-alive-entry.js","gzipLength":93,"brotliLength":0,"renderedLength":89,"metaUid":"baaa0631-10"},"baaa0631-13":{"renderedLength":4925,"gzipLength":1407,"brotliLength":0,"metaUid":"baaa0631-12"},"baaa0631-15":{"renderedLength":2090,"gzipLength":1029,"brotliLength":0,"metaUid":"baaa0631-14"},"baaa0631-17":{"renderedLength":8163,"gzipLength":2538,"brotliLength":0,"metaUid":"baaa0631-16"},"baaa0631-19":{"renderedLength":17702,"gzipLength":4689,"brotliLength":0,"metaUid":"baaa0631-18"},"baaa0631-21":{"renderedLength":9054,"gzipLength":3495,"brotliLength":0,"metaUid":"baaa0631-20"},"baaa0631-23":{"renderedLength":12162,"gzipLength":3873,"brotliLength":0,"metaUid":"baaa0631-22"},"baaa0631-24":{"renderedLength":1518,"gzipLength":724,"brotliLength":0,"metaUid":"baaa0631-10"}},"nodeMetas":{"baaa0631-0":{"id":"/src/hydration-debug.ts","moduleParts":{"index.js":"baaa0631-1"},"imported":[],"importedBy":[{"uid":"baaa0631-8"},{"uid":"baaa0631-2"}]},"baaa0631-2":{"id":"/src/hydrate.ts","moduleParts":{"index.js":"baaa0631-3"},"imported":[{"uid":"baaa0631-26"},{"uid":"baaa0631-25"},{"uid":"baaa0631-14"},{"uid":"baaa0631-0"},{"uid":"baaa0631-22"},{"uid":"baaa0631-18"},{"uid":"baaa0631-20"}],"importedBy":[{"uid":"baaa0631-8"}]},"baaa0631-4":{"id":"/src/template.ts","moduleParts":{"index.js":"baaa0631-5"},"imported":[{"uid":"baaa0631-25"},{"uid":"baaa0631-22"},{"uid":"baaa0631-20"}],"importedBy":[{"uid":"baaa0631-8"}]},"baaa0631-6":{"id":"/src/transition-group.ts","moduleParts":{"index.js":"baaa0631-7"},"imported":[{"uid":"baaa0631-26"},{"uid":"baaa0631-25"},{"uid":"baaa0631-22"}],"importedBy":[{"uid":"baaa0631-8"}]},"baaa0631-8":{"id":"/src/index.ts","moduleParts":{"index.js":"baaa0631-9"},"imported":[{"uid":"baaa0631-14"},{"uid":"baaa0631-2"},{"uid":"baaa0631-0"},{"uid":"baaa0631-10"},{"uid":"baaa0631-22"},{"uid":"baaa0631-20"},{"uid":"baaa0631-4"},{"uid":"baaa0631-12"},{"uid":"baaa0631-6"},{"uid":"baaa0631-16"}],"importedBy":[],"isEntry":true},"baaa0631-10":{"id":"/src/keep-alive.ts","moduleParts":{"keep-alive-entry.js":"baaa0631-11","_chunks/keep-alive-DznjF_h1.js":"baaa0631-24"},"imported":[{"uid":"baaa0631-26"},{"uid":"baaa0631-25"},{"uid":"baaa0631-22"}],"importedBy":[{"uid":"baaa0631-8"}],"isEntry":true},"baaa0631-12":{"id":"/src/transition.ts","moduleParts":{"transition-entry.js":"baaa0631-13"},"imported":[{"uid":"baaa0631-26"},{"uid":"baaa0631-25"}],"importedBy":[{"uid":"baaa0631-8"}],"isEntry":true},"baaa0631-14":{"id":"/src/delegate.ts","moduleParts":{"_chunks/keep-alive-DznjF_h1.js":"baaa0631-15"},"imported":[{"uid":"baaa0631-25"}],"importedBy":[{"uid":"baaa0631-8"},{"uid":"baaa0631-2"},{"uid":"baaa0631-20"}]},"baaa0631-16":{"id":"/src/devtools.ts","moduleParts":{"_chunks/keep-alive-DznjF_h1.js":"baaa0631-17"},"imported":[{"uid":"baaa0631-25"}],"importedBy":[{"uid":"baaa0631-8"},{"uid":"baaa0631-22"}]},"baaa0631-18":{"id":"/src/nodes.ts","moduleParts":{"_chunks/keep-alive-DznjF_h1.js":"baaa0631-19"},"imported":[{"uid":"baaa0631-26"},{"uid":"baaa0631-25"}],"importedBy":[{"uid":"baaa0631-2"},{"uid":"baaa0631-22"}]},"baaa0631-20":{"id":"/src/props.ts","moduleParts":{"_chunks/keep-alive-DznjF_h1.js":"baaa0631-21"},"imported":[{"uid":"baaa0631-26"},{"uid":"baaa0631-25"},{"uid":"baaa0631-14"}],"importedBy":[{"uid":"baaa0631-8"},{"uid":"baaa0631-2"},{"uid":"baaa0631-22"},{"uid":"baaa0631-4"}]},"baaa0631-22":{"id":"/src/mount.ts","moduleParts":{"_chunks/keep-alive-DznjF_h1.js":"baaa0631-23"},"imported":[{"uid":"baaa0631-26"},{"uid":"baaa0631-25"},{"uid":"baaa0631-16"},{"uid":"baaa0631-18"},{"uid":"baaa0631-20"}],"importedBy":[{"uid":"baaa0631-8"},{"uid":"baaa0631-2"},{"uid":"baaa0631-10"},{"uid":"baaa0631-4"},{"uid":"baaa0631-6"}]},"baaa0631-25":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"baaa0631-14"},{"uid":"baaa0631-2"},{"uid":"baaa0631-10"},{"uid":"baaa0631-22"},{"uid":"baaa0631-20"},{"uid":"baaa0631-4"},{"uid":"baaa0631-12"},{"uid":"baaa0631-6"},{"uid":"baaa0631-16"},{"uid":"baaa0631-18"}]},"baaa0631-26":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"baaa0631-2"},{"uid":"baaa0631-10"},{"uid":"baaa0631-22"},{"uid":"baaa0631-20"},{"uid":"baaa0631-12"},{"uid":"baaa0631-6"},{"uid":"baaa0631-18"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
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";
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-DznjF_h1.js";
2
2
  import { Transition } from "./transition-entry.js";
3
3
  import { effect, effectScope, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
4
4
  import { ForSymbol, Fragment, PortalSymbol, createRef, dispatchToErrorBoundary, h, makeReactiveProps, nativeCompat, onMount, onUnmount, reportError, runWithHooks } from "@pyreon/core";
@@ -490,7 +490,142 @@ function _rsCollapseH(html, lightClass, darkClass, isDark, handlers, bind) {
490
490
  el.className = v ? darkClass : lightClass;
491
491
  });
492
492
  const handlerDisposers = [];
493
- for (const key in handlers) {
493
+ for (const key of Object.keys(handlers)) {
494
+ const d = _bindEvent(el, key, handlers[key]);
495
+ if (d) handlerDisposers.push(d);
496
+ }
497
+ const disposeChildren = bind ? bind(el) : null;
498
+ return () => {
499
+ disposeClass();
500
+ for (const d of handlerDisposers) d();
501
+ if (disposeChildren) disposeChildren();
502
+ };
503
+ });
504
+ }
505
+ /**
506
+ * Compiler-emitted DYNAMIC-prop collapsed rocketstyle call site — PR 1
507
+ * of the dynamic-prop partial-collapse build (next bite after the
508
+ * `on*`-handler partial-collapse `_rsCollapseH`, `.claude/plans/open-work-2026-q3.md`
509
+ * → #1 dynamic-prop bucket = 15.3% of all real-corpus sites).
510
+ *
511
+ * Generalises {@link _rsCollapse}'s 2-class (light/dark) dispatch to an
512
+ * N-class dispatch for sites where one dimension prop is an enumerable
513
+ * dynamic expression (e.g. `<Button state={cond ? 'primary' : 'secondary'}>`).
514
+ * The compiler resolves EVERY value of that prop through the existing
515
+ * SSR-render resolver (so each value gets its own light + dark class
516
+ * baked in, byte-identical to a `_rsCollapse` site for that value), and
517
+ * the runtime picks the right `(value × mode)` class via the user's
518
+ * expression.
519
+ *
520
+ * Class layout in `classes` is **stride-2, value-major**: index
521
+ * `2 * valueIndex + (isDark ? 1 : 0)`. For the canonical ternary case:
522
+ *
523
+ * ```
524
+ * <Button state={cond ? 'primary' : 'secondary'}>Save</Button>
525
+ * →
526
+ * __rsCollapseDyn(
527
+ * "<button>Save</button>",
528
+ * ["btn-primary-light", "btn-primary-dark", "btn-secondary-light", "btn-secondary-dark"],
529
+ * () => cond ? 0 : 1,
530
+ * () => __pyrMode() === "dark"
531
+ * )
532
+ * ```
533
+ *
534
+ * Both the value expression AND the mode accessor are reactive: a change
535
+ * to either re-runs ONLY this className assignment, no remount (same
536
+ * contract as `_rsCollapse`'s mode flip). Both dispatches share a single
537
+ * `_bindDirect` so reading both inside one effect subscribes once per
538
+ * source — Pyreon's effect dedupe handles the rest.
539
+ *
540
+ * The structural HTML template is shared across every value (asserted
541
+ * by the resolver — divergent markup between values bails the collapse).
542
+ * Mirrors `_rsCollapse`'s mode-divergence-bails invariant.
543
+ *
544
+ * `bind` follows the same contract as `_rsCollapse` — standard `_tpl`
545
+ * child/event binder, runs after class binding, disposers chained.
546
+ *
547
+ * @param html static element HTML WITHOUT the root `class=` attr
548
+ * @param classes flat array of `2 × valueCount` class strings,
549
+ * indexed `[v0_light, v0_dark, v1_light, v1_dark, ...]`. The runtime
550
+ * does no validation — the compiler is the source of truth (an
551
+ * out-of-range `valueIndex()` would coerce to `undefined` className,
552
+ * which is correct-for-zero-style — never crashes)
553
+ * @param valueIndex user expression returning 0..valueCount-1 — reactive
554
+ * @param isDark app mode accessor — reactive
555
+ * @param bind standard _tpl binder for children/events (or null)
556
+ */
557
+ function _rsCollapseDyn(html, classes, valueIndex, isDark, bind) {
558
+ return _tpl(html, (el) => {
559
+ const disposeClass = renderEffect(() => {
560
+ el.className = classes[valueIndex() << 1 | (isDark() ? 1 : 0)] ?? "";
561
+ });
562
+ const disposeChildren = bind ? bind(el) : null;
563
+ if (!disposeChildren) return disposeClass;
564
+ return () => {
565
+ disposeClass();
566
+ disposeChildren();
567
+ };
568
+ });
569
+ }
570
+ /**
571
+ * Compiler-emitted DYNAMIC-prop + HANDLER collapsed rocketstyle call
572
+ * site — closes the largest remaining real-corpus dynamic-collapse
573
+ * gap (`.claude/plans/open-work-2026-q3.md` → #1 dynamic-prop bucket
574
+ * = 15.4% of all real-corpus sites; the strict no-handler subset was
575
+ * only 0.2% measured; this helper unlocks the handler-combined slice
576
+ * that was bailed by `tryDynamicCollapse` in PR #767 by design).
577
+ *
578
+ * Combines {@link _rsCollapseDyn}'s value-major class dispatch with
579
+ * {@link _rsCollapseH}'s handler re-attachment. Handlers are orthogonal
580
+ * to both the SSR-resolved styler class AND the value dispatcher (a
581
+ * `state={cond ? 'a' : 'b'} onClick={h}` site's onClick is identical
582
+ * for both `state="a"` and `state="b"` resolutions — the styler class
583
+ * varies, the handler does not). So this helper is structurally the
584
+ * union of the two, no new behavior:
585
+ *
586
+ * ```
587
+ * <Button state={cond ? 'primary' : 'secondary'} onClick={go}>Save</Button>
588
+ * →
589
+ * __rsCollapseDynH(
590
+ * "<button>Save</button>",
591
+ * ["pri-L", "pri-D", "sec-L", "sec-D"],
592
+ * () => cond ? 0 : 1,
593
+ * () => __pyrMode() === "dark",
594
+ * { onClick: go }
595
+ * )
596
+ * ```
597
+ *
598
+ * Class layout matches `_rsCollapseDyn` (stride-2 value-major):
599
+ * `index = 2 * valueIndex + (isDark ? 1 : 0)`. Handler attachment
600
+ * matches `_rsCollapseH` — routed through the canonical `_bindEvent`
601
+ * → `applyEventProp` path (delegation + batching + name
602
+ * normalization). All three reactives (valueIndex, isDark, handlers
603
+ * — though handler identity is captured at the call site) compose
604
+ * cleanly: a value flip OR a mode flip patches className IN PLACE
605
+ * on the SAME node, handlers stay attached across both.
606
+ *
607
+ * Layer-pure: no styler / ui-core imports (the styler injection is
608
+ * the emitted code's job via `__rsSheet.injectRules`).
609
+ *
610
+ * @param html static element HTML WITHOUT the root `class=` attr
611
+ * @param classes flat array of `2 × valueCount` class strings, indexed
612
+ * `[v0_L, v0_D, v1_L, v1_D, …]`
613
+ * @param valueIndex user expression returning 0..valueCount-1 — reactive
614
+ * @param isDark app mode accessor — reactive
615
+ * @param handlers `{ onClick: fn, onPointerEnter: fn, … }` — the
616
+ * residual handlers peeled off the call site by the
617
+ * compiler's emit (sliced source spans re-emitted
618
+ * verbatim, paren-wrapped to keep arrow / sequence
619
+ * expressions a single value)
620
+ * @param bind standard _tpl binder for children/events (or null)
621
+ */
622
+ function _rsCollapseDynH(html, classes, valueIndex, isDark, handlers, bind) {
623
+ return _tpl(html, (el) => {
624
+ const disposeClass = renderEffect(() => {
625
+ el.className = classes[valueIndex() << 1 | (isDark() ? 1 : 0)] ?? "";
626
+ });
627
+ const handlerDisposers = [];
628
+ for (const key of Object.keys(handlers)) {
494
629
  const d = _bindEvent(el, key, handlers[key]);
495
630
  if (d) handlerDisposers.push(d);
496
631
  }
@@ -811,5 +946,5 @@ function mount(root, container) {
811
946
  const render = mount;
812
947
 
813
948
  //#endregion
814
- export { DELEGATED_EVENTS, KeepAlive, Transition, TransitionGroup, applyProps as _applyProps, applyProps, _bindDirect, _bindText, _mountSlot, _rsCollapse, _rsCollapseH, _tpl, applyProp, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, onHydrationMismatch, render, sanitizeHtml, setSanitizer, setupDelegation };
949
+ export { DELEGATED_EVENTS, KeepAlive, Transition, TransitionGroup, applyProps as _applyProps, applyProps, _bindDirect, _bindText, _mountSlot, _rsCollapse, _rsCollapseDyn, _rsCollapseDynH, _rsCollapseH, _tpl, applyProp, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, onHydrationMismatch, render, sanitizeHtml, setSanitizer, setupDelegation };
815
950
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
- import { t as KeepAlive } from "./_chunks/keep-alive-BM7bn3W9.js";
1
+ import { t as KeepAlive } from "./_chunks/keep-alive-DznjF_h1.js";
2
2
 
3
3
  export { KeepAlive };
@@ -372,6 +372,112 @@ declare function _rsCollapse(html: string, lightClass: string, darkClass: string
372
372
  * sliced source spans `detectPartialCollapsibleShape` returned.
373
373
  */
374
374
  declare function _rsCollapseH(html: string, lightClass: string, darkClass: string, isDark: () => boolean, handlers: Record<string, unknown>, bind?: ((el: HTMLElement) => (() => void) | null) | null): NativeItem;
375
+ /**
376
+ * Compiler-emitted DYNAMIC-prop collapsed rocketstyle call site — PR 1
377
+ * of the dynamic-prop partial-collapse build (next bite after the
378
+ * `on*`-handler partial-collapse `_rsCollapseH`, `.claude/plans/open-work-2026-q3.md`
379
+ * → #1 dynamic-prop bucket = 15.3% of all real-corpus sites).
380
+ *
381
+ * Generalises {@link _rsCollapse}'s 2-class (light/dark) dispatch to an
382
+ * N-class dispatch for sites where one dimension prop is an enumerable
383
+ * dynamic expression (e.g. `<Button state={cond ? 'primary' : 'secondary'}>`).
384
+ * The compiler resolves EVERY value of that prop through the existing
385
+ * SSR-render resolver (so each value gets its own light + dark class
386
+ * baked in, byte-identical to a `_rsCollapse` site for that value), and
387
+ * the runtime picks the right `(value × mode)` class via the user's
388
+ * expression.
389
+ *
390
+ * Class layout in `classes` is **stride-2, value-major**: index
391
+ * `2 * valueIndex + (isDark ? 1 : 0)`. For the canonical ternary case:
392
+ *
393
+ * ```
394
+ * <Button state={cond ? 'primary' : 'secondary'}>Save</Button>
395
+ * →
396
+ * __rsCollapseDyn(
397
+ * "<button>Save</button>",
398
+ * ["btn-primary-light", "btn-primary-dark", "btn-secondary-light", "btn-secondary-dark"],
399
+ * () => cond ? 0 : 1,
400
+ * () => __pyrMode() === "dark"
401
+ * )
402
+ * ```
403
+ *
404
+ * Both the value expression AND the mode accessor are reactive: a change
405
+ * to either re-runs ONLY this className assignment, no remount (same
406
+ * contract as `_rsCollapse`'s mode flip). Both dispatches share a single
407
+ * `_bindDirect` so reading both inside one effect subscribes once per
408
+ * source — Pyreon's effect dedupe handles the rest.
409
+ *
410
+ * The structural HTML template is shared across every value (asserted
411
+ * by the resolver — divergent markup between values bails the collapse).
412
+ * Mirrors `_rsCollapse`'s mode-divergence-bails invariant.
413
+ *
414
+ * `bind` follows the same contract as `_rsCollapse` — standard `_tpl`
415
+ * child/event binder, runs after class binding, disposers chained.
416
+ *
417
+ * @param html static element HTML WITHOUT the root `class=` attr
418
+ * @param classes flat array of `2 × valueCount` class strings,
419
+ * indexed `[v0_light, v0_dark, v1_light, v1_dark, ...]`. The runtime
420
+ * does no validation — the compiler is the source of truth (an
421
+ * out-of-range `valueIndex()` would coerce to `undefined` className,
422
+ * which is correct-for-zero-style — never crashes)
423
+ * @param valueIndex user expression returning 0..valueCount-1 — reactive
424
+ * @param isDark app mode accessor — reactive
425
+ * @param bind standard _tpl binder for children/events (or null)
426
+ */
427
+ declare function _rsCollapseDyn(html: string, classes: readonly string[], valueIndex: () => number, isDark: () => boolean, bind?: ((el: HTMLElement) => (() => void) | null) | null): NativeItem;
428
+ /**
429
+ * Compiler-emitted DYNAMIC-prop + HANDLER collapsed rocketstyle call
430
+ * site — closes the largest remaining real-corpus dynamic-collapse
431
+ * gap (`.claude/plans/open-work-2026-q3.md` → #1 dynamic-prop bucket
432
+ * = 15.4% of all real-corpus sites; the strict no-handler subset was
433
+ * only 0.2% measured; this helper unlocks the handler-combined slice
434
+ * that was bailed by `tryDynamicCollapse` in PR #767 by design).
435
+ *
436
+ * Combines {@link _rsCollapseDyn}'s value-major class dispatch with
437
+ * {@link _rsCollapseH}'s handler re-attachment. Handlers are orthogonal
438
+ * to both the SSR-resolved styler class AND the value dispatcher (a
439
+ * `state={cond ? 'a' : 'b'} onClick={h}` site's onClick is identical
440
+ * for both `state="a"` and `state="b"` resolutions — the styler class
441
+ * varies, the handler does not). So this helper is structurally the
442
+ * union of the two, no new behavior:
443
+ *
444
+ * ```
445
+ * <Button state={cond ? 'primary' : 'secondary'} onClick={go}>Save</Button>
446
+ * →
447
+ * __rsCollapseDynH(
448
+ * "<button>Save</button>",
449
+ * ["pri-L", "pri-D", "sec-L", "sec-D"],
450
+ * () => cond ? 0 : 1,
451
+ * () => __pyrMode() === "dark",
452
+ * { onClick: go }
453
+ * )
454
+ * ```
455
+ *
456
+ * Class layout matches `_rsCollapseDyn` (stride-2 value-major):
457
+ * `index = 2 * valueIndex + (isDark ? 1 : 0)`. Handler attachment
458
+ * matches `_rsCollapseH` — routed through the canonical `_bindEvent`
459
+ * → `applyEventProp` path (delegation + batching + name
460
+ * normalization). All three reactives (valueIndex, isDark, handlers
461
+ * — though handler identity is captured at the call site) compose
462
+ * cleanly: a value flip OR a mode flip patches className IN PLACE
463
+ * on the SAME node, handlers stay attached across both.
464
+ *
465
+ * Layer-pure: no styler / ui-core imports (the styler injection is
466
+ * the emitted code's job via `__rsSheet.injectRules`).
467
+ *
468
+ * @param html static element HTML WITHOUT the root `class=` attr
469
+ * @param classes flat array of `2 × valueCount` class strings, indexed
470
+ * `[v0_L, v0_D, v1_L, v1_D, …]`
471
+ * @param valueIndex user expression returning 0..valueCount-1 — reactive
472
+ * @param isDark app mode accessor — reactive
473
+ * @param handlers `{ onClick: fn, onPointerEnter: fn, … }` — the
474
+ * residual handlers peeled off the call site by the
475
+ * compiler's emit (sliced source spans re-emitted
476
+ * verbatim, paren-wrapped to keep arrow / sequence
477
+ * expressions a single value)
478
+ * @param bind standard _tpl binder for children/events (or null)
479
+ */
480
+ declare function _rsCollapseDynH(html: string, classes: readonly string[], valueIndex: () => number, isDark: () => boolean, handlers: Record<string, unknown>, bind?: ((el: HTMLElement) => (() => void) | null) | null): NativeItem;
375
481
  /**
376
482
  * Mount a children slot inside a template.
377
483
  *
@@ -513,5 +619,5 @@ declare function mount(root: VNodeChild, container: Element): () => void;
513
619
  /** Alias for `mount` */
514
620
  declare const render: typeof mount;
515
621
  //#endregion
516
- export { DELEGATED_EVENTS, type DevtoolsComponentEntry, type HydrationMismatchContext, type HydrationMismatchHandler, type HydrationMismatchType, KeepAlive, type KeepAliveProps, type PyreonDevtools, type SanitizeFn, Transition, TransitionGroup, type TransitionGroupProps, type TransitionProps, applyProps as _applyProps, applyProps, _bindDirect, _bindText, _mountSlot, _rsCollapse, _rsCollapseH, _tpl, applyProp, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, onHydrationMismatch, render, sanitizeHtml, setSanitizer, setupDelegation };
622
+ export { DELEGATED_EVENTS, type DevtoolsComponentEntry, type HydrationMismatchContext, type HydrationMismatchHandler, type HydrationMismatchType, KeepAlive, type KeepAliveProps, type PyreonDevtools, type SanitizeFn, Transition, TransitionGroup, type TransitionGroupProps, type TransitionProps, applyProps as _applyProps, applyProps, _bindDirect, _bindText, _mountSlot, _rsCollapse, _rsCollapseDyn, _rsCollapseDynH, _rsCollapseH, _tpl, applyProp, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, onHydrationMismatch, render, sanitizeHtml, setSanitizer, setupDelegation };
517
623
  //# sourceMappingURL=index2.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/runtime-dom",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "DOM renderer for Pyreon",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/runtime-dom#readme",
6
6
  "bugs": {
@@ -54,15 +54,15 @@
54
54
  "prepublishOnly": "bun run build"
55
55
  },
56
56
  "dependencies": {
57
- "@pyreon/core": "^0.23.0",
58
- "@pyreon/reactivity": "^0.23.0"
57
+ "@pyreon/core": "^0.24.0",
58
+ "@pyreon/reactivity": "^0.24.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@happy-dom/global-registrator": "^20.8.9",
62
- "@pyreon/compiler": "^0.23.0",
62
+ "@pyreon/compiler": "^0.24.0",
63
63
  "@pyreon/manifest": "0.13.1",
64
- "@pyreon/runtime-server": "^0.23.0",
65
- "@pyreon/test-utils": "^0.13.10",
64
+ "@pyreon/runtime-server": "^0.24.0",
65
+ "@pyreon/test-utils": "^0.13.11",
66
66
  "@vitest/browser-playwright": "^4.1.4",
67
67
  "esbuild": "^0.28.0",
68
68
  "happy-dom": "^20.8.3",
package/src/index.ts CHANGED
@@ -29,6 +29,8 @@ export {
29
29
  _bindText,
30
30
  _mountSlot,
31
31
  _rsCollapse,
32
+ _rsCollapseDyn,
33
+ _rsCollapseDynH,
32
34
  _rsCollapseH,
33
35
  _tpl,
34
36
  createTemplate,
package/src/nodes.ts CHANGED
@@ -100,8 +100,18 @@ export function mountReactive(
100
100
  // Child components set up their OWN effects for reactivity
101
101
  // (e.g. DynamicStyled's class swap effect). Those effects track
102
102
  // their own dependencies independently.
103
+ //
104
+ // Use the marker's LIVE parent (not the closure-captured `parent`):
105
+ // when this mountReactive was created inside a DocumentFragment that
106
+ // mountFor later moved into the live tree via `insertBefore(frag, ...)`,
107
+ // the captured `parent` becomes a stale reference to the now-empty
108
+ // fragment. The marker, in contrast, was moved with the fragment's
109
+ // contents and `marker.parentNode` reflects the current live parent.
110
+ // Falling back to the captured `parent` only when the marker is
111
+ // detached (cleanup edge case) preserves prior behavior.
112
+ const liveParent = marker.parentNode ?? parent
103
113
  const cleanup = runUntracked(() =>
104
- restoreContextStack(contextSnapshot, () => mount(value, parent, marker)),
114
+ restoreContextStack(contextSnapshot, () => mount(value, liveParent, marker)),
105
115
  )
106
116
  // Guard: a re-entrant signal update (e.g. ErrorBoundary catching a child
107
117
  // throw) may have already re-run this effect and updated currentCleanup.
@@ -292,15 +302,15 @@ export function mountKeyedList(
292
302
  }
293
303
  }
294
304
 
295
- const mountNewEntries = (newList: VNode[]) => {
305
+ const mountNewEntries = (newList: VNode[], liveParent: Node) => {
296
306
  for (const vnode of newList) {
297
307
  const key = vnode.key
298
308
  if (key === null || key === undefined) continue
299
309
  if (cache.has(key)) continue
300
310
  const anchor = document.createComment('')
301
311
  _keyedAnchors.add(anchor)
302
- parent.insertBefore(anchor, tailMarker)
303
- const cleanup = mountVNode(vnode, parent, tailMarker)
312
+ liveParent.insertBefore(anchor, tailMarker)
313
+ const cleanup = mountVNode(vnode, liveParent, tailMarker)
304
314
  cache.set(key, { anchor, cleanup })
305
315
  }
306
316
  }
@@ -311,6 +321,17 @@ export function mountKeyedList(
311
321
  // Same untracking rationale as mountFor — see comment there. Child
312
322
  // mounts via mountVNode must not re-track on this effect's run.
313
323
  runUntracked(() => {
324
+ // Use the marker's LIVE parent (not the closure-captured `parent`).
325
+ // Same bug class fixed in #776 for mountReactive: when this
326
+ // mountKeyedList was created inside a DocumentFragment that mountFor
327
+ // later moved via `liveParent.insertBefore(frag, tailMarker)`, the
328
+ // captured `parent` becomes a stale reference to the now-empty
329
+ // fragment. The markers were moved with the fragment's contents
330
+ // and their `parentNode` reflects the current live parent.
331
+ // Fallback to the captured `parent` only when the marker is
332
+ // detached (cleanup edge case) preserves prior behavior.
333
+ const liveParent = tailMarker.parentNode ?? parent
334
+
314
335
  if (n === 0 && cache.size > 0) {
315
336
  for (const entry of cache.values()) {
316
337
  _emitCleanup()
@@ -325,10 +346,10 @@ export function mountKeyedList(
325
346
 
326
347
  const { newKeyOrder, newKeySet } = collectKeyOrder(newList)
327
348
  removeStaleEntries(newKeySet)
328
- mountNewEntries(newList)
349
+ mountNewEntries(newList, liveParent)
329
350
 
330
351
  if (currentKeyOrder.length > 0 && n > 0) {
331
- lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, parent, tailMarker)
352
+ lis = keyedListReorder(lis, n, newKeyOrder, curPos, cache, liveParent, tailMarker)
332
353
  }
333
354
 
334
355
  curPos.clear()