@pyreon/runtime-dom 0.19.0 → 0.21.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.
@@ -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":"8df5ffbc-1","name":"delegate.ts"},{"uid":"8df5ffbc-3","name":"hydration-debug.ts"},{"uid":"8df5ffbc-5","name":"devtools.ts"},{"uid":"8df5ffbc-7","name":"nodes.ts"},{"uid":"8df5ffbc-9","name":"props.ts"},{"uid":"8df5ffbc-11","name":"mount.ts"},{"uid":"8df5ffbc-13","name":"hydrate.ts"},{"uid":"8df5ffbc-15","name":"keep-alive.ts"},{"uid":"8df5ffbc-17","name":"template.ts"},{"uid":"8df5ffbc-19","name":"transition.ts"},{"uid":"8df5ffbc-21","name":"transition-group.ts"},{"uid":"8df5ffbc-23","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"8df5ffbc-1":{"renderedLength":2090,"gzipLength":1029,"brotliLength":0,"metaUid":"8df5ffbc-0"},"8df5ffbc-3":{"renderedLength":1395,"gzipLength":718,"brotliLength":0,"metaUid":"8df5ffbc-2"},"8df5ffbc-5":{"renderedLength":7009,"gzipLength":2149,"brotliLength":0,"metaUid":"8df5ffbc-4"},"8df5ffbc-7":{"renderedLength":17556,"gzipLength":4660,"brotliLength":0,"metaUid":"8df5ffbc-6"},"8df5ffbc-9":{"renderedLength":8288,"gzipLength":3134,"brotliLength":0,"metaUid":"8df5ffbc-8"},"8df5ffbc-11":{"renderedLength":12232,"gzipLength":3882,"brotliLength":0,"metaUid":"8df5ffbc-10"},"8df5ffbc-13":{"renderedLength":8312,"gzipLength":2472,"brotliLength":0,"metaUid":"8df5ffbc-12"},"8df5ffbc-15":{"renderedLength":1518,"gzipLength":724,"brotliLength":0,"metaUid":"8df5ffbc-14"},"8df5ffbc-17":{"renderedLength":5942,"gzipLength":2283,"brotliLength":0,"metaUid":"8df5ffbc-16"},"8df5ffbc-19":{"renderedLength":4929,"gzipLength":1410,"brotliLength":0,"metaUid":"8df5ffbc-18"},"8df5ffbc-21":{"renderedLength":8002,"gzipLength":2092,"brotliLength":0,"metaUid":"8df5ffbc-20"},"8df5ffbc-23":{"renderedLength":985,"gzipLength":549,"brotliLength":0,"metaUid":"8df5ffbc-22"}},"nodeMetas":{"8df5ffbc-0":{"id":"/src/delegate.ts","moduleParts":{"index.js":"8df5ffbc-1"},"imported":[{"uid":"8df5ffbc-24"}],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-8"}]},"8df5ffbc-2":{"id":"/src/hydration-debug.ts","moduleParts":{"index.js":"8df5ffbc-3"},"imported":[],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-12"}]},"8df5ffbc-4":{"id":"/src/devtools.ts","moduleParts":{"index.js":"8df5ffbc-5"},"imported":[],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-10"}]},"8df5ffbc-6":{"id":"/src/nodes.ts","moduleParts":{"index.js":"8df5ffbc-7"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"}],"importedBy":[{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-10"}]},"8df5ffbc-8":{"id":"/src/props.ts","moduleParts":{"index.js":"8df5ffbc-9"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-0"}],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-10"}]},"8df5ffbc-10":{"id":"/src/mount.ts","moduleParts":{"index.js":"8df5ffbc-11"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-4"},{"uid":"8df5ffbc-6"},{"uid":"8df5ffbc-8"}],"importedBy":[{"uid":"8df5ffbc-22"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-14"},{"uid":"8df5ffbc-16"},{"uid":"8df5ffbc-20"}]},"8df5ffbc-12":{"id":"/src/hydrate.ts","moduleParts":{"index.js":"8df5ffbc-13"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-0"},{"uid":"8df5ffbc-2"},{"uid":"8df5ffbc-10"},{"uid":"8df5ffbc-6"},{"uid":"8df5ffbc-8"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-14":{"id":"/src/keep-alive.ts","moduleParts":{"index.js":"8df5ffbc-15"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-10"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-16":{"id":"/src/template.ts","moduleParts":{"index.js":"8df5ffbc-17"},"imported":[{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-10"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-18":{"id":"/src/transition.ts","moduleParts":{"index.js":"8df5ffbc-19"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-20":{"id":"/src/transition-group.ts","moduleParts":{"index.js":"8df5ffbc-21"},"imported":[{"uid":"8df5ffbc-25"},{"uid":"8df5ffbc-24"},{"uid":"8df5ffbc-10"}],"importedBy":[{"uid":"8df5ffbc-22"}]},"8df5ffbc-22":{"id":"/src/index.ts","moduleParts":{"index.js":"8df5ffbc-23"},"imported":[{"uid":"8df5ffbc-0"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-2"},{"uid":"8df5ffbc-14"},{"uid":"8df5ffbc-10"},{"uid":"8df5ffbc-8"},{"uid":"8df5ffbc-16"},{"uid":"8df5ffbc-18"},{"uid":"8df5ffbc-20"},{"uid":"8df5ffbc-4"}],"importedBy":[],"isEntry":true},"8df5ffbc-24":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"8df5ffbc-0"},{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-14"},{"uid":"8df5ffbc-10"},{"uid":"8df5ffbc-8"},{"uid":"8df5ffbc-16"},{"uid":"8df5ffbc-18"},{"uid":"8df5ffbc-20"},{"uid":"8df5ffbc-6"}]},"8df5ffbc-25":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"8df5ffbc-12"},{"uid":"8df5ffbc-14"},{"uid":"8df5ffbc-10"},{"uid":"8df5ffbc-8"},{"uid":"8df5ffbc-18"},{"uid":"8df5ffbc-20"},{"uid":"8df5ffbc-6"}]}},"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":"c8f66e40-1","name":"delegate.ts"},{"uid":"c8f66e40-3","name":"hydration-debug.ts"},{"uid":"c8f66e40-5","name":"devtools.ts"},{"uid":"c8f66e40-7","name":"nodes.ts"},{"uid":"c8f66e40-9","name":"props.ts"},{"uid":"c8f66e40-11","name":"mount.ts"},{"uid":"c8f66e40-13","name":"hydrate.ts"},{"uid":"c8f66e40-15","name":"keep-alive.ts"},{"uid":"c8f66e40-17","name":"template.ts"},{"uid":"c8f66e40-19","name":"transition.ts"},{"uid":"c8f66e40-21","name":"transition-group.ts"},{"uid":"c8f66e40-23","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"c8f66e40-1":{"renderedLength":2090,"gzipLength":1029,"brotliLength":0,"metaUid":"c8f66e40-0"},"c8f66e40-3":{"renderedLength":1395,"gzipLength":718,"brotliLength":0,"metaUid":"c8f66e40-2"},"c8f66e40-5":{"renderedLength":8163,"gzipLength":2538,"brotliLength":0,"metaUid":"c8f66e40-4"},"c8f66e40-7":{"renderedLength":17556,"gzipLength":4660,"brotliLength":0,"metaUid":"c8f66e40-6"},"c8f66e40-9":{"renderedLength":9054,"gzipLength":3498,"brotliLength":0,"metaUid":"c8f66e40-8"},"c8f66e40-11":{"renderedLength":12232,"gzipLength":3882,"brotliLength":0,"metaUid":"c8f66e40-10"},"c8f66e40-13":{"renderedLength":8312,"gzipLength":2472,"brotliLength":0,"metaUid":"c8f66e40-12"},"c8f66e40-15":{"renderedLength":1518,"gzipLength":724,"brotliLength":0,"metaUid":"c8f66e40-14"},"c8f66e40-17":{"renderedLength":9693,"gzipLength":3751,"brotliLength":0,"metaUid":"c8f66e40-16"},"c8f66e40-19":{"renderedLength":4929,"gzipLength":1410,"brotliLength":0,"metaUid":"c8f66e40-18"},"c8f66e40-21":{"renderedLength":8002,"gzipLength":2092,"brotliLength":0,"metaUid":"c8f66e40-20"},"c8f66e40-23":{"renderedLength":985,"gzipLength":549,"brotliLength":0,"metaUid":"c8f66e40-22"}},"nodeMetas":{"c8f66e40-0":{"id":"/src/delegate.ts","moduleParts":{"index.js":"c8f66e40-1"},"imported":[{"uid":"c8f66e40-24"}],"importedBy":[{"uid":"c8f66e40-22"},{"uid":"c8f66e40-12"},{"uid":"c8f66e40-8"}]},"c8f66e40-2":{"id":"/src/hydration-debug.ts","moduleParts":{"index.js":"c8f66e40-3"},"imported":[],"importedBy":[{"uid":"c8f66e40-22"},{"uid":"c8f66e40-12"}]},"c8f66e40-4":{"id":"/src/devtools.ts","moduleParts":{"index.js":"c8f66e40-5"},"imported":[{"uid":"c8f66e40-24"}],"importedBy":[{"uid":"c8f66e40-22"},{"uid":"c8f66e40-10"}]},"c8f66e40-6":{"id":"/src/nodes.ts","moduleParts":{"index.js":"c8f66e40-7"},"imported":[{"uid":"c8f66e40-25"},{"uid":"c8f66e40-24"}],"importedBy":[{"uid":"c8f66e40-12"},{"uid":"c8f66e40-10"}]},"c8f66e40-8":{"id":"/src/props.ts","moduleParts":{"index.js":"c8f66e40-9"},"imported":[{"uid":"c8f66e40-25"},{"uid":"c8f66e40-24"},{"uid":"c8f66e40-0"}],"importedBy":[{"uid":"c8f66e40-22"},{"uid":"c8f66e40-12"},{"uid":"c8f66e40-10"},{"uid":"c8f66e40-16"}]},"c8f66e40-10":{"id":"/src/mount.ts","moduleParts":{"index.js":"c8f66e40-11"},"imported":[{"uid":"c8f66e40-25"},{"uid":"c8f66e40-24"},{"uid":"c8f66e40-4"},{"uid":"c8f66e40-6"},{"uid":"c8f66e40-8"}],"importedBy":[{"uid":"c8f66e40-22"},{"uid":"c8f66e40-12"},{"uid":"c8f66e40-14"},{"uid":"c8f66e40-16"},{"uid":"c8f66e40-20"}]},"c8f66e40-12":{"id":"/src/hydrate.ts","moduleParts":{"index.js":"c8f66e40-13"},"imported":[{"uid":"c8f66e40-25"},{"uid":"c8f66e40-24"},{"uid":"c8f66e40-0"},{"uid":"c8f66e40-2"},{"uid":"c8f66e40-10"},{"uid":"c8f66e40-6"},{"uid":"c8f66e40-8"}],"importedBy":[{"uid":"c8f66e40-22"}]},"c8f66e40-14":{"id":"/src/keep-alive.ts","moduleParts":{"index.js":"c8f66e40-15"},"imported":[{"uid":"c8f66e40-25"},{"uid":"c8f66e40-24"},{"uid":"c8f66e40-10"}],"importedBy":[{"uid":"c8f66e40-22"}]},"c8f66e40-16":{"id":"/src/template.ts","moduleParts":{"index.js":"c8f66e40-17"},"imported":[{"uid":"c8f66e40-24"},{"uid":"c8f66e40-10"},{"uid":"c8f66e40-8"}],"importedBy":[{"uid":"c8f66e40-22"}]},"c8f66e40-18":{"id":"/src/transition.ts","moduleParts":{"index.js":"c8f66e40-19"},"imported":[{"uid":"c8f66e40-25"},{"uid":"c8f66e40-24"}],"importedBy":[{"uid":"c8f66e40-22"}]},"c8f66e40-20":{"id":"/src/transition-group.ts","moduleParts":{"index.js":"c8f66e40-21"},"imported":[{"uid":"c8f66e40-25"},{"uid":"c8f66e40-24"},{"uid":"c8f66e40-10"}],"importedBy":[{"uid":"c8f66e40-22"}]},"c8f66e40-22":{"id":"/src/index.ts","moduleParts":{"index.js":"c8f66e40-23"},"imported":[{"uid":"c8f66e40-0"},{"uid":"c8f66e40-12"},{"uid":"c8f66e40-2"},{"uid":"c8f66e40-14"},{"uid":"c8f66e40-10"},{"uid":"c8f66e40-8"},{"uid":"c8f66e40-16"},{"uid":"c8f66e40-18"},{"uid":"c8f66e40-20"},{"uid":"c8f66e40-4"}],"importedBy":[],"isEntry":true},"c8f66e40-24":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"c8f66e40-0"},{"uid":"c8f66e40-12"},{"uid":"c8f66e40-14"},{"uid":"c8f66e40-10"},{"uid":"c8f66e40-8"},{"uid":"c8f66e40-16"},{"uid":"c8f66e40-18"},{"uid":"c8f66e40-20"},{"uid":"c8f66e40-4"},{"uid":"c8f66e40-6"}]},"c8f66e40-25":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"c8f66e40-12"},{"uid":"c8f66e40-14"},{"uid":"c8f66e40-10"},{"uid":"c8f66e40-8"},{"uid":"c8f66e40-18"},{"uid":"c8f66e40-20"},{"uid":"c8f66e40-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -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":"keep-alive-entry.js","children":[{"name":"src","children":[{"uid":"56ea40e5-1","name":"devtools.ts"},{"uid":"56ea40e5-3","name":"nodes.ts"},{"uid":"56ea40e5-5","name":"delegate.ts"},{"uid":"56ea40e5-7","name":"props.ts"},{"uid":"56ea40e5-9","name":"mount.ts"},{"uid":"56ea40e5-11","name":"keep-alive.ts"}]}]}],"isRoot":true},"nodeParts":{"56ea40e5-1":{"renderedLength":759,"gzipLength":340,"brotliLength":0,"metaUid":"56ea40e5-0"},"56ea40e5-3":{"renderedLength":17556,"gzipLength":4658,"brotliLength":0,"metaUid":"56ea40e5-2"},"56ea40e5-5":{"renderedLength":790,"gzipLength":436,"brotliLength":0,"metaUid":"56ea40e5-4"},"56ea40e5-7":{"renderedLength":7818,"gzipLength":2974,"brotliLength":0,"metaUid":"56ea40e5-6"},"56ea40e5-9":{"renderedLength":12162,"gzipLength":3873,"brotliLength":0,"metaUid":"56ea40e5-8"},"56ea40e5-11":{"renderedLength":1518,"gzipLength":724,"brotliLength":0,"metaUid":"56ea40e5-10"}},"nodeMetas":{"56ea40e5-0":{"id":"/src/devtools.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-1"},"imported":[],"importedBy":[{"uid":"56ea40e5-8"}]},"56ea40e5-2":{"id":"/src/nodes.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-3"},"imported":[{"uid":"56ea40e5-12"},{"uid":"56ea40e5-13"}],"importedBy":[{"uid":"56ea40e5-8"}]},"56ea40e5-4":{"id":"/src/delegate.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-5"},"imported":[{"uid":"56ea40e5-13"}],"importedBy":[{"uid":"56ea40e5-6"}]},"56ea40e5-6":{"id":"/src/props.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-7"},"imported":[{"uid":"56ea40e5-12"},{"uid":"56ea40e5-13"},{"uid":"56ea40e5-4"}],"importedBy":[{"uid":"56ea40e5-8"}]},"56ea40e5-8":{"id":"/src/mount.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-9"},"imported":[{"uid":"56ea40e5-12"},{"uid":"56ea40e5-13"},{"uid":"56ea40e5-0"},{"uid":"56ea40e5-2"},{"uid":"56ea40e5-6"}],"importedBy":[{"uid":"56ea40e5-10"}]},"56ea40e5-10":{"id":"/src/keep-alive.ts","moduleParts":{"keep-alive-entry.js":"56ea40e5-11"},"imported":[{"uid":"56ea40e5-12"},{"uid":"56ea40e5-13"},{"uid":"56ea40e5-8"}],"importedBy":[],"isEntry":true},"56ea40e5-12":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"56ea40e5-10"},{"uid":"56ea40e5-8"},{"uid":"56ea40e5-2"},{"uid":"56ea40e5-6"}]},"56ea40e5-13":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"56ea40e5-10"},{"uid":"56ea40e5-8"},{"uid":"56ea40e5-2"},{"uid":"56ea40e5-6"},{"uid":"56ea40e5-4"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"keep-alive-entry.js","children":[{"name":"src","children":[{"uid":"30eeaaf0-1","name":"devtools.ts"},{"uid":"30eeaaf0-3","name":"nodes.ts"},{"uid":"30eeaaf0-5","name":"delegate.ts"},{"uid":"30eeaaf0-7","name":"props.ts"},{"uid":"30eeaaf0-9","name":"mount.ts"},{"uid":"30eeaaf0-11","name":"keep-alive.ts"}]}]}],"isRoot":true},"nodeParts":{"30eeaaf0-1":{"renderedLength":759,"gzipLength":340,"brotliLength":0,"metaUid":"30eeaaf0-0"},"30eeaaf0-3":{"renderedLength":17556,"gzipLength":4658,"brotliLength":0,"metaUid":"30eeaaf0-2"},"30eeaaf0-5":{"renderedLength":790,"gzipLength":436,"brotliLength":0,"metaUid":"30eeaaf0-4"},"30eeaaf0-7":{"renderedLength":7818,"gzipLength":2974,"brotliLength":0,"metaUid":"30eeaaf0-6"},"30eeaaf0-9":{"renderedLength":12162,"gzipLength":3873,"brotliLength":0,"metaUid":"30eeaaf0-8"},"30eeaaf0-11":{"renderedLength":1518,"gzipLength":724,"brotliLength":0,"metaUid":"30eeaaf0-10"}},"nodeMetas":{"30eeaaf0-0":{"id":"/src/devtools.ts","moduleParts":{"keep-alive-entry.js":"30eeaaf0-1"},"imported":[{"uid":"30eeaaf0-13"}],"importedBy":[{"uid":"30eeaaf0-8"}]},"30eeaaf0-2":{"id":"/src/nodes.ts","moduleParts":{"keep-alive-entry.js":"30eeaaf0-3"},"imported":[{"uid":"30eeaaf0-12"},{"uid":"30eeaaf0-13"}],"importedBy":[{"uid":"30eeaaf0-8"}]},"30eeaaf0-4":{"id":"/src/delegate.ts","moduleParts":{"keep-alive-entry.js":"30eeaaf0-5"},"imported":[{"uid":"30eeaaf0-13"}],"importedBy":[{"uid":"30eeaaf0-6"}]},"30eeaaf0-6":{"id":"/src/props.ts","moduleParts":{"keep-alive-entry.js":"30eeaaf0-7"},"imported":[{"uid":"30eeaaf0-12"},{"uid":"30eeaaf0-13"},{"uid":"30eeaaf0-4"}],"importedBy":[{"uid":"30eeaaf0-8"}]},"30eeaaf0-8":{"id":"/src/mount.ts","moduleParts":{"keep-alive-entry.js":"30eeaaf0-9"},"imported":[{"uid":"30eeaaf0-12"},{"uid":"30eeaaf0-13"},{"uid":"30eeaaf0-0"},{"uid":"30eeaaf0-2"},{"uid":"30eeaaf0-6"}],"importedBy":[{"uid":"30eeaaf0-10"}]},"30eeaaf0-10":{"id":"/src/keep-alive.ts","moduleParts":{"keep-alive-entry.js":"30eeaaf0-11"},"imported":[{"uid":"30eeaaf0-12"},{"uid":"30eeaaf0-13"},{"uid":"30eeaaf0-8"}],"importedBy":[],"isEntry":true},"30eeaaf0-12":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"30eeaaf0-10"},{"uid":"30eeaaf0-8"},{"uid":"30eeaaf0-2"},{"uid":"30eeaaf0-6"}]},"30eeaaf0-13":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"30eeaaf0-10"},{"uid":"30eeaaf0-8"},{"uid":"30eeaaf0-0"},{"uid":"30eeaaf0-2"},{"uid":"30eeaaf0-6"},{"uid":"30eeaaf0-4"}]}},"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 { batch, effect, effectScope, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
1
+ import { activateReactiveDevtools, batch, deactivateReactiveDevtools, effect, effectScope, getReactiveFires, getReactiveGraph, renderEffect, runUntracked, setCurrentScope, signal } from "@pyreon/reactivity";
2
2
  import { EMPTY_PROPS, ForSymbol, Fragment, PortalSymbol, captureContextStack, createRef, cx, dispatchToErrorBoundary, h, makeReactiveProps, nativeCompat, normalizeStyleValue, onMount, onUnmount, propagateError, reportError, restoreContextStack, runWithHooks, toKebabCase } from "@pyreon/core";
3
3
 
4
4
  //#region src/delegate.ts
@@ -129,6 +129,23 @@ function warnHydrationMismatch(type, expected, actual, path) {
129
129
 
130
130
  //#endregion
131
131
  //#region src/devtools.ts
132
+ /**
133
+ * Pyreon DevTools — exposes a `__PYREON_DEVTOOLS__` global hook for browser devtools extensions
134
+ * and in-app debugging utilities.
135
+ *
136
+ * Installed automatically on first `mount()` call in the browser.
137
+ * No-op on the server (typeof window === "undefined").
138
+ *
139
+ * Usage:
140
+ * window.__PYREON_DEVTOOLS__.getComponentTree() // root component entries
141
+ * window.__PYREON_DEVTOOLS__.getAllComponents() // flat list of all live components
142
+ * window.__PYREON_DEVTOOLS__.highlight("comp-id") // outline a component's DOM node
143
+ * window.__PYREON_DEVTOOLS__.onComponentMount(cb) // subscribe to mount events
144
+ * window.__PYREON_DEVTOOLS__.onComponentUnmount(cb)// subscribe to unmount events
145
+ * window.__PYREON_DEVTOOLS__.enableOverlay() // Ctrl+Shift+P: hover to inspect components
146
+ * window.__PYREON_DEVTOOLS__.reactive.activate() // opt-in: track the live signal/effect graph
147
+ * window.__PYREON_DEVTOOLS__.reactive.getGraph() // snapshot of signals/derived/effects + edges
148
+ */
132
149
  const _components = /* @__PURE__ */ new Map();
133
150
  const _mountListeners = [];
134
151
  const _unmountListeners = [];
@@ -298,7 +315,13 @@ function installDevTools() {
298
315
  };
299
316
  },
300
317
  enableOverlay,
301
- disableOverlay
318
+ disableOverlay,
319
+ reactive: {
320
+ activate: activateReactiveDevtools,
321
+ deactivate: deactivateReactiveDevtools,
322
+ getGraph: getReactiveGraph,
323
+ getFires: getReactiveFires
324
+ }
302
325
  };
303
326
  window.__PYREON_DEVTOOLS__ = devtools;
304
327
  window.addEventListener("keydown", (e) => {
@@ -1092,6 +1115,21 @@ function applyEventProp(el, key, value) {
1092
1115
  return () => el.removeEventListener(eventName, batched);
1093
1116
  }
1094
1117
  /**
1118
+ * Bind ONE event handler through the CANONICAL event path
1119
+ * (`applyEventProp` — the same delegation, batching, and exact
1120
+ * `onXxx`→event-name normalization every compiler-emitted handler
1121
+ * uses). PR 2 of the partial-collapse build (open-work #1): a
1122
+ * collapsed-with-handler site (`_rsCollapseH`) re-attaches the residual
1123
+ * handlers `detectPartialCollapsibleShape` (compiler PR 1) peeled off.
1124
+ * Contract-consistent BY CONSTRUCTION — it IS `applyEventProp`, not a
1125
+ * re-implementation — so a partially-collapsed `<Button onClick=…>`
1126
+ * behaves byte-identically to the 5-layer mount it replaced (same
1127
+ * delegated-event prop slot, same `batch()` wrapping, same cleanup).
1128
+ */
1129
+ function _bindEvent(el, key, handler) {
1130
+ return applyEventProp(el, key, handler);
1131
+ }
1132
+ /**
1095
1133
  * Sink for a single prop's CALLED value (always a primitive / object /
1096
1134
  * `null` — never a function). Called both directly for static values and
1097
1135
  * from the reactive `renderEffect` for accessor-bound values.
@@ -2034,6 +2072,87 @@ function _tpl(html, bind) {
2034
2072
  };
2035
2073
  }
2036
2074
  /**
2075
+ * Compiler-emitted collapsed rocketstyle call site.
2076
+ *
2077
+ * The runtime half of the P0 compile-time rocketstyle wrapper-collapse.
2078
+ * For a literal-prop call site like `<Button state="primary" size="md">Save</Button>`,
2079
+ * the build resolves the FULL rocketstyle/styler pipeline once (SSR
2080
+ * render of the real component) and the compiler emits ONE `_rsCollapse`
2081
+ * call instead of the 5-layer wrapper mount (rocketstyle → attrs HOC →
2082
+ * Element → Wrapper → styled). Measured 44× wall-clock, mountChild 9→1
2083
+ * (see examples/experiments/e2-static-rocketstyle/RESULTS.md).
2084
+ *
2085
+ * Dual-emit (RFC decision 1): both the light- and dark-resolved class
2086
+ * strings are baked in; `isDark` is the app's live mode accessor (the
2087
+ * compiler threads it from the configured provider, e.g. `useMode` from
2088
+ * `@pyreon/ui-core`). A whole-theme/mode swap re-runs only this binding —
2089
+ * no remount — preserving Pyreon's reactive mode-switch contract. The
2090
+ * resolved CSS rules are injected once at module-eval via the styler's
2091
+ * idempotent `injectRules()` (emitted alongside this call), so the
2092
+ * collapsed site is self-sufficient: no prior runtime mount of the real
2093
+ * component is needed to populate the sheet.
2094
+ *
2095
+ * `bind` is the standard `_tpl` child/event binder for the (static)
2096
+ * children — identical to what the compiler emits for the non-collapsed
2097
+ * template path, so children reactivity / event delegation is unchanged.
2098
+ *
2099
+ * @param html static element HTML WITHOUT the class attr (class is applied reactively)
2100
+ * @param lightClass resolved styler class string for light mode
2101
+ * @param darkClass resolved styler class string for dark mode
2102
+ * @param isDark app mode accessor — `() => boolean` (true ⇒ dark)
2103
+ * @param bind standard _tpl binder for children/events (or null)
2104
+ */
2105
+ function _rsCollapse(html, lightClass, darkClass, isDark, bind) {
2106
+ return _tpl(html, (el) => {
2107
+ const disposeClass = _bindDirect(isDark, (v) => {
2108
+ el.className = v ? darkClass : lightClass;
2109
+ });
2110
+ const disposeChildren = bind ? bind(el) : null;
2111
+ if (!disposeChildren) return disposeClass;
2112
+ return () => {
2113
+ disposeClass();
2114
+ disposeChildren();
2115
+ };
2116
+ });
2117
+ }
2118
+ /**
2119
+ * Compiler-emitted PARTIALLY-collapsed rocketstyle call site — PR 2 of
2120
+ * the partial-collapse build (`.claude/plans/open-work-2026-q3.md` → #1).
2121
+ *
2122
+ * Identical to {@link _rsCollapse} (one `_tpl` cloneNode, dual-emit
2123
+ * reactive class, no remount on mode swap) PLUS it re-attaches the
2124
+ * residual event handlers `detectPartialCollapsibleShape` (compiler
2125
+ * PR 1) peeled off the `on*`-handler-only subset (the 7.8% the bail
2126
+ * census measured). Handlers are orthogonal to the SSR-resolved styler
2127
+ * class, so `html` / `lightClass` / `darkClass` are byte-identical to a
2128
+ * full-collapse site's — the ONLY delta vs `_rsCollapse` is the handler
2129
+ * re-attach, routed through the CANONICAL `_bindEvent` → `applyEventProp`
2130
+ * path (delegation + batching + name normalization), so the collapsed
2131
+ * node behaves byte-identically to the 5-layer mount it replaced.
2132
+ *
2133
+ * @param handlers `{ onClick: fn, onPointerEnter: fn, … }` — the peeled
2134
+ * residual handlers; compiler PR 3 emits this object literal from the
2135
+ * sliced source spans `detectPartialCollapsibleShape` returned.
2136
+ */
2137
+ function _rsCollapseH(html, lightClass, darkClass, isDark, handlers, bind) {
2138
+ return _tpl(html, (el) => {
2139
+ const disposeClass = _bindDirect(isDark, (v) => {
2140
+ el.className = v ? darkClass : lightClass;
2141
+ });
2142
+ const handlerDisposers = [];
2143
+ for (const key in handlers) {
2144
+ const d = _bindEvent(el, key, handlers[key]);
2145
+ if (d) handlerDisposers.push(d);
2146
+ }
2147
+ const disposeChildren = bind ? bind(el) : null;
2148
+ return () => {
2149
+ disposeClass();
2150
+ for (const d of handlerDisposers) d();
2151
+ if (disposeChildren) disposeChildren();
2152
+ };
2153
+ });
2154
+ }
2155
+ /**
2037
2156
  * Mount a children slot inside a template.
2038
2157
  *
2039
2158
  * Compiler emits this instead of `createTextNode()` when it detects a
@@ -2505,5 +2624,5 @@ function mount(root, container) {
2505
2624
  const render = mount;
2506
2625
 
2507
2626
  //#endregion
2508
- export { DELEGATED_EVENTS, KeepAlive, Transition, TransitionGroup, applyProps as _applyProps, applyProps, _bindDirect, _bindText, _mountSlot, _tpl, applyProp, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, onHydrationMismatch, render, sanitizeHtml, setSanitizer, setupDelegation };
2627
+ 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 };
2509
2628
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,4 @@
1
+ import { ReactiveFire, ReactiveGraph } from "@pyreon/reactivity";
1
2
  import { NativeItem, Props, VNode, VNodeChild } from "@pyreon/core";
2
3
 
3
4
  //#region src/delegate.d.ts
@@ -31,21 +32,6 @@ declare function delegatedPropName(eventName: string): string;
31
32
  declare function setupDelegation(container: Element): void;
32
33
  //#endregion
33
34
  //#region src/devtools.d.ts
34
- /**
35
- * Pyreon DevTools — exposes a `__PYREON_DEVTOOLS__` global hook for browser devtools extensions
36
- * and in-app debugging utilities.
37
- *
38
- * Installed automatically on first `mount()` call in the browser.
39
- * No-op on the server (typeof window === "undefined").
40
- *
41
- * Usage:
42
- * window.__PYREON_DEVTOOLS__.getComponentTree() // root component entries
43
- * window.__PYREON_DEVTOOLS__.getAllComponents() // flat list of all live components
44
- * window.__PYREON_DEVTOOLS__.highlight("comp-id") // outline a component's DOM node
45
- * window.__PYREON_DEVTOOLS__.onComponentMount(cb) // subscribe to mount events
46
- * window.__PYREON_DEVTOOLS__.onComponentUnmount(cb)// subscribe to unmount events
47
- * window.__PYREON_DEVTOOLS__.enableOverlay() // Ctrl+Shift+P: hover to inspect components
48
- */
49
35
  interface DevtoolsComponentEntry {
50
36
  id: string;
51
37
  name: string;
@@ -64,6 +50,22 @@ interface PyreonDevtools {
64
50
  /** Toggle the component inspector overlay (also: Ctrl+Shift+P) */
65
51
  enableOverlay(): void;
66
52
  disableOverlay(): void;
53
+ /**
54
+ * Reactive-graph bridge — powers the devtools Signals / Graph / Effects
55
+ * surfaces. Opt-in and zero-cost until `activate()` is called: nothing
56
+ * is tracked while a devtools client is not attached.
57
+ */
58
+ reactive: PyreonReactiveDevtools;
59
+ }
60
+ interface PyreonReactiveDevtools {
61
+ /** Start tracking the live signal/computed/effect graph. Idempotent. */
62
+ activate(): void;
63
+ /** Stop tracking + drop all retained registry/timeline state. */
64
+ deactivate(): void;
65
+ /** Fresh snapshot of the reactive graph (nodes + edges). */
66
+ getGraph(): ReactiveGraph;
67
+ /** Bounded recent-fire timeline (oldest → newest). */
68
+ getFires(): ReactiveFire[];
67
69
  }
68
70
  //#endregion
69
71
  //#region src/hydrate.d.ts
@@ -318,6 +320,58 @@ declare function _bindDirect(source: {
318
320
  * })
319
321
  */
320
322
  declare function _tpl(html: string, bind: (el: HTMLElement) => (() => void) | null): NativeItem;
323
+ /**
324
+ * Compiler-emitted collapsed rocketstyle call site.
325
+ *
326
+ * The runtime half of the P0 compile-time rocketstyle wrapper-collapse.
327
+ * For a literal-prop call site like `<Button state="primary" size="md">Save</Button>`,
328
+ * the build resolves the FULL rocketstyle/styler pipeline once (SSR
329
+ * render of the real component) and the compiler emits ONE `_rsCollapse`
330
+ * call instead of the 5-layer wrapper mount (rocketstyle → attrs HOC →
331
+ * Element → Wrapper → styled). Measured 44× wall-clock, mountChild 9→1
332
+ * (see examples/experiments/e2-static-rocketstyle/RESULTS.md).
333
+ *
334
+ * Dual-emit (RFC decision 1): both the light- and dark-resolved class
335
+ * strings are baked in; `isDark` is the app's live mode accessor (the
336
+ * compiler threads it from the configured provider, e.g. `useMode` from
337
+ * `@pyreon/ui-core`). A whole-theme/mode swap re-runs only this binding —
338
+ * no remount — preserving Pyreon's reactive mode-switch contract. The
339
+ * resolved CSS rules are injected once at module-eval via the styler's
340
+ * idempotent `injectRules()` (emitted alongside this call), so the
341
+ * collapsed site is self-sufficient: no prior runtime mount of the real
342
+ * component is needed to populate the sheet.
343
+ *
344
+ * `bind` is the standard `_tpl` child/event binder for the (static)
345
+ * children — identical to what the compiler emits for the non-collapsed
346
+ * template path, so children reactivity / event delegation is unchanged.
347
+ *
348
+ * @param html static element HTML WITHOUT the class attr (class is applied reactively)
349
+ * @param lightClass resolved styler class string for light mode
350
+ * @param darkClass resolved styler class string for dark mode
351
+ * @param isDark app mode accessor — `() => boolean` (true ⇒ dark)
352
+ * @param bind standard _tpl binder for children/events (or null)
353
+ */
354
+ declare function _rsCollapse(html: string, lightClass: string, darkClass: string, isDark: () => boolean, bind?: ((el: HTMLElement) => (() => void) | null) | null): NativeItem;
355
+ /**
356
+ * Compiler-emitted PARTIALLY-collapsed rocketstyle call site — PR 2 of
357
+ * the partial-collapse build (`.claude/plans/open-work-2026-q3.md` → #1).
358
+ *
359
+ * Identical to {@link _rsCollapse} (one `_tpl` cloneNode, dual-emit
360
+ * reactive class, no remount on mode swap) PLUS it re-attaches the
361
+ * residual event handlers `detectPartialCollapsibleShape` (compiler
362
+ * PR 1) peeled off the `on*`-handler-only subset (the 7.8% the bail
363
+ * census measured). Handlers are orthogonal to the SSR-resolved styler
364
+ * class, so `html` / `lightClass` / `darkClass` are byte-identical to a
365
+ * full-collapse site's — the ONLY delta vs `_rsCollapse` is the handler
366
+ * re-attach, routed through the CANONICAL `_bindEvent` → `applyEventProp`
367
+ * path (delegation + batching + name normalization), so the collapsed
368
+ * node behaves byte-identically to the 5-layer mount it replaced.
369
+ *
370
+ * @param handlers `{ onClick: fn, onPointerEnter: fn, … }` — the peeled
371
+ * residual handlers; compiler PR 3 emits this object literal from the
372
+ * sliced source spans `detectPartialCollapsibleShape` returned.
373
+ */
374
+ declare function _rsCollapseH(html: string, lightClass: string, darkClass: string, isDark: () => boolean, handlers: Record<string, unknown>, bind?: ((el: HTMLElement) => (() => void) | null) | null): NativeItem;
321
375
  /**
322
376
  * Mount a children slot inside a template.
323
377
  *
@@ -459,5 +513,5 @@ declare function mount(root: VNodeChild, container: Element): () => void;
459
513
  /** Alias for `mount` */
460
514
  declare const render: typeof mount;
461
515
  //#endregion
462
- 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, _tpl, applyProp, createTemplate, delegatedPropName, disableHydrationWarnings, enableHydrationWarnings, hydrateRoot, mount, mountChild, onHydrationMismatch, render, sanitizeHtml, setSanitizer, setupDelegation };
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 };
463
517
  //# sourceMappingURL=index2.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/runtime-dom",
3
- "version": "0.19.0",
3
+ "version": "0.21.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.19.0",
58
- "@pyreon/reactivity": "^0.19.0"
57
+ "@pyreon/core": "^0.21.0",
58
+ "@pyreon/reactivity": "^0.21.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@happy-dom/global-registrator": "^20.8.9",
62
- "@pyreon/compiler": "^0.19.0",
62
+ "@pyreon/compiler": "^0.21.0",
63
63
  "@pyreon/manifest": "0.13.1",
64
- "@pyreon/runtime-server": "^0.19.0",
65
- "@pyreon/test-utils": "^0.13.6",
64
+ "@pyreon/runtime-server": "^0.21.0",
65
+ "@pyreon/test-utils": "^0.13.8",
66
66
  "@vitest/browser-playwright": "^4.1.4",
67
67
  "esbuild": "^0.28.0",
68
68
  "happy-dom": "^20.8.3",
package/src/devtools.ts CHANGED
@@ -12,8 +12,19 @@
12
12
  * window.__PYREON_DEVTOOLS__.onComponentMount(cb) // subscribe to mount events
13
13
  * window.__PYREON_DEVTOOLS__.onComponentUnmount(cb)// subscribe to unmount events
14
14
  * window.__PYREON_DEVTOOLS__.enableOverlay() // Ctrl+Shift+P: hover to inspect components
15
+ * window.__PYREON_DEVTOOLS__.reactive.activate() // opt-in: track the live signal/effect graph
16
+ * window.__PYREON_DEVTOOLS__.reactive.getGraph() // snapshot of signals/derived/effects + edges
15
17
  */
16
18
 
19
+ import {
20
+ activateReactiveDevtools,
21
+ deactivateReactiveDevtools,
22
+ getReactiveFires,
23
+ getReactiveGraph,
24
+ type ReactiveFire,
25
+ type ReactiveGraph,
26
+ } from '@pyreon/reactivity'
27
+
17
28
  export interface DevtoolsComponentEntry {
18
29
  id: string
19
30
  name: string
@@ -33,6 +44,23 @@ export interface PyreonDevtools {
33
44
  /** Toggle the component inspector overlay (also: Ctrl+Shift+P) */
34
45
  enableOverlay(): void
35
46
  disableOverlay(): void
47
+ /**
48
+ * Reactive-graph bridge — powers the devtools Signals / Graph / Effects
49
+ * surfaces. Opt-in and zero-cost until `activate()` is called: nothing
50
+ * is tracked while a devtools client is not attached.
51
+ */
52
+ reactive: PyreonReactiveDevtools
53
+ }
54
+
55
+ export interface PyreonReactiveDevtools {
56
+ /** Start tracking the live signal/computed/effect graph. Idempotent. */
57
+ activate(): void
58
+ /** Stop tracking + drop all retained registry/timeline state. */
59
+ deactivate(): void
60
+ /** Fresh snapshot of the reactive graph (nodes + edges). */
61
+ getGraph(): ReactiveGraph
62
+ /** Bounded recent-fire timeline (oldest → newest). */
63
+ getFires(): ReactiveFire[]
36
64
  }
37
65
 
38
66
  // ─── Internal registry ────────────────────────────────────────────────────────
@@ -250,6 +278,13 @@ export function installDevTools(): void {
250
278
 
251
279
  enableOverlay,
252
280
  disableOverlay,
281
+
282
+ reactive: {
283
+ activate: activateReactiveDevtools,
284
+ deactivate: deactivateReactiveDevtools,
285
+ getGraph: getReactiveGraph,
286
+ getFires: getReactiveFires,
287
+ },
253
288
  }
254
289
 
255
290
  // Attach to window — compatible with browser devtools extensions
package/src/index.ts CHANGED
@@ -24,7 +24,15 @@ export {
24
24
  sanitizeHtml,
25
25
  setSanitizer,
26
26
  } from './props'
27
- export { _bindDirect, _bindText, _mountSlot, _tpl, createTemplate } from './template'
27
+ export {
28
+ _bindDirect,
29
+ _bindText,
30
+ _mountSlot,
31
+ _rsCollapse,
32
+ _rsCollapseH,
33
+ _tpl,
34
+ createTemplate,
35
+ } from './template'
28
36
  export type { TransitionProps } from './transition'
29
37
  export { Transition } from './transition'
30
38
  export type { TransitionGroupProps } from './transition-group'
package/src/manifest.ts CHANGED
@@ -192,6 +192,24 @@ setSanitizer(DOMPurify.sanitize)
192
192
  const clean = sanitizeHtml(userInput)`,
193
193
  seeAlso: ['setSanitizer'],
194
194
  },
195
+ {
196
+ name: '__PYREON_DEVTOOLS__',
197
+ kind: 'constant',
198
+ signature:
199
+ 'window.__PYREON_DEVTOOLS__: { version; getComponentTree(); getAllComponents(); highlight(id); onComponentMount(cb); onComponentUnmount(cb); enableOverlay(); disableOverlay(); reactive: PyreonReactiveDevtools }',
200
+ summary:
201
+ 'Browser devtools hook, installed automatically on the first `mount()` (no-op on the server). Exposes the component tree + an element-picker overlay (also `Ctrl+Shift+P`) for the `@pyreon/devtools` Chrome extension, plus a `$p` console helper. The `reactive` namespace bridges `@pyreon/reactivity`’s opt-in graph: `reactive.activate()` / `deactivate()` start/stop tracking, `reactive.getGraph()` returns the live signal/computed/effect nodes + dependency edges, `reactive.getFires()` the bounded fire timeline — powering the extension’s Signals / Graph / Effects / Profiler / Console tabs. **Dev-only and tree-shaken from production builds**; `reactive` is zero-cost until `activate()` is called by an attached panel.',
202
+ example: `// In the browser console (after the app has mounted):
203
+ $p.tree() // root component entries
204
+ window.__PYREON_DEVTOOLS__.reactive.activate()
205
+ window.__PYREON_DEVTOOLS__.reactive.getGraph() // { nodes, edges }`,
206
+ mistakes: [
207
+ 'Reading it before the first `mount()` — it is installed by mount; it is `undefined` until then (and always `undefined` on the server / in production builds)',
208
+ 'Expecting `reactive.getGraph()` to return data without calling `reactive.activate()` first — tracking is opt-in (zero-cost until a panel attaches)',
209
+ 'Depending on it in app code — it is a dev-tooling hook, tree-shaken in production; never branch runtime behavior on its presence',
210
+ ],
211
+ seeAlso: ['mount'],
212
+ },
195
213
  ],
196
214
  gotchas: [
197
215
  {
package/src/props.ts CHANGED
@@ -264,6 +264,22 @@ function applyEventProp(el: Element, key: string, value: unknown): Cleanup | nul
264
264
  return () => el.removeEventListener(eventName, batched)
265
265
  }
266
266
 
267
+ /**
268
+ * Bind ONE event handler through the CANONICAL event path
269
+ * (`applyEventProp` — the same delegation, batching, and exact
270
+ * `onXxx`→event-name normalization every compiler-emitted handler
271
+ * uses). PR 2 of the partial-collapse build (open-work #1): a
272
+ * collapsed-with-handler site (`_rsCollapseH`) re-attaches the residual
273
+ * handlers `detectPartialCollapsibleShape` (compiler PR 1) peeled off.
274
+ * Contract-consistent BY CONSTRUCTION — it IS `applyEventProp`, not a
275
+ * re-implementation — so a partially-collapsed `<Button onClick=…>`
276
+ * behaves byte-identically to the 5-layer mount it replaced (same
277
+ * delegated-event prop slot, same `batch()` wrapping, same cleanup).
278
+ */
279
+ export function _bindEvent(el: Element, key: string, handler: unknown): Cleanup | null {
280
+ return applyEventProp(el, key, handler)
281
+ }
282
+
267
283
  /**
268
284
  * Sink for a single prop's CALLED value (always a primitive / object /
269
285
  * `null` — never a function). Called both directly for static values and
package/src/template.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { NativeItem, VNodeChild } from '@pyreon/core'
2
2
  import { renderEffect } from '@pyreon/reactivity'
3
3
  import { mountChild } from './mount'
4
+ import { _bindEvent } from './props'
4
5
 
5
6
  // Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
6
7
  // uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
@@ -193,6 +194,105 @@ export function _tpl(html: string, bind: (el: HTMLElement) => (() => void) | nul
193
194
  return { __isNative: true, el, cleanup }
194
195
  }
195
196
 
197
+ /**
198
+ * Compiler-emitted collapsed rocketstyle call site.
199
+ *
200
+ * The runtime half of the P0 compile-time rocketstyle wrapper-collapse.
201
+ * For a literal-prop call site like `<Button state="primary" size="md">Save</Button>`,
202
+ * the build resolves the FULL rocketstyle/styler pipeline once (SSR
203
+ * render of the real component) and the compiler emits ONE `_rsCollapse`
204
+ * call instead of the 5-layer wrapper mount (rocketstyle → attrs HOC →
205
+ * Element → Wrapper → styled). Measured 44× wall-clock, mountChild 9→1
206
+ * (see examples/experiments/e2-static-rocketstyle/RESULTS.md).
207
+ *
208
+ * Dual-emit (RFC decision 1): both the light- and dark-resolved class
209
+ * strings are baked in; `isDark` is the app's live mode accessor (the
210
+ * compiler threads it from the configured provider, e.g. `useMode` from
211
+ * `@pyreon/ui-core`). A whole-theme/mode swap re-runs only this binding —
212
+ * no remount — preserving Pyreon's reactive mode-switch contract. The
213
+ * resolved CSS rules are injected once at module-eval via the styler's
214
+ * idempotent `injectRules()` (emitted alongside this call), so the
215
+ * collapsed site is self-sufficient: no prior runtime mount of the real
216
+ * component is needed to populate the sheet.
217
+ *
218
+ * `bind` is the standard `_tpl` child/event binder for the (static)
219
+ * children — identical to what the compiler emits for the non-collapsed
220
+ * template path, so children reactivity / event delegation is unchanged.
221
+ *
222
+ * @param html static element HTML WITHOUT the class attr (class is applied reactively)
223
+ * @param lightClass resolved styler class string for light mode
224
+ * @param darkClass resolved styler class string for dark mode
225
+ * @param isDark app mode accessor — `() => boolean` (true ⇒ dark)
226
+ * @param bind standard _tpl binder for children/events (or null)
227
+ */
228
+ export function _rsCollapse(
229
+ html: string,
230
+ lightClass: string,
231
+ darkClass: string,
232
+ isDark: () => boolean,
233
+ bind?: ((el: HTMLElement) => (() => void) | null) | null,
234
+ ): NativeItem {
235
+ return _tpl(html, (el) => {
236
+ // Reactive class: _bindDirect's plain-callable fallback wraps this in
237
+ // a renderEffect, so reading the mode accessor subscribes to the live
238
+ // mode signal — a mode swap re-runs ONLY this className assignment.
239
+ const disposeClass = _bindDirect(isDark as unknown as { _v?: unknown }, (v) => {
240
+ el.className = v ? darkClass : lightClass
241
+ })
242
+ const disposeChildren = bind ? bind(el) : null
243
+ if (!disposeChildren) return disposeClass
244
+ return () => {
245
+ disposeClass()
246
+ disposeChildren()
247
+ }
248
+ })
249
+ }
250
+
251
+ /**
252
+ * Compiler-emitted PARTIALLY-collapsed rocketstyle call site — PR 2 of
253
+ * the partial-collapse build (`.claude/plans/open-work-2026-q3.md` → #1).
254
+ *
255
+ * Identical to {@link _rsCollapse} (one `_tpl` cloneNode, dual-emit
256
+ * reactive class, no remount on mode swap) PLUS it re-attaches the
257
+ * residual event handlers `detectPartialCollapsibleShape` (compiler
258
+ * PR 1) peeled off the `on*`-handler-only subset (the 7.8% the bail
259
+ * census measured). Handlers are orthogonal to the SSR-resolved styler
260
+ * class, so `html` / `lightClass` / `darkClass` are byte-identical to a
261
+ * full-collapse site's — the ONLY delta vs `_rsCollapse` is the handler
262
+ * re-attach, routed through the CANONICAL `_bindEvent` → `applyEventProp`
263
+ * path (delegation + batching + name normalization), so the collapsed
264
+ * node behaves byte-identically to the 5-layer mount it replaced.
265
+ *
266
+ * @param handlers `{ onClick: fn, onPointerEnter: fn, … }` — the peeled
267
+ * residual handlers; compiler PR 3 emits this object literal from the
268
+ * sliced source spans `detectPartialCollapsibleShape` returned.
269
+ */
270
+ export function _rsCollapseH(
271
+ html: string,
272
+ lightClass: string,
273
+ darkClass: string,
274
+ isDark: () => boolean,
275
+ handlers: Record<string, unknown>,
276
+ bind?: ((el: HTMLElement) => (() => void) | null) | null,
277
+ ): NativeItem {
278
+ return _tpl(html, (el) => {
279
+ const disposeClass = _bindDirect(isDark as unknown as { _v?: unknown }, (v) => {
280
+ el.className = v ? darkClass : lightClass
281
+ })
282
+ const handlerDisposers: (() => void)[] = []
283
+ for (const key in handlers) {
284
+ const d = _bindEvent(el, key, handlers[key])
285
+ if (d) handlerDisposers.push(d)
286
+ }
287
+ const disposeChildren = bind ? bind(el) : null
288
+ return () => {
289
+ disposeClass()
290
+ for (const d of handlerDisposers) d()
291
+ if (disposeChildren) disposeChildren()
292
+ }
293
+ })
294
+ }
295
+
196
296
  /**
197
297
  * Test-only: clear the template cache. Used by tests that assert on
198
298
  * cache size; never called by runtime code. Not exported from the
@@ -74,7 +74,8 @@ describe('gen-docs — runtime-dom snapshot', () => {
74
74
 
75
75
  it('renders @pyreon/runtime-dom to MCP api-reference entries — one per api[] item', () => {
76
76
  const record = renderApiReferenceEntries(runtimeDomManifest)
77
- expect(Object.keys(record).length).toBe(9)
77
+ // +1: __PYREON_DEVTOOLS__ (reactive-devtools hook surface).
78
+ expect(Object.keys(record).length).toBe(10)
78
79
  expect(Object.keys(record)).toContain('runtime-dom/mount')
79
80
  // Spot-check the flagship API — mount is the primary entry point
80
81
  const mount = record['runtime-dom/mount']!
@@ -0,0 +1,152 @@
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { flush } from '@pyreon/test-utils/browser'
3
+ import { afterEach, describe, expect, it } from 'vitest'
4
+ import { _rsCollapseH, mount } from '../index'
5
+
6
+ // PR 2 of the partial-collapse build (open-work #1), in REAL Chromium.
7
+ // `_rsCollapseH` = `_rsCollapse` (one _tpl cloneNode, dual-emit reactive
8
+ // class, no remount) PLUS it re-attaches the residual `on*` handlers the
9
+ // compiler's `detectPartialCollapsibleShape` (PR 1) peeled off. The ONLY
10
+ // delta vs `_rsCollapse` is the handler re-attach, routed through the
11
+ // canonical `_bindEvent` → `applyEventProp` path — so a partially-
12
+ // collapsed `<Button onClick=…>` behaves byte-identically to the
13
+ // 5-layer mount it replaced. These specs prove exactly that delta in a
14
+ // real browser (real click/pointer events, real computed style).
15
+ //
16
+ // Bisect-verify (PR body): neutralize the handler loop in
17
+ // `_rsCollapseH` (`for (const key in handlers)` → no-op) → the 3
18
+ // handler specs fail (`expected 0 to be 1` — handler never fired) while
19
+ // every class/mode/no-remount assertion still passes; restore → all
20
+ // pass. That asymmetry proves the handler re-attach is the load-bearing
21
+ // addition, not passing for the wrong reason.
22
+
23
+ describe('_rsCollapseH (real browser) — PR 2 partial-collapse runtime', () => {
24
+ const cleanup: Array<() => void> = []
25
+ afterEach(() => {
26
+ for (const u of cleanup.splice(0)) u()
27
+ })
28
+
29
+ function injectCss(css: string): void {
30
+ const el = document.createElement('style')
31
+ el.textContent = css
32
+ document.head.appendChild(el)
33
+ cleanup.push(() => el.remove())
34
+ }
35
+
36
+ function mountInto(node: ReturnType<typeof _rsCollapseH>): HTMLElement {
37
+ const root = document.createElement('div')
38
+ document.body.appendChild(root)
39
+ const dispose = mount(node as unknown as Parameters<typeof mount>[0], root)
40
+ cleanup.push(() => {
41
+ dispose()
42
+ root.remove()
43
+ })
44
+ return root
45
+ }
46
+
47
+ it('applies the light class AND fires the peeled onClick on a real click', async () => {
48
+ injectCss('.rsh-l{color:rgb(1,2,3)}.rsh-d{color:rgb(9,8,7)}')
49
+ const isDark = signal(false)
50
+ let clicks = 0
51
+ const root = mountInto(
52
+ _rsCollapseH('<button>Save</button>', 'rsh-l', 'rsh-d', () => isDark(), {
53
+ onClick: () => {
54
+ clicks++
55
+ },
56
+ }),
57
+ )
58
+ await flush()
59
+ const btn = root.querySelector('button') as HTMLButtonElement
60
+ expect(btn).not.toBeNull()
61
+ expect(btn.className).toBe('rsh-l')
62
+ expect(btn.textContent).toBe('Save')
63
+ expect(getComputedStyle(btn).color).toBe('rgb(1, 2, 3)')
64
+
65
+ btn.click()
66
+ expect(clicks).toBe(1)
67
+ btn.click()
68
+ expect(clicks).toBe(2)
69
+ })
70
+
71
+ it('mode flip swaps the class on the SAME node AND the handler survives the flip', async () => {
72
+ injectCss('.rsh-l2{color:rgb(10,20,30)}.rsh-d2{color:rgb(40,50,60)}')
73
+ const isDark = signal(false)
74
+ let clicks = 0
75
+ const root = mountInto(
76
+ _rsCollapseH('<button>X</button>', 'rsh-l2', 'rsh-d2', () => isDark(), {
77
+ onClick: () => {
78
+ clicks++
79
+ },
80
+ }),
81
+ )
82
+ await flush()
83
+ const before = root.querySelector('button') as HTMLButtonElement
84
+ expect(before.className).toBe('rsh-l2')
85
+ before.click()
86
+ expect(clicks).toBe(1)
87
+
88
+ isDark.set(true)
89
+ await flush()
90
+ const after = root.querySelector('button') as HTMLButtonElement
91
+ expect(after).toBe(before) // node identity preserved ⇒ reactive, not remount
92
+ expect(after.className).toBe('rsh-d2')
93
+ // The load-bearing partial-collapse contract: the reactive class
94
+ // binding does NOT remount, so the handler attached at first mount is
95
+ // still live after the mode flip.
96
+ after.click()
97
+ expect(clicks).toBe(2)
98
+ })
99
+
100
+ it('peels + binds MULTIPLE handlers with correct onXxx→event normalization', async () => {
101
+ injectCss('.rsh-m{color:rgb(0,0,0)}')
102
+ const isDark = signal(false)
103
+ let clicks = 0
104
+ let enters = 0
105
+ const root = mountInto(
106
+ _rsCollapseH('<button>M</button>', 'rsh-m', 'rsh-m', () => isDark(), {
107
+ onClick: () => {
108
+ clicks++
109
+ },
110
+ // onPointerEnter must normalize to the lowercase DOM event
111
+ // `pointerenter` via the canonical path — a hand-rolled
112
+ // `addEventListener('pointerEnter', …)` would never fire.
113
+ onPointerEnter: () => {
114
+ enters++
115
+ },
116
+ }),
117
+ )
118
+ await flush()
119
+ const btn = root.querySelector('button') as HTMLButtonElement
120
+ btn.click()
121
+ btn.dispatchEvent(new PointerEvent('pointerenter', { bubbles: false }))
122
+ expect(clicks).toBe(1)
123
+ expect(enters).toBe(1)
124
+ })
125
+
126
+ it('dispose removes the listener (no leak) — composed cleanup is correct', async () => {
127
+ injectCss('.rsh-c{color:rgb(5,5,5)}')
128
+ const isDark = signal(false)
129
+ let clicks = 0
130
+ const root = document.createElement('div')
131
+ document.body.appendChild(root)
132
+ const dispose = mount(
133
+ _rsCollapseH('<button>C</button>', 'rsh-c', 'rsh-c', () => isDark(), {
134
+ onClick: () => {
135
+ clicks++
136
+ },
137
+ }) as unknown as Parameters<typeof mount>[0],
138
+ root,
139
+ )
140
+ await flush()
141
+ const btn = root.querySelector('button') as HTMLButtonElement
142
+ btn.click()
143
+ expect(clicks).toBe(1)
144
+
145
+ dispose()
146
+ // After dispose the handler disposer ran — the listener is gone, so a
147
+ // post-dispose click does NOT increment.
148
+ btn.click()
149
+ expect(clicks).toBe(1)
150
+ root.remove()
151
+ })
152
+ })
@@ -0,0 +1,237 @@
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { afterEach, describe, expect, it } from 'vitest'
3
+ import { _rsCollapseH } from '../template'
4
+
5
+ // PR 2 of the partial-collapse build (open-work #1) — happy-dom UNIT
6
+ // layer (fast + locally bisect-verifiable). Imports ONLY `../template`
7
+ // (template.test.ts style) — NOT `../index`, whose wide cross-package
8
+ // re-export graph hits the documented fresh-worktree resolution trap.
9
+ //
10
+ // Event-path split (honest, per test-environment-parity — both layers):
11
+ // - DELEGATED events (`click`, see `delegate.ts:DELEGATED_EVENTS`)
12
+ // park in a prop-slot that only fires via the root listener
13
+ // `mount()` installs → covered by `rs-collapse-h.browser.test.ts`
14
+ // (real Chromium + real `mount()`, CI-authoritative; can't run in a
15
+ // fresh worktree — `@pyreon/test-utils/browser` needs built lib).
16
+ // - NON-delegated events (`pointerenter`, `mouseenter` — NOT in
17
+ // `DELEGATED_EVENTS`) take `applyEventProp`'s direct
18
+ // `el.addEventListener(name, …)` path → fire on a bare
19
+ // `dispatchEvent` with NO `mount()`. This unit drives the
20
+ // handler-attach delta on THAT path: it still proves `_rsCollapseH`
21
+ // routes residual handlers through the canonical
22
+ // `_bindEvent`→`applyEventProp` path (incl. the exact
23
+ // `onXxx`→lowercase normalization) + composed cleanup — the
24
+ // load-bearing addition vs `_rsCollapse`.
25
+ //
26
+ // `_rsCollapseH` returns a NativeItem (`{ __isNative, el, cleanup }`);
27
+ // `_tpl` runs the bind synchronously at call time, so class + handlers
28
+ // are live on `.el` immediately (same direct-NativeItem shape
29
+ // `template.test.ts` uses for `_tpl`). Signal writes propagate
30
+ // synchronously here (cf. `template.test.ts:_bindText`); a
31
+ // `Promise.resolve()` tick covers any microtask-scheduled reactive
32
+ // class update defensively.
33
+ //
34
+ // Bisect-verify (PR body): neutralize the handler loop in
35
+ // `_rsCollapseH` (`for (const key in handlers)` body → skip) → the
36
+ // handler/normalization/cleanup specs fail with `expected 0 to be 1`
37
+ // while every class / mode-flip / no-remount / zero-handler / child
38
+ // assertion still passes; restore → 8/8. The asymmetry proves the
39
+ // handler re-attach is the load-bearing delta.
40
+
41
+ const tick = (): Promise<void> => Promise.resolve()
42
+
43
+ describe('_rsCollapseH (happy-dom unit) — PR 2 partial-collapse runtime', () => {
44
+ const cleanup: Array<() => void> = []
45
+ afterEach(() => {
46
+ for (const u of cleanup.splice(0)) u()
47
+ })
48
+
49
+ function place(item: ReturnType<typeof _rsCollapseH>): HTMLElement {
50
+ const el = item.el as HTMLElement
51
+ document.body.appendChild(el)
52
+ cleanup.push(() => {
53
+ item.cleanup?.()
54
+ el.remove()
55
+ })
56
+ return el
57
+ }
58
+
59
+ const fire = (el: HTMLElement, type: string): void => {
60
+ el.dispatchEvent(new Event(type, { bubbles: false }))
61
+ }
62
+
63
+ it('sets the light class + static children AND fires the peeled handler', () => {
64
+ const isDark = signal(false)
65
+ let enters = 0
66
+ const el = place(
67
+ _rsCollapseH('<button>Save</button>', 'rsh-l', 'rsh-d', () => isDark(), {
68
+ onPointerEnter: () => {
69
+ enters++
70
+ },
71
+ }),
72
+ )
73
+ expect(el.tagName).toBe('BUTTON')
74
+ expect(el.className).toBe('rsh-l')
75
+ expect(el.textContent).toBe('Save')
76
+
77
+ fire(el, 'pointerenter')
78
+ expect(enters).toBe(1)
79
+ fire(el, 'pointerenter')
80
+ expect(enters).toBe(2)
81
+ })
82
+
83
+ it('normalizes onXxx → lowercase DOM event via the canonical path', () => {
84
+ const isDark = signal(false)
85
+ let enters = 0
86
+ let mouse = 0
87
+ const el = place(
88
+ _rsCollapseH('<button>M</button>', 'rsh-m', 'rsh-m', () => isDark(), {
89
+ // onPointerEnter MUST bind to `pointerenter` (lowercased whole
90
+ // name) — a hand-rolled `addEventListener('pointerEnter', …)`
91
+ // would never fire. onMouseEnter → `mouseenter` likewise.
92
+ onPointerEnter: () => {
93
+ enters++
94
+ },
95
+ onMouseEnter: () => {
96
+ mouse++
97
+ },
98
+ }),
99
+ )
100
+ fire(el, 'pointerenter')
101
+ fire(el, 'mouseenter')
102
+ expect(enters).toBe(1)
103
+ expect(mouse).toBe(1)
104
+ // Wrong-case names must NOT fire (proves real normalization, not luck).
105
+ fire(el, 'pointerEnter')
106
+ fire(el, 'mouseEnter')
107
+ expect(enters).toBe(1)
108
+ expect(mouse).toBe(1)
109
+ })
110
+
111
+ it('mode flip swaps the class on the SAME node AND the handler survives the flip', async () => {
112
+ const isDark = signal(false)
113
+ let enters = 0
114
+ const el = place(
115
+ _rsCollapseH('<button>X</button>', 'rsh-l2', 'rsh-d2', () => isDark(), {
116
+ onPointerEnter: () => {
117
+ enters++
118
+ },
119
+ }),
120
+ )
121
+ expect(el.className).toBe('rsh-l2')
122
+ fire(el, 'pointerenter')
123
+ expect(enters).toBe(1)
124
+
125
+ isDark.set(true)
126
+ await tick()
127
+ // Same node identity ⇒ reactive class swap, NOT a remount.
128
+ expect(el.className).toBe('rsh-d2')
129
+ // Load-bearing partial-collapse contract: no remount ⇒ the handler
130
+ // bound at construction is still live after the mode flip.
131
+ fire(el, 'pointerenter')
132
+ expect(enters).toBe(2)
133
+
134
+ isDark.set(false)
135
+ await tick()
136
+ expect(el.className).toBe('rsh-l2')
137
+ })
138
+
139
+ it('cleanup() removes the listener — composed disposer is correct (no leak)', () => {
140
+ const isDark = signal(false)
141
+ let enters = 0
142
+ const item = _rsCollapseH('<button>C</button>', 'rsh-c', 'rsh-c', () => isDark(), {
143
+ onPointerEnter: () => {
144
+ enters++
145
+ },
146
+ })
147
+ const el = item.el as HTMLElement
148
+ document.body.appendChild(el)
149
+ fire(el, 'pointerenter')
150
+ expect(enters).toBe(1)
151
+
152
+ item.cleanup?.()
153
+ fire(el, 'pointerenter') // listener removed by the composed disposer
154
+ expect(enters).toBe(1)
155
+ el.remove()
156
+ })
157
+
158
+ it('binds MULTIPLE handlers; literal props stay out of the handler set', () => {
159
+ const isDark = signal(false)
160
+ let a = 0
161
+ let b = 0
162
+ const el = place(
163
+ _rsCollapseH('<button>N</button>', 'rsh-n', 'rsh-n', () => isDark(), {
164
+ onPointerEnter: () => {
165
+ a++
166
+ },
167
+ onMouseEnter: () => {
168
+ b++
169
+ },
170
+ }),
171
+ )
172
+ fire(el, 'pointerenter')
173
+ fire(el, 'mouseenter')
174
+ fire(el, 'pointerenter')
175
+ expect(a).toBe(2)
176
+ expect(b).toBe(1)
177
+ })
178
+
179
+ it('zero handlers: renders class + children, cleanup is safe (defensive)', async () => {
180
+ const isDark = signal(false)
181
+ const el = place(_rsCollapseH('<button>Z</button>', 'rsh-z', 'rsh-zd', () => isDark(), {}))
182
+ expect(el.className).toBe('rsh-z')
183
+ expect(el.textContent).toBe('Z')
184
+ isDark.set(true)
185
+ await tick()
186
+ expect(el.className).toBe('rsh-zd')
187
+ // cleanup runs via afterEach — must not throw with no handlers.
188
+ })
189
+
190
+ it('children bind runs alongside the class + handler binds', () => {
191
+ const isDark = signal(false)
192
+ let enters = 0
193
+ const el = place(
194
+ _rsCollapseH(
195
+ '<button><span></span></button>',
196
+ 'rsh-cb',
197
+ 'rsh-cbd',
198
+ () => isDark(),
199
+ { onPointerEnter: () => enters++ },
200
+ (root) => {
201
+ ;(root.querySelector('span') as HTMLElement).textContent = 'child'
202
+ return null
203
+ },
204
+ ),
205
+ )
206
+ expect(el.className).toBe('rsh-cb')
207
+ expect((el.querySelector('span') as HTMLElement).textContent).toBe('child')
208
+ fire(el, 'pointerenter')
209
+ expect(enters).toBe(1)
210
+ })
211
+
212
+ it('disposes class + handler + child binds together (composition)', () => {
213
+ const isDark = signal(false)
214
+ let enters = 0
215
+ let childDisposed = false
216
+ const item = _rsCollapseH(
217
+ '<button><span></span></button>',
218
+ 'rsh-x',
219
+ 'rsh-xd',
220
+ () => isDark(),
221
+ { onPointerEnter: () => enters++ },
222
+ () => () => {
223
+ childDisposed = true
224
+ },
225
+ )
226
+ const el = item.el as HTMLElement
227
+ document.body.appendChild(el)
228
+ fire(el, 'pointerenter')
229
+ expect(enters).toBe(1)
230
+
231
+ item.cleanup?.()
232
+ expect(childDisposed).toBe(true) // child disposer composed + ran
233
+ fire(el, 'pointerenter')
234
+ expect(enters).toBe(1) // handler disposer ran too
235
+ el.remove()
236
+ })
237
+ })
@@ -0,0 +1,128 @@
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { flush } from '@pyreon/test-utils/browser'
3
+ import { afterEach, describe, expect, it } from 'vitest'
4
+ import { _rsCollapse, mount } from '../index'
5
+
6
+ // Layer 2 of the P0 rocketstyle-collapse slice, in real Chromium.
7
+ // `_rsCollapse` deliberately does NOT import @pyreon/styler (layer
8
+ // purity — runtime-dom is layer 4; the styler injection is the EMITTED
9
+ // code's job). So this suite injects CSS via a plain <style> and proves
10
+ // only what _rsCollapse owns: ONE _tpl() cloneNode whose class is
11
+ // reactively bound to the app mode (dual-emit, RFC decision 1) — a mode
12
+ // flip swaps the class on the SAME node with no remount.
13
+
14
+ describe('_rsCollapse (real browser)', () => {
15
+ const cleanup: Array<() => void> = []
16
+ afterEach(() => {
17
+ for (const u of cleanup.splice(0)) u()
18
+ })
19
+
20
+ function injectCss(css: string): void {
21
+ const el = document.createElement('style')
22
+ el.textContent = css
23
+ document.head.appendChild(el)
24
+ cleanup.push(() => el.remove())
25
+ }
26
+
27
+ function mountInto(node: ReturnType<typeof _rsCollapse>): HTMLElement {
28
+ const root = document.createElement('div')
29
+ document.body.appendChild(root)
30
+ const dispose = mount(node as unknown as Parameters<typeof mount>[0], root)
31
+ cleanup.push(() => {
32
+ dispose()
33
+ root.remove()
34
+ })
35
+ return root
36
+ }
37
+
38
+ it('applies the light class + static children; the class is real CSS', async () => {
39
+ injectCss('.rsc-light{color:rgb(1,2,3)}.rsc-dark{color:rgb(9,8,7)}')
40
+ const isDark = signal(false)
41
+ const root = mountInto(
42
+ _rsCollapse('<button>Save</button>', 'rsc-light', 'rsc-dark', () => isDark()),
43
+ )
44
+ await flush()
45
+ const btn = root.querySelector('button')
46
+ expect(btn).not.toBeNull()
47
+ expect(btn?.className).toBe('rsc-light')
48
+ expect(btn?.textContent).toBe('Save')
49
+ expect(getComputedStyle(btn as Element).color).toBe('rgb(1, 2, 3)')
50
+ })
51
+
52
+ it('mode flip swaps to the dark class on the SAME node (no remount)', async () => {
53
+ injectCss('.rsc-l2{color:rgb(10,20,30)}.rsc-d2{color:rgb(40,50,60)}')
54
+ const isDark = signal(false)
55
+ const root = mountInto(
56
+ _rsCollapse('<button>X</button>', 'rsc-l2', 'rsc-d2', () => isDark()),
57
+ )
58
+ await flush()
59
+ const before = root.querySelector('button') as HTMLElement
60
+ expect(before.className).toBe('rsc-l2')
61
+
62
+ isDark.set(true)
63
+ await flush()
64
+ const after = root.querySelector('button') as HTMLElement
65
+ expect(after).toBe(before) // node identity preserved ⇒ reactive, not remount
66
+ expect(after.className).toBe('rsc-d2')
67
+ expect(getComputedStyle(after).color).toBe('rgb(40, 50, 60)')
68
+
69
+ isDark.set(false)
70
+ await flush()
71
+ expect((root.querySelector('button') as HTMLElement).className).toBe('rsc-l2')
72
+ })
73
+
74
+ it('children bind runs alongside the class bind and disposes cleanly', async () => {
75
+ injectCss('.rsc-c{color:rgb(2,2,2)}.rsc-cd{color:rgb(3,3,3)}')
76
+ const label = signal('one')
77
+ const isDark = signal(false)
78
+ let childDisposed = false
79
+ const root = mountInto(
80
+ _rsCollapse(
81
+ '<button><span></span></button>',
82
+ 'rsc-c',
83
+ 'rsc-cd',
84
+ () => isDark(),
85
+ (el) => {
86
+ const span = el.querySelector('span') as HTMLElement
87
+ const stop = (() => {
88
+ // minimal reactive child without pulling the compiler in
89
+ let raf = 0
90
+ const tick = () => {
91
+ span.textContent = label()
92
+ raf = requestAnimationFrame(tick)
93
+ }
94
+ tick()
95
+ return () => {
96
+ cancelAnimationFrame(raf)
97
+ childDisposed = true
98
+ }
99
+ })()
100
+ return stop
101
+ },
102
+ ),
103
+ )
104
+ await flush()
105
+ expect((root.querySelector('span') as HTMLElement).textContent).toBe('one')
106
+ expect((root.querySelector('button') as HTMLElement).className).toBe('rsc-c')
107
+ // dispose via afterEach → child cleanup must fire
108
+ for (const u of cleanup.splice(0)) u()
109
+ expect(childDisposed).toBe(true)
110
+ })
111
+
112
+ it('two instances of the same html share ONE parsed template, with independent reactivity', async () => {
113
+ injectCss('.rsc-s{color:rgb(7,7,7)}.rsc-sd{color:rgb(8,8,8)}')
114
+ const isDark = signal(false)
115
+ const r1 = mountInto(_rsCollapse('<button>dup</button>', 'rsc-s', 'rsc-sd', () => isDark()))
116
+ const r2 = mountInto(_rsCollapse('<button>dup</button>', 'rsc-s', 'rsc-sd', () => isDark()))
117
+ await flush()
118
+ const b1 = r1.querySelector('button') as HTMLElement
119
+ const b2 = r2.querySelector('button') as HTMLElement
120
+ expect(b1).not.toBe(b2) // distinct cloned nodes from the shared template
121
+ expect(b1.className).toBe('rsc-s')
122
+ expect(b2.className).toBe('rsc-s')
123
+ isDark.set(true)
124
+ await flush()
125
+ expect((r1.querySelector('button') as HTMLElement).className).toBe('rsc-sd')
126
+ expect((r2.querySelector('button') as HTMLElement).className).toBe('rsc-sd')
127
+ })
128
+ })