@stacksjs/stx 0.2.343 → 0.2.345

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.
Files changed (169) hide show
  1. package/dist/binding-manifest.d.ts +32 -0
  2. package/dist/binding-manifest.js +64 -0
  3. package/dist/{chunk-zr1x54bt.js → chunk-05zfez2g.js} +1 -1
  4. package/dist/{chunk-jagf7h9y.js → chunk-0k1mtwby.js} +1 -1
  5. package/dist/{chunk-jh6f1a9y.js → chunk-1c9453w0.js} +1 -1
  6. package/dist/chunk-1qyg1q6d.js +2 -0
  7. package/dist/chunk-1t4ece3v.js +2 -0
  8. package/dist/{chunk-7zxv2631.js → chunk-1vt1h19d.js} +1 -1
  9. package/dist/chunk-1vtdege3.js +2 -0
  10. package/dist/{chunk-stqm4brp.js → chunk-1vv9g6cj.js} +1 -1
  11. package/dist/{chunk-p3awq80d.js → chunk-1yh6pq6b.js} +1 -1
  12. package/dist/{chunk-57dxdvjh.js → chunk-223makqc.js} +1 -1
  13. package/dist/chunk-2fhk4e68.js +2 -0
  14. package/dist/{chunk-wyhb3nb0.js → chunk-34f8c48f.js} +1 -1
  15. package/dist/chunk-3zm4hgcf.js +2 -0
  16. package/dist/{chunk-xgw269cf.js → chunk-4spaszfn.js} +286 -286
  17. package/dist/chunk-57t402mn.js +2 -0
  18. package/dist/{chunk-wdaf98ya.js → chunk-59906baj.js} +1 -1
  19. package/dist/{chunk-z9hb80mk.js → chunk-64n10kpa.js} +1 -1
  20. package/dist/chunk-66tgkvqw.js +2 -0
  21. package/dist/{chunk-daxr6p6q.js → chunk-6fwp7gee.js} +1 -1
  22. package/dist/{chunk-1bfvdjpy.js → chunk-6h3r8v55.js} +1 -1
  23. package/dist/{chunk-safmgrq5.js → chunk-6wmjwbq9.js} +1 -1
  24. package/dist/{chunk-53sjbp3z.js → chunk-72x62kch.js} +2 -2
  25. package/dist/{chunk-amfq5rnz.js → chunk-72yrrggn.js} +2 -2
  26. package/dist/{chunk-2853ye06.js → chunk-77p1kjg2.js} +2 -2
  27. package/dist/chunk-7g5vk4c3.js +2 -0
  28. package/dist/{chunk-7mc1k1tq.js → chunk-7h9fajgs.js} +1 -1
  29. package/dist/{chunk-gvfgrve8.js → chunk-7m9qk7da.js} +1 -1
  30. package/dist/{chunk-3d3x7dwy.js → chunk-7qp3zphs.js} +1 -1
  31. package/dist/{chunk-grnmr3y1.js → chunk-86c461ca.js} +1 -1
  32. package/dist/chunk-86q2h34b.js +2 -0
  33. package/dist/{chunk-7r79j1hs.js → chunk-89mqnh67.js} +1 -1
  34. package/dist/{chunk-5hq25yky.js → chunk-8cc4vdkh.js} +1 -1
  35. package/dist/chunk-8rhjt6fh.js +2 -0
  36. package/dist/{chunk-45ydb29q.js → chunk-8v2k21n4.js} +1 -1
  37. package/dist/{chunk-aqxwwq6w.js → chunk-8y1ve8tc.js} +2 -2
  38. package/dist/{chunk-ag8gdk51.js → chunk-99b5tfxc.js} +1 -1
  39. package/dist/{chunk-4vd0rqfs.js → chunk-9jbssgem.js} +197 -88
  40. package/dist/{chunk-s60eccqk.js → chunk-9nrgjtah.js} +1 -1
  41. package/dist/{chunk-y5sxa2sq.js → chunk-9qtf2b1p.js} +1 -1
  42. package/dist/chunk-aapqf6bj.js +2 -0
  43. package/dist/{chunk-k8skvm0d.js → chunk-at622hg0.js} +1 -1
  44. package/dist/{chunk-1qtd4ex2.js → chunk-avwywzsj.js} +2 -2
  45. package/dist/{chunk-a0ajg9z5.js → chunk-b5hank5e.js} +1 -1
  46. package/dist/chunk-b5mnam29.js +2 -0
  47. package/dist/{chunk-4fafxr8d.js → chunk-bk1zb1xp.js} +1 -1
  48. package/dist/{chunk-hrdd26r4.js → chunk-bqdfbwdf.js} +1 -1
  49. package/dist/{chunk-c7qdnc5d.js → chunk-c30qnb6z.js} +1 -1
  50. package/dist/chunk-c4tf1vpy.js +2 -0
  51. package/dist/{chunk-g0dbtyk7.js → chunk-c8e2deyp.js} +1 -1
  52. package/dist/chunk-cypg8qwn.js +2 -0
  53. package/dist/{chunk-fbmjw0bh.js → chunk-d86q27j8.js} +1 -1
  54. package/dist/{chunk-dj5mqnp1.js → chunk-dgv43ccd.js} +1 -1
  55. package/dist/{chunk-em8cr7wy.js → chunk-dsztk2kp.js} +1 -1
  56. package/dist/{chunk-mwvxhv0r.js → chunk-dxkjvajv.js} +1 -1
  57. package/dist/{chunk-4pg6wbt6.js → chunk-e631svvs.js} +1 -1
  58. package/dist/chunk-e7gs18rc.js +2 -0
  59. package/dist/{chunk-h6y99nak.js → chunk-e7np0h2q.js} +1 -1
  60. package/dist/{chunk-0s3ry262.js → chunk-ejqpq0wc.js} +1 -1
  61. package/dist/chunk-ek2m0kkw.js +2 -0
  62. package/dist/chunk-eqzc88et.js +2 -0
  63. package/dist/chunk-erb294sv.js +2 -0
  64. package/dist/{chunk-36j9qset.js → chunk-esjcyxa9.js} +2 -2
  65. package/dist/{chunk-5azeswj2.js → chunk-evfr2zgg.js} +1 -1
  66. package/dist/{chunk-e53pwshq.js → chunk-fp868yhg.js} +1 -1
  67. package/dist/{chunk-5zemhhwe.js → chunk-gsc2vds7.js} +1 -1
  68. package/dist/{chunk-q6825wwz.js → chunk-gvemapw4.js} +1 -1
  69. package/dist/chunk-h1vax4wg.js +2 -0
  70. package/dist/chunk-hke0k33t.js +2 -0
  71. package/dist/{chunk-zcytk5w9.js → chunk-jekrtm2j.js} +1 -1
  72. package/dist/{chunk-pwhf5kte.js → chunk-jnbg3e4r.js} +1 -1
  73. package/dist/{chunk-brjfzbq9.js → chunk-kfkf9v2d.js} +1 -1
  74. package/dist/chunk-kq30wqp6.js +2 -0
  75. package/dist/{chunk-s7qbba8r.js → chunk-kqzpmwx2.js} +10 -10
  76. package/dist/{chunk-0dpgh7a6.js → chunk-kw9cgyfc.js} +1 -1
  77. package/dist/{chunk-0fp16c1x.js → chunk-m2j5skez.js} +1 -1
  78. package/dist/{chunk-vj3nx26v.js → chunk-mph46gkj.js} +1 -1
  79. package/dist/chunk-mr8ccsnm.js +2 -0
  80. package/dist/{chunk-1h1c2048.js → chunk-msywnv5n.js} +1 -1
  81. package/dist/{chunk-649dfqqz.js → chunk-n5657hm3.js} +1 -1
  82. package/dist/{chunk-t00ga194.js → chunk-njx93b4z.js} +1 -1
  83. package/dist/{chunk-m0xaw0qw.js → chunk-nm60wh6r.js} +1 -1
  84. package/dist/{chunk-8ah5mhst.js → chunk-nqz501eq.js} +2 -2
  85. package/dist/{chunk-hq3phk1a.js → chunk-nrt99ptw.js} +1 -1
  86. package/dist/{chunk-62vng8g8.js → chunk-pajk5680.js} +2 -2
  87. package/dist/{chunk-ey1tsk22.js → chunk-ppzz953j.js} +1 -1
  88. package/dist/{chunk-7yfk9393.js → chunk-qkdcbeaw.js} +1 -1
  89. package/dist/chunk-qr3zcm87.js +2 -0
  90. package/dist/chunk-r38xq7fp.js +33 -0
  91. package/dist/{chunk-tqw1yean.js → chunk-rwwfzkz4.js} +1 -1
  92. package/dist/{chunk-rns7wjy9.js → chunk-rzda6bd6.js} +1 -1
  93. package/dist/{chunk-rzg9pars.js → chunk-san1eq0r.js} +2 -2
  94. package/dist/{chunk-eh3meyd3.js → chunk-tc4pj651.js} +2 -2
  95. package/dist/{chunk-6g8rvnhc.js → chunk-v6e35j4g.js} +1 -1
  96. package/dist/{chunk-wnmegbw0.js → chunk-v7aa7v85.js} +1 -1
  97. package/dist/{chunk-e6rxvz3p.js → chunk-vhg91hnw.js} +1 -1
  98. package/dist/{chunk-k3dtvay1.js → chunk-vhta1be7.js} +1 -1
  99. package/dist/{chunk-tz68qv47.js → chunk-w5j9tbsk.js} +1 -1
  100. package/dist/{chunk-ec18ftcg.js → chunk-wj21f1e0.js} +1 -1
  101. package/dist/{chunk-12qteqk8.js → chunk-ws0282jr.js} +1 -1
  102. package/dist/chunk-x054qzma.js +2 -0
  103. package/dist/{chunk-q9ckfwhh.js → chunk-xj6bcwqa.js} +1 -1
  104. package/dist/chunk-xq0e2y4x.js +2 -0
  105. package/dist/chunk-xrjx7b2v.js +2 -0
  106. package/dist/{chunk-ay336sqb.js → chunk-xvh1k1a4.js} +1 -1
  107. package/dist/{chunk-vaggjvsm.js → chunk-y2f41kaq.js} +1 -1
  108. package/dist/{chunk-1zav2sf1.js → chunk-y6xzr30x.js} +1 -1
  109. package/dist/chunk-ybx8n794.js +2 -0
  110. package/dist/{chunk-yefnrr5z.js → chunk-yca761bm.js} +1 -1
  111. package/dist/{chunk-kgbz86sa.js → chunk-yg5r7bnm.js} +2 -2
  112. package/dist/{chunk-kbah2k8e.js → chunk-z8wb7kbj.js} +2 -2
  113. package/dist/chunk-zkxnarrh.js +2 -0
  114. package/dist/{chunk-hzbty7hz.js → chunk-zmazqcb8.js} +1 -1
  115. package/dist/chunk-zp14ksx1.js +2 -0
  116. package/dist/chunk-zwr4bn6a.js +2 -0
  117. package/dist/cli.js +14 -14
  118. package/dist/craft-entry.js +1 -1
  119. package/dist/craft.js +1 -1
  120. package/dist/database.js +1 -1
  121. package/dist/expressions.d.ts +1 -1
  122. package/dist/expressions.js +15 -3
  123. package/dist/includes.js +2 -2
  124. package/dist/pwa.js +2 -2
  125. package/dist/runtime-globals.d.ts +22 -0
  126. package/dist/runtime-globals.js +46 -0
  127. package/dist/signals.js +193 -84
  128. package/dist/slots.js +3 -3
  129. package/dist/ssg.js +2 -2
  130. package/dist/theme-tokens.d.ts +59 -0
  131. package/dist/theme-tokens.js +83 -17
  132. package/dist/ts-css-config.js +4 -2
  133. package/dist/utils.d.ts +10 -0
  134. package/dist/utils.js +77 -12
  135. package/dist/variable-extractor.d.ts +1 -1
  136. package/dist/variable-extractor.js +23 -3
  137. package/dist/visual-testing.js +1 -1
  138. package/package.json +5 -5
  139. package/dist/chunk-160g7ynw.js +0 -2
  140. package/dist/chunk-16hr5y08.js +0 -2
  141. package/dist/chunk-3h97y3n3.js +0 -2
  142. package/dist/chunk-3j915h2d.js +0 -2
  143. package/dist/chunk-4hkg6kx4.js +0 -2
  144. package/dist/chunk-57494khk.js +0 -2
  145. package/dist/chunk-5xavmr4t.js +0 -2
  146. package/dist/chunk-6rsfkms1.js +0 -2
  147. package/dist/chunk-6xs80tka.js +0 -32
  148. package/dist/chunk-905j49fe.js +0 -2
  149. package/dist/chunk-9czj483k.js +0 -2
  150. package/dist/chunk-9kv0qavn.js +0 -2
  151. package/dist/chunk-bcz8yp2g.js +0 -2
  152. package/dist/chunk-bxefsc7k.js +0 -2
  153. package/dist/chunk-c4kv3r8t.js +0 -2
  154. package/dist/chunk-fytemc96.js +0 -2
  155. package/dist/chunk-g7gc0ten.js +0 -2
  156. package/dist/chunk-gqsxxd81.js +0 -2
  157. package/dist/chunk-gx9z0wmm.js +0 -2
  158. package/dist/chunk-jnxzzzm4.js +0 -2
  159. package/dist/chunk-jy6jwxhs.js +0 -2
  160. package/dist/chunk-khqn40gv.js +0 -2
  161. package/dist/chunk-mhbzrh5y.js +0 -2
  162. package/dist/chunk-p8anntjb.js +0 -2
  163. package/dist/chunk-pwe51myj.js +0 -2
  164. package/dist/chunk-rqqdvnp5.js +0 -2
  165. package/dist/chunk-w1mbrq1g.js +0 -2
  166. package/dist/chunk-w84qe2m6.js +0 -2
  167. package/dist/chunk-wjm7hv5e.js +0 -2
  168. package/dist/chunk-yr7s02f7.js +0 -2
  169. package/dist/chunk-zq3tm2c2.js +0 -2
package/dist/signals.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { state, derived, effect, batch, onMount, onDestroy, isSignal, isDerived, untrack, peek } from "./signals-api.js";
2
- import { runtimeHandledXAttrsLiteral } from "./runtime-globals.js";
2
+ import { runtimeDirectiveNamesLiteral, runtimeEventRegexLiteral, runtimeHandledXAttrsLiteral } from "./runtime-globals.js";
3
3
  import { createModelSignal } from "./component-model.js";
4
4
  import { readHydratedData, clearServerData } from "./composables/use-fetch.js";
5
5
 
@@ -64,6 +64,82 @@ console.log('[stx] entering IIFE');
64
64
  (function() {
65
65
  'use strict';
66
66
 
67
+ // \u2500\u2500 Host \u2500\u2500
68
+ //
69
+ // The tree this runtime mutates. Every write the binding layer makes to a
70
+ // node goes through here, so the same runtime can drive something that is not
71
+ // the document -- native views over a bridge (stacksjs/stx#1984).
72
+ //
73
+ // Signals never touch a node, so nothing above this line needs to change; the
74
+ // coupling is entirely in the bind layer. A host is swapped in by assigning
75
+ // window.__stx_host before the runtime executes. The name is stxHost, not
76
+ // host, because the runtime already calls two local DOM nodes host -- a
77
+ // shadowed adapter fails as node.anchor is not a function, deep inside a
78
+ // branch swap, and nowhere near the line that caused it, which keeps the web path
79
+ // exactly as it was: same calls, one property lookup earlier.
80
+ var stxHost = window.__stx_host || {
81
+ setText: function(node, value) { node.textContent = value; },
82
+ setAttribute: function(node, name, value) { node.setAttribute(name, value); },
83
+ removeAttribute: function(node, name) { node.removeAttribute(name); },
84
+
85
+ // Structural operations, for the bindings that build rows rather than
86
+ // update one node (#1984).
87
+ //
88
+ // anchor() is a position in the child list that survives the nodes around
89
+ // it moving -- a comment node here, since the document has one; a native
90
+ // host keeps an index or a zero-size sentinel.
91
+ //
92
+ // clone() is the one that does not carry over as-is: a DOM subtree can be
93
+ // deep-copied, a native view hierarchy cannot. A native host materialises a
94
+ // fresh subtree from the template it was given instead of copying views,
95
+ // which is why the row template is passed rather than a node to duplicate.
96
+ // A native host registers the handler against the view and has the platform
97
+ // call it; nothing about the binding layer changes, which is the point.
98
+ // The options argument carries passive/capture/once. A native host has no use for
99
+ // passive -- there is no scroll thread to block -- but dropping the
100
+ // parameter silently makes every touch listener blocking again, so it stays
101
+ // in the signature and each host decides what to do with it.
102
+ listen: function(node, event, handler, options) { node.addEventListener(event, handler, options); },
103
+
104
+ // Returns the PREVIOUS visibility. bindShow needs to know whether it just
105
+ // revealed something, and asking the node afterwards is the one read-back
106
+ // in the binding layer -- a native host cannot answer what a UIView's
107
+ // display value was, because it has none. A write that reports what it
108
+ // replaced removes the question: the DOM host reads its own style, a native
109
+ // host reads isHidden, and the binder reads neither.
110
+ //
111
+ // Restoring an explicit display (a flex row, an inline-block) is the host's
112
+ // business too; it records it the first time it hides the node.
113
+ // The scope root a node belongs to: nearest ancestor-or-self that owns a
114
+ // scope, or null. Three binding paths need it -- ref ownership, and an
115
+ // if-chain finding the scope its branch belongs to.
116
+ //
117
+ // Not a query method. A host is never handed a selector, because a native
118
+ // view tree has nothing to match one against: the DOM host walks with
119
+ // closest(), a native host reads the node-to-scope map it maintains as it
120
+ // creates views. The from flag skips the node itself, which the ref path needs
121
+ // when it walks outward past a receiver it has already rejected.
122
+ scopeOf: function(node, from) {
123
+ var start = from ? (node && node.parentElement) : node;
124
+ return (start && start.closest) ? start.closest('[data-stx-scope]') : null;
125
+ },
126
+
127
+ setVisible: function(node, visible) {
128
+ if (node.__stx_display === undefined) {
129
+ var d = node.style.display;
130
+ node.__stx_display = (d && d !== 'none') ? d : '';
131
+ }
132
+ var was = node.style.display !== 'none';
133
+ node.style.display = visible ? node.__stx_display : 'none';
134
+ return was;
135
+ },
136
+
137
+ anchor: function(label) { return document.createComment(label); },
138
+ clone: function(node) { return node.cloneNode(true); },
139
+ insert: function(parent, node, before) { parent.insertBefore(node, before); },
140
+ remove: function(node) { if (node.parentNode) node.parentNode.removeChild(node); }
141
+ };
142
+
67
143
  // Inject x-cloak CSS to prevent FOUC (Flash of Unstyled Content)
68
144
  // Elements with x-cloak are hidden until the runtime removes the attribute after mount
69
145
  var cloakStyle = document.getElementById ? document.getElementById('stx-cloak-style') : null;
@@ -2572,7 +2648,41 @@ catch (e) {
2572
2648
  return scripts;
2573
2649
  }
2574
2650
 
2575
- function processElement(el, scope = componentScope) {
2651
+ /**
2652
+ * Bind what the server said carries a binding, instead of looking for it.
2653
+ *
2654
+ * The walk exists because the runtime does not know where the bindings are.
2655
+ * The server does -- it rendered them -- so when it ships that list the
2656
+ * runtime stops searching: resolve each id, bind that element, never touch
2657
+ * its children. An element with no bindings is never visited at all.
2658
+ *
2659
+ * This is the piece a native host needs. A view hierarchy has no attributes
2660
+ * to read and no selector to match, so discovery cannot happen there; handed
2661
+ * the list, it does not have to. On the DOM the ids resolve by attribute,
2662
+ * which is one query per entry rather than a full traversal.
2663
+ *
2664
+ * Absent a manifest nothing changes: the walk is still the default, and a
2665
+ * page that ships no list hydrates exactly as it did.
2666
+ */
2667
+ function processFromManifest(root, scope, manifest) {
2668
+ if (!root || !manifest || !manifest.length) return false;
2669
+ for (var i = 0; i < manifest.length; i++) {
2670
+ var entry = manifest[i];
2671
+ var id = entry && entry.id;
2672
+ if (id === undefined || id === null) continue;
2673
+ var node = root.getAttribute && root.getAttribute('data-stx-b') === String(id)
2674
+ ? root
2675
+ : (root.querySelector ? root.querySelector('[data-stx-b="' + id + '"]') : null);
2676
+ if (!node) continue;
2677
+ processElement(node, scope, true);
2678
+ }
2679
+ return true;
2680
+ }
2681
+
2682
+ // skipChildren: this element's bindings only, because something else already
2683
+ // knows where the rest are. The manifest path passes it for every entry, so
2684
+ // the tree is never walked; see processFromManifest.
2685
+ function processElement(el, scope = componentScope, skipChildren) {
2576
2686
  // Lazy hydration: if this element has stx-hydrate and hasn't been hydrated
2577
2687
  // yet, defer its subtree processing until the trigger fires.
2578
2688
  if (el.nodeType === Node.ELEMENT_NODE && el.hasAttribute && el.hasAttribute('stx-hydrate') && !el.__stx_hydrated) {
@@ -2818,7 +2928,7 @@ catch (e2) {
2818
2928
  };
2819
2929
 
2820
2930
  // Known directive names to exclude from generic :attr binding
2821
- var DIRECTIVE_NAMES = {class:1, style:1, text:1, html:1, show:1, model:1, 'if':1, ref:1};
2931
+ var DIRECTIVE_NAMES = ${runtimeDirectiveNamesLiteral()};
2822
2932
 
2823
2933
  // A bound value cannot introduce a URL scheme the page did not ask for.
2824
2934
  // This is the client half of url-safety.ts: the server refuses a scheme
@@ -2853,7 +2963,7 @@ catch (e2) {
2853
2963
  // a dead duplicate attribute and the real viewBox never updates.
2854
2964
  var SVG_ATTR_CASE = {attributename:'attributeName', attributetype:'attributeType', basefrequency:'baseFrequency', baseprofile:'baseProfile', calcmode:'calcMode', clippathunits:'clipPathUnits', diffuseconstant:'diffuseConstant', edgemode:'edgeMode', filterunits:'filterUnits', glyphref:'glyphRef', gradienttransform:'gradientTransform', gradientunits:'gradientUnits', kernelmatrix:'kernelMatrix', kernelunitlength:'kernelUnitLength', keypoints:'keyPoints', keysplines:'keySplines', keytimes:'keyTimes', lengthadjust:'lengthAdjust', limitingconeangle:'limitingConeAngle', markerheight:'markerHeight', markerunits:'markerUnits', markerwidth:'markerWidth', maskcontentunits:'maskContentUnits', maskunits:'maskUnits', numoctaves:'numOctaves', pathlength:'pathLength', patterncontentunits:'patternContentUnits', patterntransform:'patternTransform', patternunits:'patternUnits', pointsatx:'pointsAtX', pointsaty:'pointsAtY', pointsatz:'pointsAtZ', preservealpha:'preserveAlpha', preserveaspectratio:'preserveAspectRatio', primitiveunits:'primitiveUnits', refx:'refX', refy:'refY', repeatcount:'repeatCount', repeatdur:'repeatDur', requiredextensions:'requiredExtensions', requiredfeatures:'requiredFeatures', specularconstant:'specularConstant', specularexponent:'specularExponent', spreadmethod:'spreadMethod', startoffset:'startOffset', stddeviation:'stdDeviation', stitchtiles:'stitchTiles', surfacescale:'surfaceScale', systemlanguage:'systemLanguage', tablevalues:'tableValues', targetx:'targetX', targety:'targetY', textlength:'textLength', viewbox:'viewBox', viewtarget:'viewTarget', xchannelselector:'xChannelSelector', ychannelselector:'yChannelSelector', zoomandpan:'zoomAndPan'};
2855
2965
  var SVG_NS = 'http://www.w3.org/2000/svg';
2856
- var EVENT_RE = /^(click|dblclick|mousedown|mouseup|mousemove|mouseenter|mouseleave|keydown|keyup|keypress|input|change|submit|focus|blur|scroll|resize|touchstart|touchend|touchmove|contextmenu|wheel|pointerdown|pointerup|pointermove)/;
2966
+ var EVENT_RE = ${runtimeEventRegexLiteral()};
2857
2967
  var KEY_MAP = {enter:'Enter', tab:'Tab', escape:'Escape', space:' ', up:'ArrowUp', down:'ArrowDown', left:'ArrowLeft', right:'ArrowRight', 'delete':'Delete', backspace:'Backspace'};
2858
2968
 
2859
2969
  // Handle attributes
@@ -2942,7 +3052,7 @@ catch (e2) {
2942
3052
  el.setAttribute(attrName, String(v));
2943
3053
  }
2944
3054
  else if (v === false || v === null || v === undefined) {
2945
- el.removeAttribute(attrName);
3055
+ stxHost.removeAttribute(el, attrName);
2946
3056
  }
2947
3057
  else if (v === true) {
2948
3058
  el.setAttribute(attrName, '');
@@ -2953,7 +3063,7 @@ else {
2953
3063
  try { attrValue = JSON.stringify(v); }
2954
3064
  catch (e) { attrValue = String(v); }
2955
3065
  }
2956
- el.setAttribute(attrName, safeUrlAttr(attrName, attrValue));
3066
+ stxHost.setAttribute(el, attrName, safeUrlAttr(attrName, attrValue));
2957
3067
  }
2958
3068
  });
2959
3069
  el.removeAttribute(name);
@@ -2968,7 +3078,7 @@ else if (name === '@style' || name === ':style' || name === 'x-style') {
2968
3078
  }
2969
3079
  else if (name === '@text' || name === ':text' || name === 'x-text') {
2970
3080
  effect(() => {
2971
- el.textContent = evalAttrExpr(value);
3081
+ stxHost.setText(el, evalAttrExpr(value));
2972
3082
  });
2973
3083
  el.removeAttribute(name);
2974
3084
  }
@@ -2984,14 +3094,14 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
2984
3094
  // A page ref can be projected through a component whose own $refs
2985
3095
  // replaced the compatibility scope's map. The compiler retains the
2986
3096
  // receiver boundary so named and signal refs use the same caller.
2987
- var refRoot = el.closest && el.closest('[data-stx-scope]');
3097
+ var refRoot = stxHost.scopeOf(el);
2988
3098
  var refCaller = el.getAttribute('data-stx-ref-caller');
2989
3099
  var refOwnerScope = findElementScope(el);
2990
3100
  if (refCaller) {
2991
3101
  var receiver = refRoot;
2992
3102
  while (receiver && receiver.getAttribute('data-stx-scope') !== refCaller
2993
3103
  && receiver.getAttribute('data-stx-template-scope') !== refCaller) {
2994
- receiver = receiver.parentElement && receiver.parentElement.closest('[data-stx-scope]');
3104
+ receiver = stxHost.scopeOf(receiver, true);
2995
3105
  }
2996
3106
  refOwnerScope = receiver && receiver.__stx_parent_scope;
2997
3107
  }
@@ -3042,7 +3152,7 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
3042
3152
  ? { ...globalHelpers, ...el.__stx_parent_scope }
3043
3153
  : { ...globalHelpers, ...scope, ...(findElementScope(el) || {}) };
3044
3154
 
3045
- el.addEventListener(eventName, (event) => {
3155
+ stxHost.listen(el, eventName, (event) => {
3046
3156
  // Vue-style component listeners fall through to the rendered native
3047
3157
  // root unless the child emits the same event. Signal components use
3048
3158
  // a scope wrapper, so native events arrive here from a descendant.
@@ -3165,7 +3275,7 @@ catch (e) {
3165
3275
 
3166
3276
  // Process children (skip script/style elements \u2014 their text content is not template markup)
3167
3277
  // Skip elements that are roots of nested stx.mount() components \u2014 those have their own scope
3168
- if (el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE') {
3278
+ if (!skipChildren && el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE') {
3169
3279
  var children = Array.from(el.childNodes);
3170
3280
  children.forEach(function(child) {
3171
3281
  if (child.nodeType !== Node.ELEMENT_NODE) { processElement(child, scope); return; }
@@ -3243,8 +3353,6 @@ catch (e) {
3243
3353
  if (el.__stx_show_bound) return;
3244
3354
  el.__stx_show_bound = true;
3245
3355
 
3246
- const currentDisplay = el.style.display;
3247
- const originalDisplay = (currentDisplay && currentDisplay !== 'none') ? currentDisplay : '';
3248
3356
  const capturedScope = { ...globalHelpers, ...passedScope, ...(findElementScope(el) || {}) };
3249
3357
 
3250
3358
  // Check if the expression is a simple signal reference (most common case for :show)
@@ -3254,7 +3362,7 @@ catch (e) {
3254
3362
  effect(() => {
3255
3363
  const value = directSignal();
3256
3364
 
3257
- el.style.display = value ? originalDisplay : 'none';
3365
+ stxHost.setVisible(el, !!value);
3258
3366
  });
3259
3367
  } else {
3260
3368
  // Complex expression path \u2014 use createAutoUnwrapProxy with fallback,
@@ -3279,16 +3387,15 @@ catch (e) {
3279
3387
  // or object may not be ready yet on the first effect run, and
3280
3388
  // the next pass will re-evaluate once data arrives.
3281
3389
  if (!(e2 instanceof ReferenceError) && !(e2 instanceof TypeError)) console.warn('[STX] Show expression error:', expr, e2);
3282
- el.style.display = 'none';
3390
+ stxHost.setVisible(el, false);
3283
3391
  return;
3284
3392
  }
3285
3393
  }
3286
- var wasHidden = el.style.display === 'none';
3287
- el.style.display = value ? originalDisplay : 'none';
3394
+ var wasVisible = stxHost.setVisible(el, !!value);
3288
3395
  // When transitioning from hidden to visible, stamp the element so
3289
3396
  // @click handlers can ignore clicks from the same frame (prevents
3290
3397
  // modal backdrop from catching the click that opened the modal)
3291
- if (value && wasHidden) {
3398
+ if (value && !wasVisible) {
3292
3399
  el.__stx_shown_at = performance.now();
3293
3400
  }
3294
3401
  });
@@ -3365,7 +3472,7 @@ catch (e) {
3365
3472
  };
3366
3473
  el.__stx_model_sync = syncCheckbox;
3367
3474
  effect(syncCheckbox);
3368
- el.addEventListener('change', () => {
3475
+ stxHost.listen(el, 'change', () => {
3369
3476
  var current = getValue();
3370
3477
  if (!Array.isArray(current)) {
3371
3478
  setValue(el.checked);
@@ -3382,7 +3489,7 @@ catch (e) {
3382
3489
  var syncRadio = () => { el.checked = String(getValue()) === String(el.value); };
3383
3490
  el.__stx_model_sync = syncRadio;
3384
3491
  effect(syncRadio);
3385
- el.addEventListener('change', () => {
3492
+ stxHost.listen(el, 'change', () => {
3386
3493
  if (el.checked) setValue(coerceValue(el.value));
3387
3494
  });
3388
3495
  }
@@ -3408,7 +3515,7 @@ catch (e) {
3408
3515
  el.__stx_model_observer = null;
3409
3516
  });
3410
3517
  }
3411
- el.addEventListener('change', () => setValue(coerceValue(el.value)));
3518
+ stxHost.listen(el, 'change', () => setValue(coerceValue(el.value)));
3412
3519
  }
3413
3520
  else {
3414
3521
  // Writing el.value moves the text cursor to the END of the control, and
@@ -3446,7 +3553,7 @@ else {
3446
3553
  catch (e) { /* type does not support selection */ }
3447
3554
  }
3448
3555
  });
3449
- el.addEventListener(modifiers.includes('lazy') ? 'change' : 'input', () => setValue(coerceValue(el.value)));
3556
+ stxHost.listen(el, modifiers.includes('lazy') ? 'change' : 'input', () => setValue(coerceValue(el.value)));
3450
3557
  }
3451
3558
 
3452
3559
  el.removeAttribute(attrName);
@@ -3854,7 +3961,7 @@ else if (typeof value === 'string') {
3854
3961
  return;
3855
3962
  }
3856
3963
 
3857
- const placeholder = document.createComment('stx-for');
3964
+ const placeholder = stxHost.anchor('stx-for');
3858
3965
  const isTemplate = el.tagName === 'TEMPLATE';
3859
3966
  const componentSetupTemplates = [];
3860
3967
  if (!isTemplate && el.hasAttribute('data-stx-scope')) {
@@ -3876,8 +3983,8 @@ else if (typeof value === 'string') {
3876
3983
  && setupSibling.hasAttribute('data-stx-scoped')
3877
3984
  && ((setupSibling.textContent || '').indexOf(componentScopeId) !== -1
3878
3985
  || (componentTemplateScopeId && (setupSibling.textContent || '').indexOf(componentTemplateScopeId) !== -1))) {
3879
- componentSetupTemplates.push(setupSibling.cloneNode(true));
3880
- setupSibling.remove();
3986
+ componentSetupTemplates.push(stxHost.clone(setupSibling));
3987
+ stxHost.remove(setupSibling);
3881
3988
  setupSibling = nextSetupSibling;
3882
3989
  continue;
3883
3990
  }
@@ -3900,16 +4007,16 @@ else if (typeof value === 'string') {
3900
4007
  let sibling = el.nextElementSibling;
3901
4008
  while (sibling) {
3902
4009
  if (sibling.hasAttribute('@for-loading')) {
3903
- loadingTemplate = sibling.cloneNode(true);
4010
+ loadingTemplate = stxHost.clone(sibling);
3904
4011
  loadingTemplate.removeAttribute('@for-loading');
3905
- sibling.remove();
4012
+ stxHost.remove(sibling);
3906
4013
  sibling = el.nextElementSibling;
3907
4014
  continue;
3908
4015
  }
3909
4016
  if (sibling.hasAttribute('@for-empty')) {
3910
- emptyTemplate = sibling.cloneNode(true);
4017
+ emptyTemplate = stxHost.clone(sibling);
3911
4018
  emptyTemplate.removeAttribute('@for-empty');
3912
- sibling.remove();
4019
+ stxHost.remove(sibling);
3913
4020
  sibling = el.nextElementSibling;
3914
4021
  continue;
3915
4022
  }
@@ -3919,13 +4026,13 @@ else if (typeof value === 'string') {
3919
4026
  // Capture the scope NOW before element is removed from DOM
3920
4027
  const capturedScope = findElementScope(el) || findElementScope(parent);
3921
4028
 
3922
- parent.insertBefore(placeholder, el);
4029
+ stxHost.insert(parent, placeholder, el);
3923
4030
  // Component setup scripts execute while the server-rendered loop source is
3924
4031
  // still in the document. That source becomes a template, not a live
3925
4032
  // instance, so reclaim all registered scopes before removing it. Every
3926
4033
  // rendered iteration receives remapped setup scripts and fresh scopes.
3927
4034
  disposeSubtreeScopes(el);
3928
- parent.removeChild(el);
4035
+ stxHost.remove(el);
3929
4036
 
3930
4037
  // Capture :key expression BEFORE removing the attribute
3931
4038
  const keyExpr = el.getAttribute(':key') || el.getAttribute('x-bind:key');
@@ -3936,7 +4043,7 @@ else if (typeof value === 'string') {
3936
4043
  templateContent = el.content;
3937
4044
  }
3938
4045
  else {
3939
- const wrapper = el.cloneNode(true);
4046
+ const wrapper = stxHost.clone(el);
3940
4047
  wrapper.removeAttribute('@for');
3941
4048
  wrapper.removeAttribute(':for');
3942
4049
  wrapper.removeAttribute('x-for');
@@ -3989,12 +4096,12 @@ else {
3989
4096
  const removeGroup = (group) => {
3990
4097
  const liveNodes = liveGroupNodes(group);
3991
4098
  const liveSet = new Set(liveNodes);
3992
- liveNodes.forEach(e => { disposeSubtreeEffects(e); disposeSubtreeScopes(e); e.remove(); });
4099
+ liveNodes.forEach(e => { disposeSubtreeEffects(e); disposeSubtreeScopes(e); stxHost.remove(e); });
3993
4100
  group.forEach(groupNode => {
3994
4101
  if (liveSet.has(groupNode)) return;
3995
4102
  disposeSubtreeEffects(groupNode);
3996
4103
  disposeSubtreeScopes(groupNode);
3997
- groupNode.remove();
4104
+ stxHost.remove(groupNode);
3998
4105
  });
3999
4106
  };
4000
4107
 
@@ -4057,15 +4164,15 @@ catch (e) {
4057
4164
  hideLoading();
4058
4165
  hideEmpty();
4059
4166
  if (loadingTemplate) {
4060
- loadingElement = loadingTemplate.cloneNode(true);
4061
- parent.insertBefore(loadingElement, placeholder);
4167
+ loadingElement = stxHost.clone(loadingTemplate);
4168
+ stxHost.insert(parent, loadingElement, placeholder);
4062
4169
  }
4063
4170
  };
4064
4171
 
4065
4172
  // Helper to hide loading state
4066
4173
  const hideLoading = () => {
4067
4174
  if (loadingElement) {
4068
- loadingElement.remove();
4175
+ stxHost.remove(loadingElement);
4069
4176
  loadingElement = null;
4070
4177
  }
4071
4178
  };
@@ -4075,8 +4182,8 @@ catch (e) {
4075
4182
  hideLoading();
4076
4183
  hideEmpty();
4077
4184
  if (emptyTemplate) {
4078
- emptyElement = emptyTemplate.cloneNode(true);
4079
- parent.insertBefore(emptyElement, placeholder);
4185
+ emptyElement = stxHost.clone(emptyTemplate);
4186
+ stxHost.insert(parent, emptyElement, placeholder);
4080
4187
  const shownEmpty = emptyElement;
4081
4188
  shownEmpty.__stx_effect_disposers = trackEffects(function() { processElement(shownEmpty); });
4082
4189
  }
@@ -4087,7 +4194,7 @@ catch (e) {
4087
4194
  if (emptyElement) {
4088
4195
  // A string @empty renders a text node, which has no subtree to dispose.
4089
4196
  if (emptyElement.nodeType === 1) disposeSubtreeEffects(emptyElement);
4090
- emptyElement.remove();
4197
+ stxHost.remove(emptyElement);
4091
4198
  emptyElement = null;
4092
4199
  }
4093
4200
  };
@@ -4109,9 +4216,9 @@ catch (e) {
4109
4216
 
4110
4217
  const elements = [];
4111
4218
  if (isTemplate) {
4112
- elements.push(document.createComment('stx-for-item-start'));
4219
+ elements.push(stxHost.anchor('stx-for-item-start'));
4113
4220
  Array.from(templateContent.childNodes).forEach(node => {
4114
- const clone = node.cloneNode(true);
4221
+ const clone = stxHost.clone(node);
4115
4222
  if (clone.nodeType === 1) {
4116
4223
  // Structural directives need a live parent. Keep the iteration
4117
4224
  // scope on the clone until the keyed diff inserts it, then bind.
@@ -4119,13 +4226,13 @@ catch (e) {
4119
4226
  }
4120
4227
  elements.push(clone);
4121
4228
  });
4122
- elements.push(document.createComment('stx-for-item-end'));
4229
+ elements.push(stxHost.anchor('stx-for-item-end'));
4123
4230
  } else {
4124
- const clone = templateContent.cloneNode(true);
4231
+ const clone = stxHost.clone(templateContent);
4125
4232
  clone.__stx_for_scope = itemScope;
4126
4233
  elements.push(clone);
4127
4234
  componentSetupTemplates.forEach(function(setupTemplate) {
4128
- elements.push(setupTemplate.cloneNode(true));
4235
+ elements.push(stxHost.clone(setupTemplate));
4129
4236
  });
4130
4237
  }
4131
4238
  remapForComponentScopes(elements);
@@ -4251,7 +4358,7 @@ catch (e) {
4251
4358
  hideEmpty();
4252
4359
  if (emptyContent && typeof emptyContent === 'string') {
4253
4360
  emptyElement = document.createTextNode(emptyContent);
4254
- parent.insertBefore(emptyElement, placeholder);
4361
+ stxHost.insert(parent, emptyElement, placeholder);
4255
4362
  }
4256
4363
  } else if (emptyTemplate) {
4257
4364
  showEmpty();
@@ -4345,7 +4452,7 @@ catch (e) {
4345
4452
  // Moves without detaching. The cursor does not advance: this node
4346
4453
  // is now behind it, and the cursor still marks the next position
4347
4454
  // to fill.
4348
- parent.insertBefore(groupNode, cursor || placeholder);
4455
+ stxHost.insert(parent, groupNode, cursor || placeholder);
4349
4456
  }
4350
4457
  }
4351
4458
  newElements.push(...group);
@@ -4365,7 +4472,7 @@ catch (e) {
4365
4472
  // Before the cursor, not before the placeholder: a new row inserted
4366
4473
  // in the MIDDLE of the list must land at its own position, without
4367
4474
  // forcing every row after it to be moved to get back in order.
4368
- parent.insertBefore(el, cursor || placeholder);
4475
+ stxHost.insert(parent, el, cursor || placeholder);
4369
4476
  if (tgReady && el.isConnected) tgEnter(el, tgName);
4370
4477
  });
4371
4478
  hydrateComponentScopes(elements, elements.find(function(node) {
@@ -4409,13 +4516,13 @@ catch (e) {
4409
4516
  disposeSubtreeEffects(el);
4410
4517
  disposeSubtreeScopes(el);
4411
4518
  if (tgLeave(el, tgName)) transitioned.add(el);
4412
- else el.remove();
4519
+ else stxHost.remove(el);
4413
4520
  });
4414
4521
  group.forEach((el) => {
4415
4522
  if (transitioned.has(el) || liveNodes.includes(el)) return;
4416
4523
  disposeSubtreeEffects(el);
4417
4524
  disposeSubtreeScopes(el);
4418
- el.remove();
4525
+ stxHost.remove(el);
4419
4526
  });
4420
4527
  }
4421
4528
  else {
@@ -4505,8 +4612,8 @@ catch (e) {
4505
4612
  b.el.__stx_chain_member = true;
4506
4613
  b.el.__stx_chain_active = false;
4507
4614
  b.capturedElementScope = findElementScope(b.el);
4508
- b.placeholder = document.createComment('stx-if-chain');
4509
- parent.insertBefore(b.placeholder, b.el);
4615
+ b.placeholder = stxHost.anchor('stx-if-chain');
4616
+ stxHost.insert(parent, b.placeholder, b.el);
4510
4617
  b.el.removeAttribute(b.attr);
4511
4618
  // A <template> branch (the wrapper convertSignalDirectivesToAttributes
4512
4619
  // emits for a multi-child / text branch) is INERT \u2014 inserting the element
@@ -4535,10 +4642,10 @@ catch (e) {
4535
4642
  // and the attributes it consumed (x-model, :id) do not come back -- so a
4536
4643
  // second clone taken from the element would arrive already stripped, with
4537
4644
  // no binding and no way to acquire one.
4538
- b.pristine = b.isTemplate ? b.el.content.cloneNode(true) : null;
4645
+ b.pristine = b.isTemplate ? stxHost.clone(b.el.content) : null;
4539
4646
  b.nodes = b.isTemplate ? [] : [b.el];
4540
4647
  b.endMarker = null;
4541
- b.el.remove();
4648
+ stxHost.remove(b.el);
4542
4649
  b.childrenProcessed = false;
4543
4650
  });
4544
4651
 
@@ -4602,7 +4709,7 @@ catch (e2) {
4602
4709
  console.log('[stx] bindIfChain pick:', pickedIdx, 'of', chain.length, '(', pickedIdx >= 0 ? chain[pickedIdx].attr : 'none', ') prev:', currentIdx);
4603
4710
  if (__stxDevtoolsTracking) {
4604
4711
  var __ifScope = null;
4605
- try { __ifScope = (head && head.closest) ? head.closest('[data-stx-scope]') : null; } catch (e) { __ifScope = null; }
4712
+ try { __ifScope = stxHost.scopeOf(head); } catch (e) { __ifScope = null; }
4606
4713
  __stxDevtoolsRecordIf({
4607
4714
  scopeId: __ifScope ? __ifScope.getAttribute('data-stx-scope') : null,
4608
4715
  branches: chain.map(function(c) { return c.attr; }),
@@ -4630,21 +4737,20 @@ catch (e2) {
4630
4737
  // contains. The marker bounds the range, so a sibling that belongs to
4631
4738
  // a later branch or to ordinary markup is never touched.
4632
4739
  if (gone.endMarker && gone.endMarker.parentNode) {
4633
- var host = gone.endMarker.parentNode;
4634
4740
  var doomed = gone.placeholder.nextSibling;
4635
4741
  while (doomed && doomed !== gone.endMarker) {
4636
4742
  var nextDoomed = doomed.nextSibling;
4637
4743
  disposeSubtreeEffects(doomed);
4638
- host.removeChild(doomed);
4744
+ stxHost.remove(doomed);
4639
4745
  doomed = nextDoomed;
4640
4746
  }
4641
- gone.endMarker.remove();
4747
+ stxHost.remove(gone.endMarker);
4642
4748
  gone.endMarker = null;
4643
4749
  }
4644
4750
  gone.nodes.forEach(function (n) {
4645
4751
  if (!n.parentNode) return;
4646
4752
  disposeSubtreeEffects(n);
4647
- n.parentNode.removeChild(n);
4753
+ stxHost.remove(n);
4648
4754
  });
4649
4755
  // The next show clones afresh and processes again, so these effects
4650
4756
  // are finished with; leaving them bound only accumulates work (#1954).
@@ -4662,10 +4768,10 @@ catch (e) { /* a disposer of its own is not this branch's problem */ }
4662
4768
  // Detached nodes are out of reach of a DOM walk, so a row removed
4663
4769
  // while this branch is hidden could not dispose the effects it was
4664
4770
  // processed with (#1954). Same registry the single-element :if uses.
4665
- var host = n.parentNode;
4666
- if (!host.__stx_detached_if) host.__stx_detached_if = new Set();
4667
- host.__stx_detached_if.add(n);
4668
- host.removeChild(n);
4771
+ var parentEl = n.parentNode;
4772
+ if (!parentEl.__stx_detached_if) parentEl.__stx_detached_if = new Set();
4773
+ parentEl.__stx_detached_if.add(n);
4774
+ stxHost.remove(n);
4669
4775
  });
4670
4776
  }
4671
4777
  }
@@ -4679,16 +4785,16 @@ catch (e) { /* a disposer of its own is not this branch's problem */ }
4679
4785
  var anchor = pick.placeholder.nextSibling;
4680
4786
  var pickHost = pick.placeholder.parentNode;
4681
4787
  if (pick.isTemplate && pick.nodes.length === 0)
4682
- pick.nodes = Array.prototype.slice.call(pick.pristine.cloneNode(true).childNodes);
4788
+ pick.nodes = Array.prototype.slice.call(stxHost.clone(pick.pristine).childNodes);
4683
4789
  pick.nodes.forEach(function (n) {
4684
4790
  if (pickHost.__stx_detached_if) pickHost.__stx_detached_if.delete(n);
4685
- pickHost.insertBefore(n, anchor);
4791
+ stxHost.insert(pickHost, n, anchor);
4686
4792
  });
4687
4793
  // Bound AFTER the clones, so hiding can remove whatever a structural
4688
4794
  // directive inside the branch renders as their sibling.
4689
4795
  if (pick.isTemplate) {
4690
- if (!pick.endMarker) pick.endMarker = document.createComment('stx-if-chain-end');
4691
- pickHost.insertBefore(pick.endMarker, anchor);
4796
+ if (!pick.endMarker) pick.endMarker = stxHost.anchor('stx-if-chain-end');
4797
+ stxHost.insert(pickHost, pick.endMarker, anchor);
4692
4798
  }
4693
4799
  pick.el.__stx_shown_at = performance.now();
4694
4800
  if (!pick.childrenProcessed) {
@@ -4751,7 +4857,7 @@ catch (e) { /* a disposer of its own is not this branch's problem */ }
4751
4857
  return;
4752
4858
  }
4753
4859
 
4754
- const placeholder = document.createComment('stx-if');
4860
+ const placeholder = stxHost.anchor('stx-if');
4755
4861
  // Marks where this branch's rendered content ENDS (stacksjs/stx#1955).
4756
4862
  // Hiding used to remove only the cloned nodes, but a structural directive
4757
4863
  // directly inside the template renders its own output as siblings that are
@@ -4773,27 +4879,27 @@ catch (e) { /* a disposer of its own is not this branch's problem */ }
4773
4879
  const capturedComponentScope = { ...passedScope };
4774
4880
 
4775
4881
  preserveConditionalComponentScopes(el, capturedComponentScope);
4776
- parent.insertBefore(placeholder, el);
4882
+ stxHost.insert(parent, placeholder, el);
4777
4883
  el.removeAttribute(attrName);
4778
4884
 
4779
4885
  if (isTemplate) {
4780
4886
  // For templates, we need to handle the content fragment
4781
4887
  const content = el.content;
4782
- currentNodes = Array.from(content.childNodes).map(n => n.cloneNode(true));
4888
+ currentNodes = Array.from(content.childNodes).map(n => stxHost.clone(n));
4783
4889
  // Insert cloned content initially
4784
4890
  // Capture the anchor once. Re-reading placeholder.nextSibling after
4785
4891
  // every insertion points at the node just inserted and reverses a
4786
4892
  // multi-root template branch.
4787
4893
  const initialAnchor = placeholder.nextSibling;
4788
- currentNodes.forEach(node => parent.insertBefore(node, initialAnchor));
4894
+ currentNodes.forEach(node => stxHost.insert(parent, node, initialAnchor));
4789
4895
  // The end of this branch's range, bound at the same time as the content
4790
4896
  // it closes (#1955). Needed here too, not only on re-show: the first
4791
4897
  // hide of a branch that was never toggled removes THIS content, and a
4792
4898
  // nested :for renders rows that are siblings of these clones rather than
4793
4899
  // among them.
4794
- endMarker = document.createComment('stx-if-end');
4795
- parent.insertBefore(endMarker, initialAnchor);
4796
- el.remove(); // Remove the template element itself
4900
+ endMarker = stxHost.anchor('stx-if-end');
4901
+ stxHost.insert(parent, endMarker, initialAnchor);
4902
+ stxHost.remove(el); // Remove the template element itself
4797
4903
  }
4798
4904
 
4799
4905
  // Helper to evaluate with captured scope (auto-unwraps signals).
@@ -4883,13 +4989,13 @@ catch (e2) {
4883
4989
  if (isTemplate) {
4884
4990
  if (value && !isInserted) {
4885
4991
  // Re-insert cloned content
4886
- currentNodes = Array.from(el.content.childNodes).map(n => n.cloneNode(true));
4992
+ currentNodes = Array.from(el.content.childNodes).map(n => stxHost.clone(n));
4887
4993
  const insertionAnchor = placeholder.nextSibling;
4888
- currentNodes.forEach(node => parent.insertBefore(node, insertionAnchor));
4994
+ currentNodes.forEach(node => stxHost.insert(parent, node, insertionAnchor));
4889
4995
  // Bound AFTER the clones, so hiding can remove whatever a nested
4890
4996
  // structural directive rendered as their sibling (#1955).
4891
- if (!endMarker) endMarker = document.createComment('stx-if-end');
4892
- parent.insertBefore(endMarker, insertionAnchor);
4997
+ if (!endMarker) endMarker = stxHost.anchor('stx-if-end');
4998
+ stxHost.insert(parent, endMarker, insertionAnchor);
4893
4999
  processTemplateNodes(currentNodes);
4894
5000
  childrenProcessed = true;
4895
5001
  // Stamp insertion time for click propagation guard
@@ -4915,12 +5021,12 @@ else if (!value && isInserted) {
4915
5021
  while (doomed && doomed !== endMarker) {
4916
5022
  var nextDoomed = doomed.nextSibling;
4917
5023
  disposeSubtreeEffects(doomed);
4918
- doomed.remove();
5024
+ stxHost.remove(doomed);
4919
5025
  doomed = nextDoomed;
4920
5026
  }
4921
- endMarker.remove();
5027
+ stxHost.remove(endMarker);
4922
5028
  }
4923
- currentNodes.forEach(node => { disposeSubtreeEffects(node); node.remove(); });
5029
+ currentNodes.forEach(node => { disposeSubtreeEffects(node); stxHost.remove(node); });
4924
5030
  currentNodes = [];
4925
5031
  isInserted = false;
4926
5032
  childrenProcessed = false;
@@ -4941,7 +5047,7 @@ else {
4941
5047
  if (value && !isInserted) {
4942
5048
  console.log('[stx] bindIf INSERTING element for :if=' + expr);
4943
5049
  const insertionAnchor = placeholder.nextSibling;
4944
- currentNodes.forEach(node => parent.insertBefore(node, insertionAnchor));
5050
+ currentNodes.forEach(node => stxHost.insert(parent, node, insertionAnchor));
4945
5051
  if (parent && parent.__stx_detached_if) currentNodes.forEach(node => parent.__stx_detached_if.delete(node));
4946
5052
  el.__stx_shown_at = performance.now();
4947
5053
  isInserted = true;
@@ -4957,7 +5063,7 @@ else if (!value && isInserted) {
4957
5063
  // once (#1737). Permanent disposal stays in bindFor (item removal)
4958
5064
  // and cleanupContainer (SPA navigation). The double-bind guards make
4959
5065
  // re-show idempotent, so toggling doesn't leak.
4960
- currentNodes.forEach(node => node.remove());
5066
+ currentNodes.forEach(node => stxHost.remove(node));
4961
5067
  // Detached nodes are out of reach of a DOM walk, so an ancestor row or
4962
5068
  // branch removed while this is hidden could not dispose their effects
4963
5069
  // (#1954). Record them on the parent that keeps the placeholder.
@@ -7843,7 +7949,10 @@ else {
7843
7949
  // Bind page roots after child components have registered their local scope.
7844
7950
  document.querySelectorAll('[data-stx]').forEach(el => {
7845
7951
  console.log('[DOMContentLoaded] processElement on:', el.tagName, 'scope keys:', Object.keys(componentScope).slice(0, 10));
7846
- var disposeEffects = trackEffects(function() { processElement(el); });
7952
+ var disposeEffects = trackEffects(function() {
7953
+ // The server's list when the page shipped one, the walk otherwise.
7954
+ if (!processFromManifest(el, componentScope, window.__stx_bindings)) processElement(el);
7955
+ });
7847
7956
  el.__stx_disposers = disposeEffects;
7848
7957
 
7849
7958
  // Remove x-cloak after bindings are applied (prevents FOUC)