@stacksjs/stx 0.2.176 → 0.2.178

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 (44) hide show
  1. package/dist/builtins/attrs.d.ts +19 -0
  2. package/dist/builtins/attrs.js +18 -0
  3. package/dist/builtins/safe-image.js +2 -1
  4. package/dist/builtins/stx-image.js +2 -1
  5. package/dist/builtins/stx-link.js +2 -1
  6. package/dist/cli-flags.d.ts +34 -0
  7. package/dist/cli-flags.js +20 -0
  8. package/dist/cli.js +899 -778
  9. package/dist/client-script.js +2 -1
  10. package/dist/component-registry.d.ts +9 -0
  11. package/dist/component-renderer.js +20 -1
  12. package/dist/composables/use-fetch.d.ts +6 -1
  13. package/dist/composables/use-fetch.js +40 -18
  14. package/dist/composables/use-query.d.ts +7 -2
  15. package/dist/cookie-serialize.d.ts +40 -0
  16. package/dist/cookie-serialize.js +18 -0
  17. package/dist/craft.js +500 -381
  18. package/dist/crosswind-config.d.ts +13 -0
  19. package/dist/crosswind-config.js +6 -2
  20. package/dist/dev-server/crosswind.js +2 -1
  21. package/dist/edge-runtime.d.ts +2 -9
  22. package/dist/edge-runtime.js +2 -16
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/page-action.d.ts +10 -20
  26. package/dist/page-action.js +33 -9
  27. package/dist/production-server.js +8 -7
  28. package/dist/pwa.js +452 -333
  29. package/dist/reactive.js +2 -3
  30. package/dist/signal-processing.js +37 -6
  31. package/dist/signals.js +150 -32
  32. package/dist/ssg.js +465 -346
  33. package/dist/ssr.js +2 -16
  34. package/dist/stx-virtual-ts.d.ts +45 -1
  35. package/dist/stx-virtual-ts.js +75 -5
  36. package/dist/stx.d.ts +66 -7
  37. package/dist/template-hydrator.d.ts +1 -1
  38. package/dist/template-hydrator.js +6 -6
  39. package/dist/theme-tokens.d.ts +88 -0
  40. package/dist/theme-tokens.js +71 -0
  41. package/dist/typecheck.d.ts +6 -1
  42. package/dist/typecheck.js +7 -6
  43. package/package.json +5 -5
  44. package/stx.d.ts +66 -7
package/dist/reactive.js CHANGED
@@ -529,7 +529,6 @@ function removeReactiveAttributes(template) {
529
529
  return output;
530
530
  }
531
531
  export function processReactiveDirectives(template, context, _filePath) {
532
- scopeCounter = 0;
533
532
  if (!hasReactiveDirectives(template))
534
533
  return template;
535
534
  const scopes = findReactiveScopes(template);
@@ -585,8 +584,8 @@ function finalizeTemplate(template, scopes) {
585
584
  });
586
585
  output = output.replace(/\s*x-data\s*=\s*"([^"]*)"/g, ' data-stx-xdata="$1"');
587
586
  output = output.replace(/\s*x-data\s*=\s*'([^']*)'/g, " data-stx-xdata='$1'");
588
- output = output.replace(/\s*x-init\s*=\s*"[^"]*"/g, "");
589
- output = output.replace(/\s*x-init\s*=\s*'[^']*'/g, "");
587
+ output = output.replace(/\s*x-init\s*=\s*"([^"]*)"/g, ' data-stx-xinit="$1"');
588
+ output = output.replace(/\s*x-init\s*=\s*'([^']*)'/g, " data-stx-xinit='$1'");
590
589
  return output;
591
590
  }
592
591
  export function hasReactiveDirectives(template) {
@@ -90,6 +90,30 @@ function extractClientSignalNames(template) {
90
90
  }
91
91
  return names;
92
92
  }
93
+ function collectClientLoopScopes(template) {
94
+ if (!/[:@]for\s*=/.test(template) && !/\bx-for\s*=/.test(template))
95
+ return [];
96
+ const openingTag = new RegExp(`<([a-zA-Z][\\w-]*)\\b(${TAG_ATTR_RUN})>`, "iy"), loopAttr = /(?:^|\s)(?::for|@for|x-for)\s*=\s*(?:"([^"]*)"|'([^']*)')/i, starts = scanAtElementPosition(template, (html, index) => {
97
+ openingTag.lastIndex = index;
98
+ const match = openingTag.exec(html);
99
+ if (!match || match.index !== index || !loopAttr.test(match[2]))
100
+ return -1;
101
+ return openingTag.lastIndex;
102
+ }), scopes = [];
103
+ for (const start of starts) {
104
+ openingTag.lastIndex = 0;
105
+ const match = openingTag.exec(start.token);
106
+ if (!match)
107
+ continue;
108
+ const attrMatch = match[2].match(loopAttr), parsed = parseLoopExpression(attrMatch?.[1] ?? attrMatch?.[2] ?? ""), vars = [parsed.itemVar, parsed.indexVar].filter(Boolean);
109
+ if (vars.length === 0)
110
+ continue;
111
+ const tag = match[1], end = /\/\s*>$/.test(start.token) || VOID_ELEMENT_TAGS.has(tag.toLowerCase()) ? start.end : findElementEnd(template, start.start, tag, start.end);
112
+ if (end !== -1)
113
+ scopes.push({ start: start.start, end, vars: new Set(vars) });
114
+ }
115
+ return scopes;
116
+ }
93
117
  const JS_NON_VARS = new Set([
94
118
  "true",
95
119
  "false",
@@ -132,7 +156,7 @@ const JS_NON_VARS = new Set([
132
156
  function stripExprStrings(expr) {
133
157
  return expr.replace(/'(?:\\.|[^'\\])*'/g, "''").replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/`(?:\\.|[^`\\])*`/g, "``");
134
158
  }
135
- function conditionIsClientReactive(condition, signalNames, context) {
159
+ function conditionIsClientReactive(condition, signalNames, context, clientLoopVars) {
136
160
  if (!condition)
137
161
  return !1;
138
162
  const expr = stripExprStrings(condition);
@@ -145,7 +169,7 @@ function conditionIsClientReactive(condition, signalNames, context) {
145
169
  continue;
146
170
  if (JS_NON_VARS.has(id))
147
171
  continue;
148
- if (signalNames.has(id))
172
+ if (signalNames.has(id) || clientLoopVars?.has(id))
149
173
  refsSignal = !0;
150
174
  else if (context && id in context)
151
175
  refsServerVar = !0;
@@ -186,7 +210,14 @@ export function convertSignalDirectivesToAttributes(template, context) {
186
210
  const signalNames = extractClientSignalNames(template);
187
211
  function convert(input) {
188
212
  let output = input;
189
- const ifDirectiveStart = /@if\s*\(/g;
213
+ const clientLoopScopes = collectClientLoopScopes(input), loopVarsAt = (position) => {
214
+ const vars = new Set;
215
+ for (const scope of clientLoopScopes)
216
+ if (position > scope.start && position < scope.end)
217
+ for (const name of scope.vars)
218
+ vars.add(name);
219
+ return vars;
220
+ }, ifDirectiveStart = /@if\s*\(/g;
190
221
  let match;
191
222
  const replacements = [];
192
223
  while ((match = ifDirectiveStart.exec(output)) !== null) {
@@ -224,8 +255,8 @@ export function convertSignalDirectivesToAttributes(template, context) {
224
255
  const parsed = findIfBlocks(output.substring(startIdx, endIdx));
225
256
  if (parsed.length === 0)
226
257
  continue;
227
- const valueBranches = parsed[0].branches.filter((b) => b.type !== "else");
228
- if (!(valueBranches.length > 0 && valueBranches.every((b) => conditionIsClientReactive(b.condition, signalNames, context))))
258
+ const valueBranches = parsed[0].branches.filter((b) => b.type !== "else"), clientLoopVars = loopVarsAt(startIdx);
259
+ if (!(valueBranches.length > 0 && valueBranches.every((b) => conditionIsClientReactive(b.condition, signalNames, context, clientLoopVars))))
229
260
  continue;
230
261
  replacement = parsed[0].branches.map((branch) => {
231
262
  const branchContent = convert(branch.content);
@@ -237,7 +268,7 @@ export function convertSignalDirectivesToAttributes(template, context) {
237
268
  }).join(`
238
269
  `);
239
270
  } else {
240
- if (!conditionIsClientReactive(condition, signalNames, context))
271
+ if (!conditionIsClientReactive(condition, signalNames, context, loopVarsAt(startIdx)))
241
272
  continue;
242
273
  replacement = branchToAttrElement(convert(content), "@if", condition);
243
274
  }
package/dist/signals.js CHANGED
@@ -1065,6 +1065,10 @@ finally {
1065
1065
  // case where a template is most likely driving a spinner off it (#1818).
1066
1066
  const immediate = options.immediate !== false;
1067
1067
  const loading = state(immediate);
1068
+ // Any request in flight, background ones included. loading answers "is
1069
+ // there nothing to show yet"; isFetching answers "is a request open" \u2014 and
1070
+ // a background refresh is the case where those two stop agreeing (#1929).
1071
+ const isFetching = state(immediate);
1068
1072
  const error = state(null);
1069
1073
  if (options.suspense) registerSuspense(loading, error);
1070
1074
 
@@ -1074,17 +1078,24 @@ finally {
1074
1078
  // last in-flight one. State writes are guarded on signal.aborted (#1871).
1075
1079
  let __stxAbort = null;
1076
1080
 
1077
- const fetchData = async () => {
1081
+ // runOptions: { background: true } refreshes data WITHOUT touching loading
1082
+ // or clearing the current error, so a poll cannot flash a spinner or blank
1083
+ // an error message that is still true. Everything already on screen stays
1084
+ // there until the new response actually lands (#1929).
1085
+ const fetchData = async (runOptions) => {
1086
+ const background = !!(runOptions && runOptions.background);
1078
1087
  if (__stxAbort) __stxAbort.abort();
1079
1088
  __stxAbort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
1080
1089
  const __signal = __stxAbort ? __stxAbort.signal : undefined;
1081
- loading.set(true);
1082
- error.set(null);
1090
+ if (!background) loading.set(true);
1091
+ isFetching.set(true);
1092
+ if (!background) error.set(null);
1083
1093
 
1084
1094
  try {
1085
1095
  const url = typeof urlOrFn === 'function' ? urlOrFn() : urlOrFn;
1086
1096
  if (!url) {
1087
- loading.set(false);
1097
+ if (!background) loading.set(false);
1098
+ isFetching.set(false);
1088
1099
  return;
1089
1100
  }
1090
1101
 
@@ -1114,6 +1125,10 @@ finally {
1114
1125
  // Apply transform if provided
1115
1126
  const transformed = options.transform ? options.transform(result) : result;
1116
1127
  data.set(transformed);
1128
+ // A background run did not clear the error up front, so a recovery has
1129
+ // to clear it here \u2014 otherwise the first failed poll would leave the
1130
+ // message on screen for the life of the page.
1131
+ if (background) error.set(null);
1117
1132
  }
1118
1133
  catch (e) {
1119
1134
  // A superseded/unmounted request rejects with AbortError \u2014 expected,
@@ -1128,7 +1143,10 @@ catch (e) {
1128
1143
  }
1129
1144
  finally {
1130
1145
  // If a newer run (or onDestroy) aborted this one, that run owns loading.
1131
- if (!__signal || !__signal.aborted) loading.set(false);
1146
+ if (!__signal || !__signal.aborted) {
1147
+ if (!background) loading.set(false);
1148
+ isFetching.set(false);
1149
+ }
1132
1150
  }
1133
1151
  };
1134
1152
 
@@ -1161,6 +1179,7 @@ else if (immediate) {
1161
1179
  return {
1162
1180
  data,
1163
1181
  loading,
1182
+ isFetching,
1164
1183
  error,
1165
1184
  refetch: fetchData,
1166
1185
  // Convenience getters
@@ -1411,6 +1430,10 @@ else if (immediate) {
1411
1430
  var cacheKey = options.cacheKey || (typeof url === 'function' ? null : url);
1412
1431
  var data = state(options.initialData || null);
1413
1432
  var loading = state(true);
1433
+ // See useFetch: loading means "nothing to show yet", isFetching means "a
1434
+ // request is open" \u2014 including a background refresh, which is the case that
1435
+ // makes the distinction necessary (#1929).
1436
+ var isFetching = state(true);
1414
1437
  var error = state(null);
1415
1438
  var isStale = state(false);
1416
1439
  if (options.suspense) registerSuspense(loading, error);
@@ -1429,16 +1452,21 @@ else if (immediate) {
1429
1452
  // can release its claim without cancelling a request someone else joined.
1430
1453
  var __joined = null;
1431
1454
 
1432
- var fetchData = async function() {
1455
+ // runOptions: { background: true } refreshes without touching loading or
1456
+ // clearing the current error. A poll must not flash a spinner over data
1457
+ // that is already on screen (#1929).
1458
+ var fetchData = async function(runOptions) {
1459
+ var background = !!(runOptions && runOptions.background);
1433
1460
  var resolvedUrl = typeof url === 'function' ? url() : url;
1434
- if (!resolvedUrl) { loading.set(false); return; }
1461
+ if (!resolvedUrl) { if (!background) loading.set(false); isFetching.set(false); return; }
1435
1462
  var key = cacheKey || resolvedUrl;
1436
1463
 
1437
1464
  // Check cache
1438
1465
  var cached = _queryCache[key];
1439
1466
  if (cached && (Date.now() - cached.timestamp < staleTime)) {
1440
1467
  data.set(cached.data);
1441
- loading.set(false);
1468
+ if (!background) loading.set(false);
1469
+ isFetching.set(false);
1442
1470
  isStale.set(false);
1443
1471
  if (options.onSuccess) options.onSuccess(cached.data);
1444
1472
  return;
@@ -1458,12 +1486,14 @@ else if (immediate) {
1458
1486
  if (shared && shared.owner !== __instanceId) {
1459
1487
  shared.refs++;
1460
1488
  __joined = shared;
1461
- loading.set(true);
1462
- error.set(null);
1489
+ if (!background) loading.set(true);
1490
+ isFetching.set(true);
1491
+ if (!background) error.set(null);
1463
1492
  try {
1464
1493
  var joinedResult = await shared.promise;
1465
1494
  var joinedTransformed = options.transform ? options.transform(joinedResult) : joinedResult;
1466
1495
  data.set(joinedTransformed);
1496
+ if (background) error.set(null);
1467
1497
  isStale.set(false);
1468
1498
  if (options.onSuccess) options.onSuccess(joinedTransformed);
1469
1499
  }
@@ -1475,7 +1505,8 @@ else if (immediate) {
1475
1505
  finally {
1476
1506
  shared.refs--;
1477
1507
  if (__joined === shared) __joined = null;
1478
- loading.set(false);
1508
+ if (!background) loading.set(false);
1509
+ isFetching.set(false);
1479
1510
  }
1480
1511
  return;
1481
1512
  }
@@ -1490,8 +1521,9 @@ else if (immediate) {
1490
1521
  __joined = null;
1491
1522
  __stxAbort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
1492
1523
  var __signal = __stxAbort ? __stxAbort.signal : undefined;
1493
- loading.set(true);
1494
- error.set(null);
1524
+ if (!background) loading.set(true);
1525
+ isFetching.set(true);
1526
+ if (!background) error.set(null);
1495
1527
  try {
1496
1528
  var fetchOpts = { method: 'GET', headers: { 'Content-Type': 'application/json', ...__stxHeaders(options.headers) }, signal: __signal };
1497
1529
  // Published BEFORE it is awaited, so a caller starting in the same tick
@@ -1515,6 +1547,7 @@ else if (immediate) {
1515
1547
  if (__signal && __signal.aborted) return;
1516
1548
  var transformed = options.transform ? options.transform(result) : result;
1517
1549
  data.set(transformed);
1550
+ if (background) error.set(null);
1518
1551
  isStale.set(false);
1519
1552
  _queryCache[key] = { data: transformed, timestamp: Date.now() };
1520
1553
  if (options.onSuccess) options.onSuccess(transformed);
@@ -1529,7 +1562,10 @@ catch (e) {
1529
1562
  if (options.onError) options.onError(e);
1530
1563
  }
1531
1564
  finally {
1532
- if (!__signal || !__signal.aborted) loading.set(false);
1565
+ if (!__signal || !__signal.aborted) {
1566
+ if (!background) loading.set(false);
1567
+ isFetching.set(false);
1568
+ }
1533
1569
  }
1534
1570
  };
1535
1571
 
@@ -1537,13 +1573,20 @@ finally {
1537
1573
  onMount(fetchData);
1538
1574
  }
1539
1575
 
1576
+ // A refetch nobody asked for does not set loading. Both of these refresh
1577
+ // data that is ALREADY on screen, so driving the first-load state from them
1578
+ // put a spinner over a populated view once a minute, forever \u2014 the reason a
1579
+ // polling view could not use this composable at all (#1929). Bind
1580
+ // isFetching for a subtle in-flight indicator.
1581
+ var backgroundRun = { background: true };
1582
+
1540
1583
  // refetchOnFocus \u2014 removed on destroy, like refetchInterval right below.
1541
1584
  // It used to add a listener and never remove it, so every instance leaked
1542
1585
  // one for the life of the page and each SPA navigation added more; every
1543
1586
  // stale listener still fired, refetching for components long gone (#1818).
1544
1587
  if (options.refetchOnFocus) {
1545
1588
  var onVisibility = function() {
1546
- if (!document.hidden) fetchData();
1589
+ if (!document.hidden) fetchData(backgroundRun);
1547
1590
  };
1548
1591
  document.addEventListener('visibilitychange', onVisibility);
1549
1592
  onDestroy(function() { document.removeEventListener('visibilitychange', onVisibility); });
@@ -1557,7 +1600,7 @@ finally {
1557
1600
  if (typeof document !== 'undefined' && document.hidden) return;
1558
1601
  var en = options.enabled;
1559
1602
  if (typeof en === 'function' ? !en() : en === false) return;
1560
- var p = fetchData();
1603
+ var p = fetchData(backgroundRun);
1561
1604
  if (options.stopOnError && p && typeof p.then === 'function') {
1562
1605
  p.then(function() { if (error()) clearInterval(intervalId); });
1563
1606
  }
@@ -1571,13 +1614,14 @@ finally {
1571
1614
  return {
1572
1615
  data: data,
1573
1616
  loading: loading,
1617
+ isFetching: isFetching,
1574
1618
  error: error,
1575
1619
  isStale: isStale,
1576
1620
  refetch: fetchData,
1577
- invalidate: function() {
1621
+ invalidate: function(runOptions) {
1578
1622
  var key = cacheKey || (typeof url === 'function' ? url() : url);
1579
1623
  delete _queryCache[key];
1580
- return fetchData();
1624
+ return fetchData(runOptions);
1581
1625
  }
1582
1626
  };
1583
1627
  }
@@ -2145,11 +2189,18 @@ catch (e) {
2145
2189
  if (!el || !el.getAttribute || value === undefined) return;
2146
2190
  var scopeId = el.getAttribute('data-stx-scope');
2147
2191
  var scopeVars = scopeId && window.stx._scopes && window.stx._scopes[scopeId];
2148
- if (!scopeVars) return;
2149
2192
  var camelName = name.replace(/-([a-z0-9])/g, function(_, character) {
2150
2193
  return character.toUpperCase();
2151
2194
  });
2152
- var propSignal = scopeVars[name] || scopeVars[camelName];
2195
+ // useReactiveProp signals are often intentionally aliased inside the
2196
+ // child, for example liveClosable = useReactiveProp('closable', true).
2197
+ // The public prop name therefore cannot be inferred from the variable
2198
+ // name exported by the component scope. Keep an element-owned registry as
2199
+ // the canonical bridge, with the scope lookup retained for older output.
2200
+ var propSignals = el.__stx_reactive_prop_signals;
2201
+ var propSignal = propSignals && (propSignals[name] || propSignals[camelName]);
2202
+ if (!propSignal && scopeVars)
2203
+ propSignal = scopeVars[name] || scopeVars[camelName];
2153
2204
  if (!propSignal || !propSignal._isSignal || typeof propSignal.set !== 'function') return;
2154
2205
  var directValues = el.__stx_direct_prop_values || (el.__stx_direct_prop_values = {});
2155
2206
  var directValue = {
@@ -2477,13 +2528,12 @@ else if (part) {
2477
2528
  }
2478
2529
 
2479
2530
  // x-else / x-else-if are owned by their preceding x-if chain (#1734).
2480
- // A detached chain member reached via the parent's snapshot child-
2481
- // iteration must be skipped \u2014 bindIfChain manages its lifecycle and
2482
- // only the active branch is (re)connected. Connected chain members
2483
- // (the active branch) fall through; their if/else attr was already
2484
- // removed by bindIfChain, and double-bind guards make any redundant
2485
- // pass idempotent.
2486
- if (el.__stx_chain_member && !el.isConnected) return;
2531
+ // An inactive chain member reached via the parent's snapshot child-
2532
+ // iteration must be skipped. Connectivity cannot identify inactivity:
2533
+ // an active inner branch is also disconnected while an outer conditional
2534
+ // temporarily detaches its parent. bindIfChain owns explicit branch state
2535
+ // so nested structural effects can still hydrate active detached subtrees.
2536
+ if (el.__stx_chain_member && !el.__stx_chain_active) return;
2487
2537
 
2488
2538
  // An else/else-if with no preceding if is an orphan. Match Vue: warn,
2489
2539
  // strip the attribute, and render it as a plain element (fall through).
@@ -4175,6 +4225,7 @@ catch (e) {
4175
4225
  preserveConditionalComponentScopes(b.el, capturedComponentScope);
4176
4226
  b.el.__stx_if_bound = true;
4177
4227
  b.el.__stx_chain_member = true;
4228
+ b.el.__stx_chain_active = false;
4178
4229
  b.capturedElementScope = findElementScope(b.el);
4179
4230
  b.placeholder = document.createComment('stx-if-chain');
4180
4231
  parent.insertBefore(b.placeholder, b.el);
@@ -4274,12 +4325,14 @@ catch (e2) {
4274
4325
  // components under reactive conditionals (#1737). Permanent disposal is
4275
4326
  // still handled by bindFor (item removal) and cleanupContainer (SPA nav).
4276
4327
  if (currentIdx !== -1) {
4328
+ chain[currentIdx].el.__stx_chain_active = false;
4277
4329
  chain[currentIdx].nodes.forEach(function (n) { if (n.parentNode) n.parentNode.removeChild(n); });
4278
4330
  }
4279
4331
 
4280
4332
  // Insert + process the newly-picked branch.
4281
4333
  if (pickedIdx !== -1) {
4282
4334
  var pick = chain[pickedIdx];
4335
+ pick.el.__stx_chain_active = true;
4283
4336
  // Insert this branch's node(s) before the placeholder's next sibling.
4284
4337
  // The anchor is captured once so the nodes stack in source order.
4285
4338
  var anchor = pick.placeholder.nextSibling;
@@ -5478,6 +5531,11 @@ catch (e) {} }
5478
5531
  }
5479
5532
  var s = state(initial);
5480
5533
  if (!root) return s;
5534
+ var propSignals = root.__stx_reactive_prop_signals
5535
+ || (root.__stx_reactive_prop_signals = {});
5536
+ propSignals[name] = s;
5537
+ propSignals[kebabName] = s;
5538
+ propSignals[lowerName] = s;
5481
5539
  var observer = new MutationObserver(function() {
5482
5540
  var attribute = readAttribute();
5483
5541
  var hasAttr = attribute.has;
@@ -5500,7 +5558,12 @@ catch (e) {} }
5500
5558
  if (next !== s()) s.set(next);
5501
5559
  });
5502
5560
  observer.observe(root, { attributes: true, attributeFilter: attributeNames });
5503
- onDestroy(function() { observer.disconnect(); });
5561
+ onDestroy(function() {
5562
+ observer.disconnect();
5563
+ attributeNames.forEach(function(attributeName) {
5564
+ if (propSignals[attributeName] === s) delete propSignals[attributeName];
5565
+ });
5566
+ });
5504
5567
  return s;
5505
5568
  }
5506
5569
 
@@ -7156,6 +7219,49 @@ else {
7156
7219
  });
7157
7220
  var pageScopeSnapshot = { ...componentScope };
7158
7221
 
7222
+ /*
7223
+ * Any x-data scope the reactive bridge did not reach, initialized here.
7224
+ *
7225
+ * The bridge script addresses each scope by a selector it generated, and
7226
+ * that numbering restarts per render - so a page carrying three component
7227
+ * islands emits three scripts all looking for the same scope id, and two of
7228
+ * the three scopes are never initialized at all. The symptom is the
7229
+ * quietest one available: the markup is there, every binding shows its
7230
+ * initial value, and whatever x-init was meant to start never starts.
7231
+ *
7232
+ * Registered scopes are skipped, so the bridge stays authoritative where it
7233
+ * did run and this only picks up what it missed. The init expression is
7234
+ * read from data-stx-xinit, which the server preserves for exactly this.
7235
+ *
7236
+ * No backticks in this comment, deliberately: it lives inside the runtime
7237
+ * source that is embedded as a template literal, and one would end the
7238
+ * literal and take the whole file's syntax with it.
7239
+ */
7240
+ if (window.__stx_reactive && window.__stx_reactive.initScope) {
7241
+ document.querySelectorAll('[data-stx-xdata]').forEach(function(el) {
7242
+ var scopeId = el.getAttribute('data-stx-scope');
7243
+ if (scopeId && window.stx._scopes && window.stx._scopes[scopeId]) return;
7244
+ if (el.__stx_scope_initialized) return;
7245
+
7246
+ var xdata = el.getAttribute('data-stx-xdata');
7247
+ if (!xdata) return;
7248
+
7249
+ if (!scopeId) {
7250
+ scopeId = '__stx_scope_island_' + (++window.stx._scopeCounter);
7251
+ el.setAttribute('data-stx-scope', scopeId);
7252
+ }
7253
+
7254
+ el.__stx_scope_initialized = true;
7255
+
7256
+ try {
7257
+ window.__stx_reactive.initScope(el, xdata, [], {}, el.getAttribute('data-stx-xinit') || null);
7258
+ }
7259
+ catch (e) {
7260
+ console.error('[stx] island scope init error:', e);
7261
+ }
7262
+ });
7263
+ }
7264
+
7159
7265
  // Process mount queue (from stx.mount() calls during loading) only after
7160
7266
  // caller setup is available for Vue-style projected slot ownership.
7161
7267
  mountQueue.forEach(function(fn) { fn(); });
@@ -7520,7 +7626,11 @@ catch (e) {
7520
7626
  var walker = document.createTreeWalker(root, showText, null);
7521
7627
  var node;
7522
7628
  while ((node = walker.nextNode())) {
7523
- var text = node.nodeValue || '';
7629
+ // Browsers expose Text.nodeValue as a string, while lightweight DOM and
7630
+ // native embedding shims can preserve an assigned number. Diagnostics
7631
+ // must normalize the boundary instead of aborting the entire audit on a
7632
+ // missing String#indexOf method.
7633
+ var text = node.nodeValue == null ? '' : String(node.nodeValue);
7524
7634
  if (text.indexOf('{{') === -1 || text.indexOf('}}') === -1) continue;
7525
7635
  if (auditSkipped(node)) continue;
7526
7636
  found.push(text.trim().slice(0, 80));
@@ -7776,8 +7886,14 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
7776
7886
  scopeId = '__stx_scope_spa_' + (++window.stx._scopeCounter);
7777
7887
  el.setAttribute('data-stx-scope', scopeId);
7778
7888
  }
7889
+ // The init expression, which used to be dropped here. A scope
7890
+ // re-initialized with its data and none of its setup renders every
7891
+ // binding at its initial value and starts nothing - a live region
7892
+ // showing its empty state forever, which is indistinguishable from one
7893
+ // where nothing has happened yet.
7894
+ var xinit = el.getAttribute('data-stx-xinit');
7779
7895
  console.log('[stx:load] initializing x-data scope:', scopeId, xdata.substring(0, 40));
7780
- window.__stx_reactive.initScope(el, xdata, [], {}, null);
7896
+ window.__stx_reactive.initScope(el, xdata, [], {}, xinit || null);
7781
7897
  });
7782
7898
  }
7783
7899
 
@@ -7868,8 +7984,10 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
7868
7984
  if (el.nodeType !== Node.ELEMENT_NODE) return;
7869
7985
  // Skip scoped elements - they were already processed
7870
7986
  if (el.hasAttribute && el.hasAttribute('data-stx-scope')) return;
7871
- // Detached chain members are owned by bindIfChain \u2014 skip (#1734).
7872
- if (el.__stx_chain_member && !el.isConnected) return;
7987
+ // Inactive chain members are owned by bindIfChain. Use explicit branch
7988
+ // state because an active nested branch can live under a detached outer
7989
+ // conditional while its reactive effects continue to run.
7990
+ if (el.__stx_chain_member && !el.__stx_chain_active) return;
7873
7991
  // Process this element's directives without recursing into children
7874
7992
  // (we'll handle children manually to skip scoped ones)
7875
7993
  const hasFor = el.hasAttribute && (el.hasAttribute('@for') || el.hasAttribute(':for') || el.hasAttribute('x-for'));