@stacksjs/stx 0.2.119 → 0.2.121

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 (62) hide show
  1. package/dist/builtins/attrs.d.ts +16 -0
  2. package/dist/builtins/attrs.js +16 -0
  3. package/dist/builtins/safe-image.js +2 -9
  4. package/dist/builtins/stx-image.js +2 -9
  5. package/dist/builtins/stx-link.js +2 -9
  6. package/dist/cli.js +1869 -863
  7. package/dist/client-script-bundler.d.ts +9 -7
  8. package/dist/client-script-bundler.js +43 -4
  9. package/dist/client-script.d.ts +24 -0
  10. package/dist/client-script.js +59 -5
  11. package/dist/component-library.js +30 -12
  12. package/dist/component-registry.d.ts +1 -0
  13. package/dist/component-renderer.js +13 -5
  14. package/dist/composables/index.d.ts +1 -0
  15. package/dist/composables/index.js +1 -0
  16. package/dist/composables/use-scroll-lock.d.ts +13 -0
  17. package/dist/composables/use-scroll-lock.js +55 -0
  18. package/dist/composition-api.d.ts +1 -1
  19. package/dist/composition-api.js +13 -4
  20. package/dist/craft-bridge.d.ts +80 -1
  21. package/dist/craft-bridge.js +80 -1
  22. package/dist/craft-components.d.ts +77 -3
  23. package/dist/craft-components.js +138 -9
  24. package/dist/craft.js +12708 -118
  25. package/dist/dev-server/crosswind.d.ts +15 -1
  26. package/dist/dev-server/crosswind.js +12 -3
  27. package/dist/dev-server/serve-app.js +3 -3
  28. package/dist/document-shell.d.ts +17 -0
  29. package/dist/document-shell.js +72 -2
  30. package/dist/expressions.js +14 -7
  31. package/dist/head.d.ts +8 -0
  32. package/dist/head.js +11 -8
  33. package/dist/html-masking.d.ts +12 -0
  34. package/dist/html-masking.js +50 -15
  35. package/dist/index.d.ts +9 -1
  36. package/dist/index.js +9 -1
  37. package/dist/native-build.d.ts +6 -0
  38. package/dist/native-build.js +77 -60
  39. package/dist/process.js +11 -3
  40. package/dist/pwa.js +1232 -508
  41. package/dist/render.js +6 -1
  42. package/dist/runtime-globals.d.ts +7 -4
  43. package/dist/runtime-globals.js +10 -2
  44. package/dist/runtime-injection.js +15 -1
  45. package/dist/signal-processing.js +19 -28
  46. package/dist/signals-api.d.ts +2 -0
  47. package/dist/signals-api.js +9 -0
  48. package/dist/signals.js +785 -62
  49. package/dist/ssg.js +1176 -452
  50. package/dist/state-management.d.ts +1 -1
  51. package/dist/state-management.js +1 -1
  52. package/dist/strict-lint.d.ts +1 -1
  53. package/dist/strict-lint.js +1 -1
  54. package/dist/stx.d.ts +6 -0
  55. package/dist/style-scoping.js +10 -0
  56. package/dist/utils.js +5 -3
  57. package/dist/variable-extractor.js +14 -22
  58. package/dist/vue-template.js +3 -2
  59. package/dist/x-element.d.ts +2 -2
  60. package/dist/x-element.js +9 -8
  61. package/package.json +5 -5
  62. package/stx.d.ts +6 -0
package/dist/signals.js CHANGED
@@ -270,6 +270,12 @@ else {
270
270
  const val = target[prop];
271
271
  // If it's a signal or derived, call it to get the value
272
272
  if (val && typeof val === 'function' && (val._isSignal || val._isDerived)) {
273
+ // Loop variables are implementation-detail signals so keyed rows can
274
+ // update reactively. In template expressions they must behave like
275
+ // their item value, including for objects with fields named value,
276
+ // set, or subscribe. Otherwise item.value is mistaken for the
277
+ // signal compatibility API and resolves to the whole item object.
278
+ if (val._isStxLoopItem) return val();
273
279
  if (preserveSignal && preserveSignal(prop, val)) return val;
274
280
  return val();
275
281
  }
@@ -1274,7 +1280,7 @@ finally {
1274
1280
  return null;
1275
1281
  }
1276
1282
 
1277
- function toValue(expr, el, enableAutoUnwrap = true) {
1283
+ function toValue(expr, el, enableAutoUnwrap = true, extraScope = {}) {
1278
1284
  try {
1279
1285
  // Skip placeholder expressions like __TITLE__ (build-time placeholders)
1280
1286
  if (/^__[A-Z_]+__$/.test(expr.trim())) {
@@ -1282,7 +1288,7 @@ finally {
1282
1288
  }
1283
1289
  // First try component-level scope
1284
1290
  const elementScope = findElementScope(el || currentElement);
1285
- const baseScope = { ...globalHelpers, ...componentScope, ...(elementScope || {}) };
1291
+ const baseScope = { ...globalHelpers, ...componentScope, ...(elementScope || {}), ...extraScope };
1286
1292
 
1287
1293
  // Check for pipe syntax (Feature #2)
1288
1294
  const pipeResult = parsePipeExpression(expr, baseScope);
@@ -1361,8 +1367,8 @@ catch (e) {
1361
1367
  const [, varName, op, valueExpr] = assignMatch;
1362
1368
  const signal = scope[varName];
1363
1369
  if (signal && signal._isSignal) {
1364
- return () => {
1365
- const addValue = toValue(valueExpr, null, true);
1370
+ return ($event) => {
1371
+ const addValue = toValue(valueExpr, null, true, { ...scope, $event: $event });
1366
1372
  signal.update(n => {
1367
1373
  switch (op) {
1368
1374
  case '+': return n + addValue;
@@ -1383,8 +1389,8 @@ catch (e) {
1383
1389
  const [, varName, valueExpr] = simpleAssignMatch;
1384
1390
  const signal = scope[varName];
1385
1391
  if (signal && signal._isSignal) {
1386
- return () => {
1387
- const newValue = toValue(valueExpr, null, true);
1392
+ return ($event) => {
1393
+ const newValue = toValue(valueExpr, null, true, { ...scope, $event: $event });
1388
1394
  signal.set(newValue);
1389
1395
  };
1390
1396
  }
@@ -1467,8 +1473,16 @@ catch (e) {
1467
1473
  return;
1468
1474
  }
1469
1475
 
1470
- const fn = new Function(...Object.keys(scope), '$event', expr);
1471
- fn(...Object.values(scope), event);
1476
+ // Loop items arrive as signals (see createItemElements). Unwrap them for
1477
+ // the same reason the inline @event path does: \`item.value\` in a handler
1478
+ // means the item's field, not the signal API.
1479
+ const handlerScope = {};
1480
+ Object.keys(scope).forEach(function(k) {
1481
+ const v = scope[k];
1482
+ handlerScope[k] = (v && typeof v === 'function' && v._isStxLoopItem) ? v() : v;
1483
+ });
1484
+ const fn = new Function(...Object.keys(handlerScope), '$event', expr);
1485
+ fn(...Object.values(handlerScope), event);
1472
1486
  }
1473
1487
  catch (e) {
1474
1488
  // Event handlers fire on user interaction, not reactive re-runs \u2014 there's no
@@ -1625,10 +1639,22 @@ catch (e) {
1625
1639
  && /^\\s*\\(?\\s*\\w+(?:\\s*,\\s*\\w+)?\\s*\\)?\\s+(?:in|of)\\s+.+\\s*$/.test(expr);
1626
1640
  }
1627
1641
 
1642
+ function getElementAttributes(el) {
1643
+ if (!el || !el.attributes) return [];
1644
+ return Array.from(el.attributes).map(function(attr) {
1645
+ // Browser DOM exposes Attr records through NamedNodeMap. HappyDOM and
1646
+ // other standards-oriented DOM implementations may expose Map entries
1647
+ // for cloned nodes instead. Normalize both shapes so reactive nodes
1648
+ // created by :for hydrate exactly like their browser counterparts.
1649
+ if (Array.isArray(attr)) return { name: attr[0], value: attr[1] };
1650
+ return attr;
1651
+ });
1652
+ }
1653
+
1628
1654
  function getModelBinding(el) {
1629
1655
  if (!el || !el.attributes) return null;
1630
1656
  var prefixes = ['x-model', ':model', '@model'];
1631
- var attrs = Array.from(el.attributes);
1657
+ var attrs = getElementAttributes(el);
1632
1658
  for (var i = 0; i < attrs.length; i++) {
1633
1659
  var name = attrs[i] && attrs[i].name;
1634
1660
  if (!name) continue;
@@ -1640,17 +1666,112 @@ catch (e) {
1640
1666
  return null;
1641
1667
  }
1642
1668
 
1669
+ function readDeferredParentComponentProps(el) {
1670
+ var serialized = el.getAttribute('data-stx-deferred-parent-bindings');
1671
+ if (!serialized) return {};
1672
+ try {
1673
+ var parsed = JSON.parse(serialized);
1674
+ return parsed && typeof parsed === 'object' ? parsed : {};
1675
+ }
1676
+ catch (e) {
1677
+ return {};
1678
+ }
1679
+ }
1680
+
1681
+ function preserveDeferredParentComponentProps(el) {
1682
+ var names = (el.getAttribute('data-stx-parent-bindings') || '').split(/\\s+/).filter(Boolean);
1683
+ if (!names.length) return;
1684
+ var deferred = readDeferredParentComponentProps(el);
1685
+ names.forEach(function(name) {
1686
+ var sourceName = ':' + name;
1687
+ if (el.hasAttribute(sourceName))
1688
+ deferred[name] = el.getAttribute(sourceName);
1689
+ });
1690
+ if (Object.keys(deferred).length)
1691
+ el.setAttribute('data-stx-deferred-parent-bindings', JSON.stringify(deferred));
1692
+ }
1693
+
1694
+ function hasPendingLoopBoundary(el) {
1695
+ var loopBoundary = el;
1696
+ while (loopBoundary && loopBoundary !== document) {
1697
+ var loopExpression = loopBoundary.getAttribute && (
1698
+ loopBoundary.getAttribute('@for')
1699
+ || loopBoundary.getAttribute(':for')
1700
+ || loopBoundary.getAttribute('x-for')
1701
+ );
1702
+ if (loopExpression && isForDirectiveExpression(loopExpression))
1703
+ return true;
1704
+ loopBoundary = loopBoundary.parentElement || loopBoundary.parentNode;
1705
+ }
1706
+ return false;
1707
+ }
1708
+
1709
+ function hasPendingAncestorLoopBoundary(el) {
1710
+ var loopBoundary = el && (el.parentElement || el.parentNode);
1711
+ while (loopBoundary && loopBoundary !== document) {
1712
+ var loopExpression = loopBoundary.getAttribute && (
1713
+ loopBoundary.getAttribute('@for')
1714
+ || loopBoundary.getAttribute(':for')
1715
+ || loopBoundary.getAttribute('x-for')
1716
+ );
1717
+ if (loopExpression && isForDirectiveExpression(loopExpression))
1718
+ return true;
1719
+ loopBoundary = loopBoundary.parentElement || loopBoundary.parentNode;
1720
+ }
1721
+ return false;
1722
+ }
1723
+
1724
+ function syncComponentPropSignal(el, name, value) {
1725
+ if (!el || !el.getAttribute || value === undefined) return;
1726
+ var scopeId = el.getAttribute('data-stx-scope');
1727
+ var scopeVars = scopeId && window.stx._scopes && window.stx._scopes[scopeId];
1728
+ if (!scopeVars) return;
1729
+ var camelName = name.replace(/-([a-z0-9])/g, function(_, character) {
1730
+ return character.toUpperCase();
1731
+ });
1732
+ var propSignal = scopeVars[name] || scopeVars[camelName];
1733
+ if (!propSignal || !propSignal._isSignal || typeof propSignal.set !== 'function') return;
1734
+ var directValues = el.__stx_direct_prop_values || (el.__stx_direct_prop_values = {});
1735
+ var directValue = {
1736
+ raw: el.hasAttribute(name) ? el.getAttribute(name) : null,
1737
+ value: value,
1738
+ };
1739
+ directValues[name] = directValue;
1740
+ directValues[camelName] = directValue;
1741
+ // This runs inside the parent binding effect. Reading the child prop
1742
+ // signal normally would subscribe that effect to its own output. Parent
1743
+ // expressions that return a fresh array or object would then recurse:
1744
+ // syncing the child retriggers the parent, which creates another value.
1745
+ // Compare without dependency tracking so the effect only follows the
1746
+ // caller-owned signals used by the binding expression.
1747
+ if (!Object.is(peek(function() { return propSignal(); }), value))
1748
+ propSignal.set(value);
1749
+ }
1750
+
1643
1751
  function bindParentComponentProps(el, callerScope) {
1644
1752
  if (!el || !el.getAttribute || el.__stx_parent_props_bound) return;
1645
1753
  var names = (el.getAttribute('data-stx-parent-bindings') || '').split(/\\s+/).filter(Boolean);
1646
1754
  if (!names.length) return;
1755
+ if (hasPendingLoopBoundary(el)) {
1756
+ preserveDeferredParentComponentProps(el);
1757
+ return;
1758
+ }
1759
+ var deferred = readDeferredParentComponentProps(el);
1760
+ var bindings = names.map(function(name) {
1761
+ var sourceName = ':' + name;
1762
+ var expression = el.hasAttribute(sourceName) ? el.getAttribute(sourceName) : deferred[name];
1763
+ return typeof expression === 'string'
1764
+ ? { name: name, sourceName: sourceName, expression: expression }
1765
+ : null;
1766
+ }).filter(Boolean);
1767
+ if (!bindings.length) return;
1647
1768
  el.__stx_parent_props_bound = true;
1648
1769
  var scope = { ...globalHelpers, ...callerScope };
1649
1770
 
1650
- names.forEach(function(name) {
1651
- var sourceName = ':' + name;
1652
- if (!el.hasAttribute(sourceName)) return;
1653
- var expression = el.getAttribute(sourceName);
1771
+ bindings.forEach(function(binding) {
1772
+ var name = binding.name;
1773
+ var sourceName = binding.sourceName;
1774
+ var expression = binding.expression;
1654
1775
  effect(function() {
1655
1776
  var value;
1656
1777
  try {
@@ -1687,9 +1808,21 @@ catch (e) {
1687
1808
  }
1688
1809
  el.setAttribute(name, serialized);
1689
1810
  }
1811
+ // The component setup scope is already registered for normal and
1812
+ // cloned signal components. Update its prop signal directly so object
1813
+ // identity and synchronous Vue-like prop delivery do not depend on a
1814
+ // MutationObserver round trip through a serialized DOM attribute.
1815
+ // The attribute remains the public DOM reflection and observer fallback
1816
+ // for scopes that register after this binding pass.
1817
+ syncComponentPropSignal(el, name, value);
1690
1818
  });
1691
1819
  el.removeAttribute(sourceName);
1820
+ delete deferred[name];
1692
1821
  });
1822
+ if (Object.keys(deferred).length)
1823
+ el.setAttribute('data-stx-deferred-parent-bindings', JSON.stringify(deferred));
1824
+ else
1825
+ el.removeAttribute('data-stx-deferred-parent-bindings');
1693
1826
  }
1694
1827
 
1695
1828
  function resolveComponentCallerScope(el, pageScope) {
@@ -1720,11 +1853,69 @@ catch (e) {
1720
1853
  root.querySelectorAll('[data-stx-scope]').forEach(function(scopeEl) { scopeEls.push(scopeEl); });
1721
1854
  scopeEls.forEach(function(scopeEl) {
1722
1855
  if (scopeEl.__stx_parent_props_bound) return;
1856
+ // A component inside a descendant loop cannot resolve its caller props
1857
+ // until bindFor creates the iteration scope. Binding it while the outer
1858
+ // conditional is being detached would consume expressions such as
1859
+ // :status="deployment.status" before the deployment item exists, leaving every
1860
+ // later clone with the child's default prop. bindFor hydrates these
1861
+ // component scopes after insertion with the correct per-item scope.
1862
+ var current = scopeEl;
1863
+ while (current && current !== root) {
1864
+ if (current.hasAttribute
1865
+ && (current.hasAttribute('@for') || current.hasAttribute(':for') || current.hasAttribute('x-for'))) {
1866
+ preserveDeferredParentComponentProps(scopeEl);
1867
+ return;
1868
+ }
1869
+ current = current.parentElement || current.parentNode;
1870
+ }
1723
1871
  scopeEl.__stx_parent_scope = resolveComponentCallerScope(scopeEl, callerScope);
1724
1872
  bindParentComponentProps(scopeEl, scopeEl.__stx_parent_scope);
1725
1873
  });
1726
1874
  }
1727
1875
 
1876
+ // Component setup scripts are emitted immediately after their scoped root.
1877
+ // When that root itself carries :if, detaching only the element leaves the
1878
+ // sibling setup script outside the conditional node group. Scripts inserted
1879
+ // through innerHTML are inert, so the later hydration pass has no script to
1880
+ // execute and the component keeps raw directives and interpolations.
1881
+ function findConditionalComponentSetupSiblings(root) {
1882
+ if (!root || !root.hasAttribute || !root.hasAttribute('data-stx-scope')) return [];
1883
+ var scopeId = root.getAttribute('data-stx-scope');
1884
+ var templateScopeId = root.getAttribute('data-stx-template-scope') || scopeId;
1885
+ if (!scopeId) return [];
1886
+
1887
+ var scripts = [];
1888
+ var sibling = root.nextSibling;
1889
+ while (sibling) {
1890
+ var next = sibling.nextSibling;
1891
+ if (sibling.nodeType === Node.TEXT_NODE && !(sibling.textContent || '').trim()) {
1892
+ sibling = next;
1893
+ continue;
1894
+ }
1895
+ if (sibling.nodeType === Node.COMMENT_NODE) {
1896
+ sibling = next;
1897
+ continue;
1898
+ }
1899
+ if (sibling.nodeType === Node.ELEMENT_NODE
1900
+ && sibling.tagName === 'STYLE'
1901
+ && sibling.hasAttribute('data-stx-vendor')) {
1902
+ sibling = next;
1903
+ continue;
1904
+ }
1905
+ if (sibling.nodeType === Node.ELEMENT_NODE
1906
+ && sibling.tagName === 'SCRIPT'
1907
+ && sibling.hasAttribute('data-stx-scoped')
1908
+ && ((sibling.textContent || '').indexOf(scopeId) !== -1
1909
+ || (templateScopeId && (sibling.textContent || '').indexOf(templateScopeId) !== -1))) {
1910
+ scripts.push(sibling);
1911
+ sibling = next;
1912
+ continue;
1913
+ }
1914
+ break;
1915
+ }
1916
+ return scripts;
1917
+ }
1918
+
1728
1919
  function processElement(el, scope = componentScope) {
1729
1920
  // Debug: log every element with x-class or @click
1730
1921
  if (el.nodeType === Node.ELEMENT_NODE && el.hasAttribute) {
@@ -1739,6 +1930,17 @@ catch (e) {
1739
1930
  deferHydration(el, trigger, scope);
1740
1931
  return;
1741
1932
  }
1933
+ if (el.nodeType === Node.ELEMENT_NODE
1934
+ && el.hasAttribute
1935
+ && el.hasAttribute('data-stx-scope')
1936
+ && hasPendingAncestorLoopBoundary(el)) {
1937
+ preserveDeferredParentComponentProps(el);
1938
+ // Even when the component is itself a loop root, an ancestor loop must
1939
+ // create its caller item scope first. The ancestor's bindFor pass will
1940
+ // clone and hydrate this component with that scope, then its own loop can
1941
+ // evaluate expressions such as group.resources.
1942
+ return;
1943
+ }
1742
1944
 
1743
1945
  if (el.nodeType === Node.ELEMENT_NODE && el.tagName === 'BUTTON' && el.hasAttribute && el.hasAttribute('@click')) {
1744
1946
 
@@ -1964,9 +2166,9 @@ catch (e2) {
1964
2166
 
1965
2167
  // Handle attributes
1966
2168
  if (el.hasAttribute && (el.hasAttribute('x-class') || el.hasAttribute('@click'))) {
1967
- console.log('[stx] attr loop entry:', el.tagName, 'attrs:', Array.from(el.attributes).map(a => a.name).join(', '));
2169
+ console.log('[stx] attr loop entry:', el.tagName, 'attrs:', getElementAttributes(el).map(a => a.name).join(', '));
1968
2170
  }
1969
- Array.from(el.attributes).forEach(attr => {
2171
+ getElementAttributes(el).forEach(attr => {
1970
2172
  const name = attr.name;
1971
2173
  const value = attr.value;
1972
2174
  if (!name) return;
@@ -2008,6 +2210,7 @@ catch (e2) {
2008
2210
  el.value = desiredValue;
2009
2211
  if (desiredValue === '' && el.tagName !== 'OPTION') el.removeAttribute(attrName);
2010
2212
  else if (el.getAttribute(attrName) !== desiredValue) el.setAttribute(attrName, desiredValue);
2213
+ if (typeof el.__stx_model_sync === 'function') el.__stx_model_sync();
2011
2214
  };
2012
2215
  applyControlValue();
2013
2216
 
@@ -2032,6 +2235,24 @@ catch (e2) {
2032
2235
  }
2033
2236
  return;
2034
2237
  }
2238
+ // Checked, selected, and indeterminate are live DOM properties, just like value.
2239
+ // Updating only the HTML attribute leaves a user-touched checkbox or
2240
+ // radio in its previous state because the property becomes dirty.
2241
+ // Indeterminate has no corresponding effective HTML attribute at all.
2242
+ if ((attrName === 'checked' || attrName === 'selected' || attrName === 'indeterminate') && attrName in el) {
2243
+ var selectedState = Boolean(v);
2244
+ el[attrName] = selectedState;
2245
+ if (attrName === 'indeterminate') {
2246
+ el.removeAttribute(attrName);
2247
+ }
2248
+ else if (selectedState) {
2249
+ el.setAttribute(attrName, '');
2250
+ }
2251
+ else {
2252
+ el.removeAttribute(attrName);
2253
+ }
2254
+ return;
2255
+ }
2035
2256
  if (v === false || v === null || v === undefined) {
2036
2257
  el.removeAttribute(attrName);
2037
2258
  }
@@ -2111,11 +2332,15 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
2111
2332
  console.log('[stx] binding event:', eventName, 'on', el.tagName, 'expr:', value.substring(0, 40));
2112
2333
  el.addEventListener(eventName, (event) => {
2113
2334
  console.log('[stx] event fired:', eventName, 'on', el.tagName);
2114
- // A component listener belongs to the component root. Ignore native
2115
- // events bubbling out of its internal markup, otherwise a form
2116
- // component with @submit fires the parent once for the native form
2117
- // submit and again for defineEmits('submit').
2118
- if (isForwardedComponentEvent && event.target !== el) return;
2335
+ // Vue-style component listeners fall through to the rendered native
2336
+ // root unless the child emits the same event. Signal components use
2337
+ // a scope wrapper, so native events arrive here from a descendant.
2338
+ // Ignore that native event only after defineEmits() has declared or
2339
+ // emitted the same name, preventing submit/click handlers from
2340
+ // firing once for the native event and again for the CustomEvent.
2341
+ var componentEmits = el.__stx_component_emits;
2342
+ if (isForwardedComponentEvent && event.target !== el
2343
+ && componentEmits && componentEmits[eventName]) return;
2119
2344
 
2120
2345
  // Vue-style component handlers receive the emitted payload directly.
2121
2346
  // Native root events still receive the Event object so server-only
@@ -2180,7 +2405,17 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
2180
2405
  // primitive reads.
2181
2406
  var unwrapVars = Object.keys(eventCapturedScope).map(function(k) {
2182
2407
  var v = eventCapturedScope[k];
2183
- if (v && typeof v === 'function' && v._isSignal && expressionUsesSignalApi(value, k)) {
2408
+ // A :for item is a signal only as an implementation detail, so
2409
+ // that keyed rows can update in place. Its \`.value\` is a field
2410
+ // on the item, never the signal compatibility API \u2014 the same
2411
+ // rule createAutoUnwrapProxy states for binding expressions.
2412
+ // Without the carve-out, expressionUsesSignalApi sees
2413
+ // \`option.value\` and hands the handler the raw signal, so a
2414
+ // handler like \`pick(option.value)\` receives the whole item.
2415
+ // It is not an error, so it lands silently: the wrong value is
2416
+ // stored and only surfaces much later.
2417
+ if (v && typeof v === 'function' && v._isSignal && !v._isStxLoopItem
2418
+ && (expressionCallsSignal(value, k) || expressionUsesSignalApi(value, k))) {
2184
2419
  return 'var ' + k + ' = __s["' + k + '"]';
2185
2420
  }
2186
2421
  return 'var ' + k + ' = __s["' + k + '"] && typeof __s["' + k + '"] === "function" && __s["' + k + '"]._isSignal ? __s["' + k + '"]() : __s["' + k + '"]';
@@ -2387,12 +2622,54 @@ catch (e) {
2387
2622
  }
2388
2623
  };
2389
2624
 
2390
- if (tag === 'input' && (type === 'checkbox' || type === 'radio')) {
2391
- effect(() => { el.checked = getValue(); });
2392
- el.addEventListener('change', () => setValue(el.checked));
2625
+ if (tag === 'input' && type === 'checkbox') {
2626
+ var syncCheckbox = () => {
2627
+ var current = getValue();
2628
+ el.checked = Array.isArray(current)
2629
+ ? current.some(function(item) { return String(item) === String(el.value); })
2630
+ : Boolean(current);
2631
+ };
2632
+ el.__stx_model_sync = syncCheckbox;
2633
+ effect(syncCheckbox);
2634
+ el.addEventListener('change', () => {
2635
+ var current = getValue();
2636
+ if (!Array.isArray(current)) {
2637
+ setValue(el.checked);
2638
+ return;
2639
+ }
2640
+
2641
+ var checkboxValue = coerceValue(el.value);
2642
+ var next = current.filter(function(item) { return String(item) !== String(checkboxValue); });
2643
+ if (el.checked) next.push(checkboxValue);
2644
+ setValue(next);
2645
+ });
2646
+ }
2647
+ else if (tag === 'input' && type === 'radio') {
2648
+ var syncRadio = () => { el.checked = String(getValue()) === String(el.value); };
2649
+ el.__stx_model_sync = syncRadio;
2650
+ effect(syncRadio);
2651
+ el.addEventListener('change', () => {
2652
+ if (el.checked) setValue(coerceValue(el.value));
2653
+ });
2393
2654
  }
2394
2655
  else if (tag === 'select') {
2395
- effect(() => { el.value = getValue(); });
2656
+ var syncSelect = () => { el.value = getValue() ?? ''; };
2657
+ el.__stx_model_sync = syncSelect;
2658
+ effect(syncSelect);
2659
+ if (!el.__stx_model_observer && typeof window.MutationObserver !== 'undefined') {
2660
+ el.__stx_model_observer = new window.MutationObserver(syncSelect);
2661
+ el.__stx_model_observer.observe(el, {
2662
+ childList: true,
2663
+ subtree: true,
2664
+ attributes: true,
2665
+ attributeFilter: ['value'],
2666
+ });
2667
+ el.__stx_destroy = el.__stx_destroy || [];
2668
+ el.__stx_destroy.push(function() {
2669
+ if (el.__stx_model_observer) el.__stx_model_observer.disconnect();
2670
+ el.__stx_model_observer = null;
2671
+ });
2672
+ }
2396
2673
  el.addEventListener('change', () => setValue(coerceValue(el.value)));
2397
2674
  }
2398
2675
  else {
@@ -2599,6 +2876,179 @@ else if (typeof value === 'string') {
2599
2876
  });
2600
2877
  }
2601
2878
 
2879
+ var __forComponentScopeCounter = 0;
2880
+ var __forComponentSetupFactories = new Map();
2881
+
2882
+ function forEachGroupElement(nodes, callback) {
2883
+ nodes.forEach(function(node) {
2884
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
2885
+ callback(node);
2886
+ if (node.querySelectorAll) {
2887
+ node.querySelectorAll('*').forEach(function(child) { callback(child); });
2888
+ }
2889
+ });
2890
+ }
2891
+
2892
+ // A signal component expanded inside <template :for> carries a compiled
2893
+ // data-stx-scope id and setup script. cloneNode() used to duplicate that id
2894
+ // for every row, so all instances shared the first child's props and state.
2895
+ // Give each cloned component tree its own ids before insertion, and rewrite
2896
+ // the setup scripts that target those ids.
2897
+ function remapForComponentScopes(nodes) {
2898
+ var scopeMap = {};
2899
+ forEachGroupElement(nodes, function(node) {
2900
+ if (!node.hasAttribute || !node.hasAttribute('data-stx-scope')) return;
2901
+ var oldId = node.getAttribute('data-stx-scope');
2902
+ if (!oldId) return;
2903
+ if (!scopeMap[oldId])
2904
+ scopeMap[oldId] = oldId + '_for_' + (++__forComponentScopeCounter);
2905
+ // Keep the compiler's original id through any additional structural
2906
+ // clones. DOM cloneNode() preserves attributes but not expando fields.
2907
+ if (!node.hasAttribute('data-stx-template-scope'))
2908
+ node.setAttribute('data-stx-template-scope', oldId);
2909
+ node.setAttribute('data-stx-scope', scopeMap[oldId]);
2910
+ });
2911
+
2912
+ var oldIds = Object.keys(scopeMap);
2913
+ if (!oldIds.length) return;
2914
+
2915
+ forEachGroupElement(nodes, function(node) {
2916
+ if (node.attributes) {
2917
+ Array.from(node.attributes).forEach(function(rawAttribute) {
2918
+ var attribute = Array.isArray(rawAttribute)
2919
+ ? { name: rawAttribute[0], value: rawAttribute[1] }
2920
+ : rawAttribute;
2921
+ if (!attribute || attribute.name === 'data-stx-scope' || attribute.name === 'data-stx-template-scope') return;
2922
+ var value = attribute.value;
2923
+ if (typeof value !== 'string') return;
2924
+ oldIds.forEach(function(oldId) {
2925
+ if (value.indexOf(oldId) !== -1)
2926
+ value = value.split(oldId).join(scopeMap[oldId]);
2927
+ });
2928
+ if (value !== attribute.value) node.setAttribute(attribute.name, value);
2929
+ });
2930
+ }
2931
+ });
2932
+ }
2933
+
2934
+ function hydrateComponentScopes(nodes, callerScope) {
2935
+ var roots = [];
2936
+ var scripts = [];
2937
+ forEachGroupElement(nodes, function(node) {
2938
+ if (node.hasAttribute && node.hasAttribute('data-stx-scope')) roots.push(node);
2939
+ if (node.tagName === 'SCRIPT' && node.hasAttribute('data-stx-scoped')) scripts.push(node);
2940
+ });
2941
+ if (!roots.length) return;
2942
+
2943
+ function hasPendingStructuralAncestor(root) {
2944
+ var cursor = root && root.parentElement;
2945
+ while (cursor && cursor.nodeType === Node.ELEMENT_NODE) {
2946
+ if (cursor.hasAttribute('@for')
2947
+ || cursor.hasAttribute(':for')
2948
+ || cursor.hasAttribute('x-for')
2949
+ || cursor.hasAttribute('@if')
2950
+ || cursor.hasAttribute(':if')
2951
+ || cursor.hasAttribute('x-if')
2952
+ || cursor.hasAttribute('@else-if')
2953
+ || cursor.hasAttribute(':else-if')
2954
+ || cursor.hasAttribute('x-else-if')
2955
+ || cursor.hasAttribute('@else')
2956
+ || cursor.hasAttribute(':else')
2957
+ || cursor.hasAttribute('x-else'))
2958
+ return true;
2959
+ cursor = cursor.parentElement;
2960
+ }
2961
+ return false;
2962
+ }
2963
+
2964
+ // Scripts cloned from template.content are inert in browsers. Compile each
2965
+ // distinct component setup body once, then run that cached function for
2966
+ // every loop instance. Temporarily restoring the template's original scope
2967
+ // id lets existing compiled setup code locate the correct root without
2968
+ // rewriting and reparsing the full script for every row.
2969
+ scripts.forEach(function(old) {
2970
+ var text = old.textContent || '';
2971
+ var pendingRoots = roots.filter(function(root) {
2972
+ var scopeId = root.getAttribute('data-stx-scope');
2973
+ var originalScopeId = root.getAttribute('data-stx-template-scope') || scopeId;
2974
+ return scopeId
2975
+ && originalScopeId
2976
+ && text.indexOf(originalScopeId) !== -1
2977
+ && !hasPendingStructuralAncestor(root)
2978
+ && !(window.stx._scopes && window.stx._scopes[scopeId]);
2979
+ });
2980
+ if (!pendingRoots.length) return;
2981
+
2982
+ var setup = __forComponentSetupFactories.get(text);
2983
+ if (!setup) {
2984
+ try {
2985
+ setup = new Function(text);
2986
+ __forComponentSetupFactories.set(text, setup);
2987
+ }
2988
+ catch (e) {
2989
+ console.error('[stx] cloned component setup compile error:', e);
2990
+ return;
2991
+ }
2992
+ }
2993
+
2994
+ pendingRoots.forEach(function(root) {
2995
+ var scopeId = root.getAttribute('data-stx-scope');
2996
+ var originalScopeId = root.getAttribute('data-stx-template-scope') || scopeId;
2997
+ if (!scopeId || !originalScopeId) return;
2998
+
2999
+ root.setAttribute('data-stx-scope', originalScopeId);
3000
+ try {
3001
+ setup();
3002
+ var registeredScope = window.stx._scopes && (window.stx._scopes[originalScopeId] || window.stx._scopes[scopeId]);
3003
+ if (registeredScope && originalScopeId !== scopeId) {
3004
+ delete window.stx._scopes[originalScopeId];
3005
+ window.stx._scopes[scopeId] = registeredScope;
3006
+ }
3007
+ }
3008
+ catch (e) {
3009
+ console.error('[stx] cloned component setup error:', e);
3010
+ }
3011
+ finally {
3012
+ root.setAttribute('data-stx-scope', scopeId);
3013
+ }
3014
+ });
3015
+
3016
+ // A component that is itself a structural source still needs its setup
3017
+ // sibling when bindFor/bindIf turns that source into a clone template.
3018
+ // Final row instances have no structural attribute and can discard it.
3019
+ var isStructuralSource = pendingRoots.some(function(root) {
3020
+ return root.hasAttribute('@for')
3021
+ || root.hasAttribute(':for')
3022
+ || root.hasAttribute('x-for')
3023
+ || root.hasAttribute('@if')
3024
+ || root.hasAttribute(':if')
3025
+ || root.hasAttribute('x-if');
3026
+ });
3027
+ if (!isStructuralSource && old.parentNode) old.parentNode.removeChild(old);
3028
+ });
3029
+
3030
+ roots.forEach(function(root) {
3031
+ var scopeId = root.getAttribute('data-stx-scope');
3032
+ var scopeVars = scopeId && window.stx._scopes && window.stx._scopes[scopeId];
3033
+ var resolvedCallerScope = resolveComponentCallerScope(root, callerScope);
3034
+ root.__stx_parent_scope = resolvedCallerScope;
3035
+ bindParentComponentProps(root, resolvedCallerScope);
3036
+ if (!scopeVars || root.__stx_disposers) return;
3037
+ scopeVars.__el = root;
3038
+ var childScope = { ...resolvedCallerScope, ...scopeVars };
3039
+ root.__stx_disposers = trackEffects(function() {
3040
+ processElement(root, childScope);
3041
+ });
3042
+ if (scopeVars.__mountCallbacks && !scopeVars.__mounted) {
3043
+ scopeVars.__mounted = true;
3044
+ scopeVars.__mountCallbacks.forEach(function(fn) {
3045
+ try { fn(); }
3046
+ catch (e) { console.error('[stx] onMount error:', e); }
3047
+ });
3048
+ }
3049
+ });
3050
+ }
3051
+
2602
3052
  function bindFor(el, passedScope = componentScope, attrName = '@for') {
2603
3053
  if (el.__stx_for_bound) return;
2604
3054
  el.__stx_for_bound = true;
@@ -2622,6 +3072,34 @@ else if (typeof value === 'string') {
2622
3072
 
2623
3073
  const placeholder = document.createComment('stx-for');
2624
3074
  const isTemplate = el.tagName === 'TEMPLATE';
3075
+ const componentSetupTemplates = [];
3076
+ if (!isTemplate && el.hasAttribute('data-stx-scope')) {
3077
+ const componentScopeId = el.getAttribute('data-stx-scope');
3078
+ const componentTemplateScopeId = el.getAttribute('data-stx-template-scope') || componentScopeId;
3079
+ let setupSibling = el.nextSibling;
3080
+ while (setupSibling) {
3081
+ const nextSetupSibling = setupSibling.nextSibling;
3082
+ if (setupSibling.nodeType === Node.TEXT_NODE && !(setupSibling.textContent || '').trim()) {
3083
+ setupSibling = nextSetupSibling;
3084
+ continue;
3085
+ }
3086
+ if (setupSibling.nodeType === Node.COMMENT_NODE) {
3087
+ setupSibling = nextSetupSibling;
3088
+ continue;
3089
+ }
3090
+ if (setupSibling.nodeType === Node.ELEMENT_NODE
3091
+ && setupSibling.tagName === 'SCRIPT'
3092
+ && setupSibling.hasAttribute('data-stx-scoped')
3093
+ && ((setupSibling.textContent || '').indexOf(componentScopeId) !== -1
3094
+ || (componentTemplateScopeId && (setupSibling.textContent || '').indexOf(componentTemplateScopeId) !== -1))) {
3095
+ componentSetupTemplates.push(setupSibling.cloneNode(true));
3096
+ setupSibling.remove();
3097
+ setupSibling = nextSetupSibling;
3098
+ continue;
3099
+ }
3100
+ break;
3101
+ }
3102
+ }
2625
3103
 
2626
3104
  // Check if element also has @if / :if - need to handle together
2627
3105
  const ifExpr = el.getAttribute('@if') || el.getAttribute(':if');
@@ -2658,6 +3136,11 @@ else if (typeof value === 'string') {
2658
3136
  const capturedScope = findElementScope(el) || findElementScope(parent);
2659
3137
 
2660
3138
  parent.insertBefore(placeholder, el);
3139
+ // Component setup scripts execute while the server-rendered loop source is
3140
+ // still in the document. That source becomes a template, not a live
3141
+ // instance, so reclaim all registered scopes before removing it. Every
3142
+ // rendered iteration receives remapped setup scripts and fresh scopes.
3143
+ disposeSubtreeScopes(el);
2661
3144
  parent.removeChild(el);
2662
3145
 
2663
3146
  // Capture :key expression BEFORE removing the attribute
@@ -2694,6 +3177,49 @@ else {
2694
3177
  let loadingElement = null;
2695
3178
  let emptyElement = null;
2696
3179
 
3180
+ // A <template :for> can contain another structural directive. That
3181
+ // nested directive replaces its source node with a placeholder and owns
3182
+ // additional live siblings around that placeholder. Track an explicit
3183
+ // boundary for every template iteration so keyed reuse moves the complete
3184
+ // live range, including nodes created by nested :for / :if bindings.
3185
+ // Moving only the original cloned nodes pulled static siblings past the
3186
+ // nested range on the next outer update (header, then rows became rows,
3187
+ // then header).
3188
+ const liveGroupNodes = (group) => {
3189
+ if (!isTemplate || !group || group.length < 2) return group || [];
3190
+ const start = group[0];
3191
+ const end = group[group.length - 1];
3192
+ if (!start || !end || start.parentNode !== parent || end.parentNode !== parent)
3193
+ return group.filter(groupNode => groupNode && groupNode.parentNode === parent);
3194
+
3195
+ const nodes = [];
3196
+ let currentNode = start;
3197
+ while (currentNode) {
3198
+ nodes.push(currentNode);
3199
+ if (currentNode === end) break;
3200
+ currentNode = currentNode.nextSibling;
3201
+ }
3202
+ return nodes;
3203
+ };
3204
+
3205
+ const removeGroup = (group) => {
3206
+ const liveNodes = liveGroupNodes(group);
3207
+ const liveSet = new Set(liveNodes);
3208
+ liveNodes.forEach(e => { disposeSubtreeScopes(e); e.remove(); });
3209
+ group.forEach(groupNode => {
3210
+ if (liveSet.has(groupNode)) return;
3211
+ disposeSubtreeScopes(groupNode);
3212
+ groupNode.remove();
3213
+ });
3214
+ };
3215
+
3216
+ const clearRenderedItems = () => {
3217
+ currentGroups.forEach(removeGroup);
3218
+ currentElements = [];
3219
+ currentGroups = [];
3220
+ currentKeys = [];
3221
+ };
3222
+
2697
3223
  // Helper to evaluate with captured scope (auto-unwraps signals)
2698
3224
  // NOTE: This eagerly reads ALL signals in scope \u2014 use evalLazy inside effects
2699
3225
  // to avoid over-broad dependency tracking.
@@ -2787,6 +3313,7 @@ catch (e) {
2787
3313
  // The item is wrapped in a signal so bindings track it reactively.
2788
3314
  const createItemElements = (item, index, key) => {
2789
3315
  const itemSignal = state(item);
3316
+ itemSignal._isStxLoopItem = true;
2790
3317
  const indexSignal = state(index);
2791
3318
  if (key) itemSignalMap.set(key, { item: itemSignal, index: indexSignal });
2792
3319
 
@@ -2796,6 +3323,7 @@ catch (e) {
2796
3323
 
2797
3324
  const elements = [];
2798
3325
  if (isTemplate) {
3326
+ elements.push(document.createComment('stx-for-item-start'));
2799
3327
  Array.from(templateContent.childNodes).forEach(node => {
2800
3328
  const clone = node.cloneNode(true);
2801
3329
  if (clone.nodeType === 1) {
@@ -2805,11 +3333,16 @@ catch (e) {
2805
3333
  }
2806
3334
  elements.push(clone);
2807
3335
  });
3336
+ elements.push(document.createComment('stx-for-item-end'));
2808
3337
  } else {
2809
3338
  const clone = templateContent.cloneNode(true);
2810
3339
  clone.__stx_for_scope = itemScope;
2811
3340
  elements.push(clone);
3341
+ componentSetupTemplates.forEach(function(setupTemplate) {
3342
+ elements.push(setupTemplate.cloneNode(true));
3343
+ });
2812
3344
  }
3345
+ remapForComponentScopes(elements);
2813
3346
  return elements;
2814
3347
  };
2815
3348
 
@@ -2835,10 +3368,7 @@ catch (e) {
2835
3368
  const isLoading = evalLazy(loadingExpr);
2836
3369
  if (isLoading) {
2837
3370
  // Dispose scopes registered by items before they leave the DOM (#1727).
2838
- currentElements.forEach(e => { disposeSubtreeScopes(e); e.remove(); });
2839
- currentElements = [];
2840
- currentGroups = [];
2841
- currentKeys = [];
3371
+ clearRenderedItems();
2842
3372
  showLoading();
2843
3373
  return;
2844
3374
  }
@@ -2884,10 +3414,7 @@ catch (e) {
2884
3414
  const ifValue = evalLazy(ifExpr);
2885
3415
  if (!ifValue) {
2886
3416
  // Dispose scopes registered by items before they leave the DOM (#1727).
2887
- currentElements.forEach(e => { disposeSubtreeScopes(e); e.remove(); });
2888
- currentElements = [];
2889
- currentGroups = [];
2890
- currentKeys = [];
3417
+ clearRenderedItems();
2891
3418
  hideEmpty();
2892
3419
  return;
2893
3420
  }
@@ -2925,10 +3452,7 @@ catch (e) {
2925
3452
  // Check empty state (Feature #3)
2926
3453
  if (list.length === 0) {
2927
3454
  // Dispose scopes registered by items before they leave the DOM (#1727).
2928
- currentElements.forEach(e => { disposeSubtreeScopes(e); e.remove(); });
2929
- currentElements = [];
2930
- currentGroups = [];
2931
- currentKeys = [];
3455
+ clearRenderedItems();
2932
3456
  if (emptyExpr) {
2933
3457
  const emptyContent = evalLazy(emptyExpr);
2934
3458
  if (emptyContent && typeof emptyContent === 'string') {
@@ -2981,9 +3505,10 @@ catch (e) {
2981
3505
  // group's nodes. Its own placeholder owns reinsertion, so keyed reuse
2982
3506
  // must not resurrect that hidden node.
2983
3507
  const group = existing.shift();
2984
- group.forEach(el => {
2985
- if (el.parentNode === parent) parent.insertBefore(el, placeholder);
2986
- });
3508
+ const liveNodes = liveGroupNodes(group);
3509
+ const fragment = document.createDocumentFragment();
3510
+ liveNodes.forEach(groupNode => fragment.appendChild(groupNode));
3511
+ parent.insertBefore(fragment, placeholder);
2987
3512
  newElements.push(...group);
2988
3513
  newGroups.push(group);
2989
3514
  usedKeys.add(key);
@@ -3000,14 +3525,20 @@ catch (e) {
3000
3525
  const elements = createItemElements(list[i], i, key);
3001
3526
  elements.forEach(el => {
3002
3527
  parent.insertBefore(el, placeholder);
3528
+ if (tgReady && el.isConnected) tgEnter(el, tgName);
3529
+ });
3530
+ hydrateComponentScopes(elements, elements.find(function(node) {
3531
+ return node && node.__stx_for_scope;
3532
+ })?.__stx_for_scope || passedScope);
3533
+ elements.forEach(el => {
3003
3534
  if (el.nodeType === 1) {
3004
3535
  var itemScope = el.__stx_for_scope || passedScope;
3005
3536
  delete el.__stx_for_scope;
3006
- processElement(el, itemScope);
3537
+ if (!el.__stx_disposers && el.tagName !== 'SCRIPT')
3538
+ processElement(el, itemScope);
3007
3539
  el.removeAttribute('x-cloak');
3008
3540
  el.querySelectorAll('[x-cloak]').forEach(c => c.removeAttribute('x-cloak'));
3009
3541
  }
3010
- if (tgReady && el.isConnected) tgEnter(el, tgName);
3011
3542
  });
3012
3543
  newElements.push(...elements);
3013
3544
  newGroups.push(elements);
@@ -3019,11 +3550,28 @@ catch (e) {
3019
3550
  // remove() call (#1727).
3020
3551
  for (const [key, groups] of oldKeyMap) {
3021
3552
  if (!usedKeys.has(key)) {
3022
- const elements = groups.flat();
3023
3553
  // In a transition group, run the leave animation and let it remove the
3024
3554
  // node; otherwise remove immediately. Scopes are disposed up-front
3025
3555
  // either way (their destroy hooks shouldn't wait on a CSS transition).
3026
- elements.forEach(el => { disposeSubtreeScopes(el); if (!(tgReady && tgLeave(el, tgName))) el.remove(); });
3556
+ groups.forEach((group) => {
3557
+ if (tgReady) {
3558
+ const liveNodes = liveGroupNodes(group);
3559
+ const transitioned = new Set();
3560
+ liveNodes.forEach((el) => {
3561
+ disposeSubtreeScopes(el);
3562
+ if (tgLeave(el, tgName)) transitioned.add(el);
3563
+ else el.remove();
3564
+ });
3565
+ group.forEach((el) => {
3566
+ if (transitioned.has(el) || liveNodes.includes(el)) return;
3567
+ disposeSubtreeScopes(el);
3568
+ el.remove();
3569
+ });
3570
+ }
3571
+ else {
3572
+ removeGroup(group);
3573
+ }
3574
+ });
3027
3575
  itemSignalMap.delete(key);
3028
3576
  }
3029
3577
  }
@@ -3222,9 +3770,26 @@ catch (e2) {
3222
3770
  (function (branch) {
3223
3771
  setTimeout(function () {
3224
3772
  var childScope = { ...globalHelpers, ...capturedComponentScope, ...(branch.capturedElementScope || {}) };
3773
+ // Components compiled inside any branch of an if/else-if/else
3774
+ // chain carry scoped setup scripts with them. Branch nodes are
3775
+ // detached before those scripts can execute, so simply
3776
+ // re-inserting the selected branch leaves component props,
3777
+ // events, directives, and projected slot expressions inert.
3778
+ // Hydrate those scopes before the normal subtree walk, matching
3779
+ // the single-template :if and :for insertion paths.
3780
+ var previouslyTrackedNodes = new Set();
3781
+ branch.nodes.forEach(function (n) {
3782
+ if (n && n.__stx_disposers) previouslyTrackedNodes.add(n);
3783
+ });
3784
+ hydrateComponentScopes(branch.nodes, childScope);
3225
3785
  branch.nodes.forEach(function (n) {
3226
3786
  if (n.nodeType !== 1) return; // whitespace/text between template children
3227
- processElement(n, childScope);
3787
+ // A scoped component receives its disposer in the outer scope
3788
+ // walk immediately after processElement returns from binding
3789
+ // this conditional. That first pass intentionally stops at
3790
+ // :if, so a disposer does not mean its children were hydrated.
3791
+ if (previouslyTrackedNodes.has(n) || !n.__stx_disposers)
3792
+ processElement(n, childScope);
3228
3793
  n.removeAttribute('x-cloak');
3229
3794
  n.querySelectorAll('[x-cloak]').forEach(function (c) { c.removeAttribute('x-cloak'); });
3230
3795
  });
@@ -3253,10 +3818,11 @@ catch (e2) {
3253
3818
 
3254
3819
  const placeholder = document.createComment('stx-if');
3255
3820
  let isInserted = true;
3256
- let currentNodes = [];
3257
3821
 
3258
3822
  // Handle <template> elements specially - clone their content
3259
3823
  const isTemplate = el.tagName === 'TEMPLATE';
3824
+ const componentSetupSiblings = isTemplate ? [] : findConditionalComponentSetupSiblings(el);
3825
+ let currentNodes = isTemplate ? [] : [el, ...componentSetupSiblings];
3260
3826
 
3261
3827
  // Capture BOTH element scope AND passedScope NOW before anything changes
3262
3828
  // passedScope may contain @for iteration variables or parent component signals
@@ -3331,6 +3897,28 @@ catch (e2) {
3331
3897
  childrenProcessed = true;
3332
3898
  };
3333
3899
 
3900
+ // A <template :if> inserts clones of template.content rather than the
3901
+ // template element itself. Those clones still need the normal directive
3902
+ // and interpolation pass. Previously the initially-visible branch was
3903
+ // inserted and left inert, so bindings such as :text and {{ expression }}
3904
+ // leaked into the rendered UI until the condition toggled.
3905
+ const processTemplateNodes = (nodes) => {
3906
+ const childScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
3907
+ // Components compiled inside a <template :if> carry setup scripts in the
3908
+ // template content. Browser-cloned scripts are inert, so register their
3909
+ // scopes and forwarded props before walking the inserted branch. This is
3910
+ // the same component hydration path used by <template :for>.
3911
+ hydrateComponentScopes(nodes, childScope);
3912
+ nodes.forEach(node => {
3913
+ if (!node.__stx_disposers && node.tagName !== 'SCRIPT')
3914
+ processElement(node, childScope);
3915
+ if (node.nodeType === 1) {
3916
+ node.removeAttribute('x-cloak');
3917
+ node.querySelectorAll('[x-cloak]').forEach(c => c.removeAttribute('x-cloak'));
3918
+ }
3919
+ });
3920
+ };
3921
+
3334
3922
  // Evaluate the :if expression \u2014 use direct signal read for simple refs,
3335
3923
  // falling back to evalExpr for complex expressions
3336
3924
  const fullScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
@@ -3359,16 +3947,8 @@ catch (e2) {
3359
3947
  currentNodes = Array.from(el.content.childNodes).map(n => n.cloneNode(true));
3360
3948
  const insertionAnchor = placeholder.nextSibling;
3361
3949
  currentNodes.forEach(node => parent.insertBefore(node, insertionAnchor));
3362
- // Process the new nodes \u2014 use LIVE componentScope so all functions are available
3363
- const childScope = { ...componentScope, ...(capturedElementScope || {}) };
3364
- currentNodes.forEach(node => {
3365
- if (node.nodeType === 1) {
3366
- processElement(node, childScope);
3367
- // Remove x-cloak from processed clones
3368
- node.removeAttribute('x-cloak');
3369
- node.querySelectorAll('[x-cloak]').forEach(c => c.removeAttribute('x-cloak'));
3370
- }
3371
- });
3950
+ processTemplateNodes(currentNodes);
3951
+ childrenProcessed = true;
3372
3952
  // Stamp insertion time for click propagation guard
3373
3953
  currentNodes.forEach(function(n) { if (n.nodeType === 1) n.__stx_shown_at = performance.now(); });
3374
3954
  isInserted = true;
@@ -3380,12 +3960,22 @@ else if (!value && isInserted) {
3380
3960
  currentNodes.forEach(node => node.remove());
3381
3961
  currentNodes = [];
3382
3962
  isInserted = false;
3963
+ childrenProcessed = false;
3964
+ }
3965
+ else if (value && isInserted && !childrenProcessed) {
3966
+ // Initial template content is cloned before the effect runs. Process
3967
+ // it after the condition is confirmed so hidden branches stay inert.
3968
+ childrenProcessed = true;
3969
+ setTimeout(function() {
3970
+ if (isInserted) processTemplateNodes(currentNodes);
3971
+ }, 0);
3383
3972
  }
3384
3973
  }
3385
3974
  else {
3386
3975
  if (value && !isInserted) {
3387
3976
  console.log('[stx] bindIf INSERTING element for :if=' + expr);
3388
- parent.insertBefore(el, placeholder.nextSibling);
3977
+ const insertionAnchor = placeholder.nextSibling;
3978
+ currentNodes.forEach(node => parent.insertBefore(node, insertionAnchor));
3389
3979
  el.__stx_shown_at = performance.now();
3390
3980
  isInserted = true;
3391
3981
  }
@@ -3400,7 +3990,7 @@ else if (!value && isInserted) {
3400
3990
  // once (#1737). Permanent disposal stays in bindFor (item removal)
3401
3991
  // and cleanupContainer (SPA navigation). The double-bind guards make
3402
3992
  // re-show idempotent, so toggling doesn't leak.
3403
- el.remove();
3993
+ currentNodes.forEach(node => node.remove());
3404
3994
  isInserted = false;
3405
3995
  console.log('[stx] bindIf REMOVED, el.isConnected:', el.isConnected);
3406
3996
  }
@@ -3415,7 +4005,15 @@ else if (!value && isInserted) {
3415
4005
  childrenProcessed = true;
3416
4006
  setTimeout(function() {
3417
4007
  var childScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
3418
- processElement(el, childScope);
4008
+ // A false single-element conditional is detached before the
4009
+ // DOMContentLoaded component-scope pass. When it becomes visible,
4010
+ // hydrate any component roots in the restored subtree first so
4011
+ // projected slot bindings and component-local directives receive
4012
+ // the same lifecycle as components inserted by template :if.
4013
+ var wasTrackedBeforeConditionalHydration = !!el.__stx_disposers;
4014
+ hydrateComponentScopes(currentNodes, childScope);
4015
+ if (wasTrackedBeforeConditionalHydration || !el.__stx_disposers)
4016
+ processElement(el, childScope);
3419
4017
  // Remove x-cloak from the inserted subtree \u2014 the initial
3420
4018
  // cloak removal (after processElement on the root) already
3421
4019
  // ran before this deferred processing, so newly-inserted
@@ -3677,8 +4275,16 @@ else if (timer === null) {
3677
4275
  }
3678
4276
 
3679
4277
  function useClickOutside(target, handler) {
4278
+ function resolveTarget() {
4279
+ if (typeof target === 'string') return document.querySelector(target);
4280
+ if (target && typeof target === 'object') {
4281
+ if ('current' in target) return target.current;
4282
+ if ('value' in target) return target.value;
4283
+ }
4284
+ return target;
4285
+ }
3680
4286
  function listener(event) {
3681
- var el = typeof target === 'string' ? document.querySelector(target) : target;
4287
+ var el = resolveTarget();
3682
4288
  if (!el) return;
3683
4289
  if (el === event.target || el.contains(event.target)) return;
3684
4290
  handler(event);
@@ -3725,6 +4331,15 @@ else if (timer === null) {
3725
4331
  };
3726
4332
  }
3727
4333
 
4334
+ // Queue work after the current synchronous signal/effect flush. This mirrors
4335
+ // the Composition API nextTick contract and gives template refs a chance to
4336
+ // resolve after a structural directive inserts their elements.
4337
+ function nextTick(fn) {
4338
+ return Promise.resolve().then(function() {
4339
+ if (fn) fn();
4340
+ });
4341
+ }
4342
+
3728
4343
  function useAsync(fn, options) {
3729
4344
  options = options || {};
3730
4345
  var asyncState = 'idle';
@@ -3863,6 +4478,61 @@ catch (e) {}
3863
4478
  return matches;
3864
4479
  }
3865
4480
 
4481
+ var scrollLockStates = new WeakMap();
4482
+
4483
+ function resolveScrollLockTarget(target) {
4484
+ var value = target;
4485
+ if (typeof value === 'function') value = value();
4486
+ if (value && typeof value === 'object' && 'current' in value) value = value.current;
4487
+ if (value && typeof value === 'object' && !value.nodeType && 'value' in value) value = value.value;
4488
+ return value || document.body || document.documentElement;
4489
+ }
4490
+
4491
+ function acquireScrollLock(element) {
4492
+ if (!element) return;
4493
+ var existing = scrollLockStates.get(element);
4494
+ if (existing) {
4495
+ existing.count += 1;
4496
+ return;
4497
+ }
4498
+ scrollLockStates.set(element, { count: 1, overflow: element.style.overflow });
4499
+ element.style.overflow = 'hidden';
4500
+ }
4501
+
4502
+ function releaseScrollLock(element) {
4503
+ if (!element) return;
4504
+ var existing = scrollLockStates.get(element);
4505
+ if (!existing) return;
4506
+ existing.count -= 1;
4507
+ if (existing.count > 0) return;
4508
+ element.style.overflow = existing.overflow;
4509
+ scrollLockStates.delete(element);
4510
+ }
4511
+
4512
+ function useScrollLock(target) {
4513
+ var locked = state(false);
4514
+ var lockedElement = null;
4515
+ var unsubscribe = locked.subscribe(function(value) {
4516
+ if (value) {
4517
+ if (lockedElement) return;
4518
+ lockedElement = resolveScrollLockTarget(target);
4519
+ acquireScrollLock(lockedElement);
4520
+ }
4521
+ else if (lockedElement) {
4522
+ releaseScrollLock(lockedElement);
4523
+ lockedElement = null;
4524
+ }
4525
+ });
4526
+ onDestroy(function() {
4527
+ unsubscribe();
4528
+ if (lockedElement) {
4529
+ releaseScrollLock(lockedElement);
4530
+ lockedElement = null;
4531
+ }
4532
+ });
4533
+ return locked;
4534
+ }
4535
+
3866
4536
  function usePreferredDark() { return useMediaQuery('(prefers-color-scheme: dark)'); }
3867
4537
  function usePreferredLight() { return useMediaQuery('(prefers-color-scheme: light)'); }
3868
4538
  function usePreferredReducedMotion() { return useMediaQuery('(prefers-reduced-motion: reduce)'); }
@@ -3950,6 +4620,30 @@ catch (e) {}
3950
4620
  return s;
3951
4621
  }
3952
4622
 
4623
+ var __stxGeneratedIdCounter = 0;
4624
+
4625
+ // Stable, scope-aware DOM ids for component authoring. A component setup
4626
+ // runs with window.__STX_CURRENT_ELEMENT__ pointing at its compiled root, so
4627
+ // the data-stx scope id is already the canonical per-instance identity. The
4628
+ // local counter supports multiple useId() calls in one component while the
4629
+ // global fallback covers page scripts and manually-mounted roots.
4630
+ function useId(prefix) {
4631
+ var normalizedPrefix = String(prefix || 'stx')
4632
+ .trim()
4633
+ .replace(/[^a-zA-Z0-9_-]+/g, '-')
4634
+ .replace(/^-+|-+$/g, '') || 'stx';
4635
+ var root = window.__STX_CURRENT_ELEMENT__;
4636
+ if (root && root.getAttribute) {
4637
+ var scopeId = root.getAttribute('data-stx-scope') || root.getAttribute('data-stx') || '';
4638
+ if (scopeId) {
4639
+ root.__stx_id_counter = (root.__stx_id_counter || 0) + 1;
4640
+ return normalizedPrefix + '-' + String(scopeId).replace(/[^a-zA-Z0-9_-]+/g, '-') + '-' + root.__stx_id_counter;
4641
+ }
4642
+ }
4643
+ __stxGeneratedIdCounter++;
4644
+ return normalizedPrefix + '-' + __stxGeneratedIdCounter;
4645
+ }
4646
+
3953
4647
  // Reactive prop binding. Bridges the gap between a parent's clientReactive
3954
4648
  // attribute (e.g. :open="modalOpen()" on a component) and the component's
3955
4649
  // internal state. Returns a signal whose value tracks the named attribute
@@ -4036,7 +4730,19 @@ catch (e) {}
4036
4730
  var raw = attribute.raw;
4037
4731
  if (raw === lastRaw) return;
4038
4732
  lastRaw = raw;
4039
- var next = hasAttr ? parse(raw) : staticValue;
4733
+ var directValues = root.__stx_direct_prop_values;
4734
+ var directValue = directValues && directValues[name];
4735
+ var next;
4736
+ if (directValue && directValue.raw === raw) {
4737
+ next = directValue.value;
4738
+ delete directValues[name];
4739
+ for (var directKey in directValues) {
4740
+ if (directValues[directKey] === directValue) delete directValues[directKey];
4741
+ }
4742
+ }
4743
+ else {
4744
+ next = hasAttr ? parse(raw) : staticValue;
4745
+ }
4040
4746
  if (next !== s()) s.set(next);
4041
4747
  });
4042
4748
  observer.observe(root, { attributes: true, attributeFilter: attributeNames });
@@ -4579,16 +5285,19 @@ catch (e) {}
4579
5285
  useCounter,
4580
5286
  useClickOutside,
4581
5287
  useFocus,
5288
+ nextTick,
4582
5289
  useAsync,
4583
5290
  useLocalStorage,
4584
5291
  useSessionStorage,
4585
5292
  useCookie,
5293
+ useId,
4586
5294
  useReactiveProp,
4587
5295
  useEventListener,
4588
5296
  useWebSocket,
4589
5297
  useColorMode,
4590
5298
  useDark,
4591
5299
  useMediaQuery,
5300
+ useScrollLock,
4592
5301
  usePreferredDark,
4593
5302
  usePreferredLight,
4594
5303
  usePreferredReducedMotion,
@@ -4633,10 +5342,23 @@ catch (e) {}
4633
5342
  }
4634
5343
  return result;
4635
5344
  },
4636
- defineEmits: function() {
5345
+ defineEmits: function(events) {
4637
5346
  var el = window.__STX_CURRENT_ELEMENT__ || null;
5347
+ var declaredEvents = Array.isArray(events)
5348
+ ? events
5349
+ : (events && typeof events === 'object' ? Object.keys(events) : []);
5350
+ if (el && declaredEvents.length) {
5351
+ el.__stx_component_emits = el.__stx_component_emits || Object.create(null);
5352
+ declaredEvents.forEach(function(event) {
5353
+ el.__stx_component_emits[event] = true;
5354
+ });
5355
+ }
4638
5356
  return function(event, payload) {
4639
5357
  var target = el || document.body;
5358
+ if (el) {
5359
+ el.__stx_component_emits = el.__stx_component_emits || Object.create(null);
5360
+ el.__stx_component_emits[event] = true;
5361
+ }
4640
5362
  target.dispatchEvent(new CustomEvent(event, { detail: payload, bubbles: true, cancelable: true }));
4641
5363
  };
4642
5364
  },
@@ -5093,6 +5815,7 @@ else {
5093
5815
  window.useCounter = useCounter;
5094
5816
  window.useClickOutside = useClickOutside;
5095
5817
  window.useFocus = useFocus;
5818
+ window.nextTick = nextTick;
5096
5819
  window.useAsync = useAsync;
5097
5820
  window.useLocalStorage = useLocalStorage;
5098
5821
  window.useEventListener = useEventListener;