@stacksjs/stx 0.2.175 → 0.2.177

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 (59) hide show
  1. package/dist/a11y.d.ts +72 -2
  2. package/dist/a11y.js +25 -10
  3. package/dist/builtins/attrs.d.ts +19 -0
  4. package/dist/builtins/attrs.js +18 -0
  5. package/dist/builtins/safe-image.js +2 -1
  6. package/dist/builtins/stx-image.js +2 -1
  7. package/dist/builtins/stx-link.js +2 -1
  8. package/dist/cli-flags.d.ts +34 -0
  9. package/dist/cli-flags.js +20 -0
  10. package/dist/cli.js +1132 -895
  11. package/dist/client-script.js +2 -1
  12. package/dist/component-registry.d.ts +9 -0
  13. package/dist/component-renderer.js +20 -1
  14. package/dist/composables/use-fetch.d.ts +6 -1
  15. package/dist/composables/use-fetch.js +40 -18
  16. package/dist/composables/use-query.d.ts +7 -2
  17. package/dist/cookie-serialize.d.ts +40 -0
  18. package/dist/cookie-serialize.js +18 -0
  19. package/dist/craft.js +670 -442
  20. package/dist/crosswind-config.d.ts +13 -0
  21. package/dist/crosswind-config.js +6 -2
  22. package/dist/dev-server/crosswind.js +2 -1
  23. package/dist/edge-runtime.d.ts +2 -9
  24. package/dist/edge-runtime.js +2 -16
  25. package/dist/includes.js +8 -4
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +3 -0
  28. package/dist/page-action.d.ts +10 -20
  29. package/dist/page-action.js +33 -9
  30. package/dist/process.js +2 -1
  31. package/dist/production-server.js +8 -7
  32. package/dist/props.d.ts +51 -8
  33. package/dist/pwa.js +637 -409
  34. package/dist/reactive.js +2 -2
  35. package/dist/resolve-stx-targets.d.ts +49 -0
  36. package/dist/resolve-stx-targets.js +38 -0
  37. package/dist/runtime-globals.d.ts +24 -0
  38. package/dist/runtime-globals.js +16 -0
  39. package/dist/runtime-injection.d.ts +36 -0
  40. package/dist/runtime-injection.js +14 -0
  41. package/dist/server-bindings.js +3 -0
  42. package/dist/signal-processing.js +38 -7
  43. package/dist/signals.js +157 -35
  44. package/dist/ssg.d.ts +23 -0
  45. package/dist/ssg.js +615 -383
  46. package/dist/ssr.js +2 -16
  47. package/dist/stx-virtual-ts.d.ts +72 -4
  48. package/dist/stx-virtual-ts.js +111 -5
  49. package/dist/stx.d.ts +66 -7
  50. package/dist/template-hydrator.d.ts +1 -1
  51. package/dist/template-hydrator.js +6 -6
  52. package/dist/theme-tokens.d.ts +88 -0
  53. package/dist/theme-tokens.js +71 -0
  54. package/dist/typecheck.d.ts +6 -1
  55. package/dist/typecheck.js +102 -67
  56. package/dist/types/config-types.d.ts +24 -0
  57. package/dist/utils.js +5 -4
  58. package/package.json +5 -5
  59. package/stx.d.ts +66 -7
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).
@@ -2769,12 +2819,16 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
2769
2819
  ? event.detail
2770
2820
  : event;
2771
2821
 
2772
- // Skip clicks on elements that just became visible in this frame.
2773
- // Prevents modal backdrop from catching the click that opened the modal.
2822
+ // Skip an opener click that reaches UI which just became visible in
2823
+ // this frame. A click whose target is inside the revealed subtree is
2824
+ // a real interaction and must not be discarded, especially for
2825
+ // async lists whose controls are usable as soon as they render.
2774
2826
  if (eventName === 'click') {
2775
2827
  var ancestor = el;
2776
2828
  while (ancestor) {
2777
- if (ancestor.__stx_shown_at && (performance.now() - ancestor.__stx_shown_at) < 50) return;
2829
+ if (ancestor.__stx_shown_at
2830
+ && (performance.now() - ancestor.__stx_shown_at) < 50
2831
+ && !(ancestor.contains && ancestor.contains(event.target))) return;
2778
2832
  ancestor = ancestor.parentElement;
2779
2833
  }
2780
2834
  }
@@ -4171,6 +4225,7 @@ catch (e) {
4171
4225
  preserveConditionalComponentScopes(b.el, capturedComponentScope);
4172
4226
  b.el.__stx_if_bound = true;
4173
4227
  b.el.__stx_chain_member = true;
4228
+ b.el.__stx_chain_active = false;
4174
4229
  b.capturedElementScope = findElementScope(b.el);
4175
4230
  b.placeholder = document.createComment('stx-if-chain');
4176
4231
  parent.insertBefore(b.placeholder, b.el);
@@ -4270,12 +4325,14 @@ catch (e2) {
4270
4325
  // components under reactive conditionals (#1737). Permanent disposal is
4271
4326
  // still handled by bindFor (item removal) and cleanupContainer (SPA nav).
4272
4327
  if (currentIdx !== -1) {
4328
+ chain[currentIdx].el.__stx_chain_active = false;
4273
4329
  chain[currentIdx].nodes.forEach(function (n) { if (n.parentNode) n.parentNode.removeChild(n); });
4274
4330
  }
4275
4331
 
4276
4332
  // Insert + process the newly-picked branch.
4277
4333
  if (pickedIdx !== -1) {
4278
4334
  var pick = chain[pickedIdx];
4335
+ pick.el.__stx_chain_active = true;
4279
4336
  // Insert this branch's node(s) before the placeholder's next sibling.
4280
4337
  // The anchor is captured once so the nodes stack in source order.
4281
4338
  var anchor = pick.placeholder.nextSibling;
@@ -5474,6 +5531,11 @@ catch (e) {} }
5474
5531
  }
5475
5532
  var s = state(initial);
5476
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;
5477
5539
  var observer = new MutationObserver(function() {
5478
5540
  var attribute = readAttribute();
5479
5541
  var hasAttr = attribute.has;
@@ -5496,7 +5558,12 @@ catch (e) {} }
5496
5558
  if (next !== s()) s.set(next);
5497
5559
  });
5498
5560
  observer.observe(root, { attributes: true, attributeFilter: attributeNames });
5499
- 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
+ });
5500
5567
  return s;
5501
5568
  }
5502
5569
 
@@ -7152,6 +7219,49 @@ else {
7152
7219
  });
7153
7220
  var pageScopeSnapshot = { ...componentScope };
7154
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
+
7155
7265
  // Process mount queue (from stx.mount() calls during loading) only after
7156
7266
  // caller setup is available for Vue-style projected slot ownership.
7157
7267
  mountQueue.forEach(function(fn) { fn(); });
@@ -7516,7 +7626,11 @@ catch (e) {
7516
7626
  var walker = document.createTreeWalker(root, showText, null);
7517
7627
  var node;
7518
7628
  while ((node = walker.nextNode())) {
7519
- 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);
7520
7634
  if (text.indexOf('{{') === -1 || text.indexOf('}}') === -1) continue;
7521
7635
  if (auditSkipped(node)) continue;
7522
7636
  found.push(text.trim().slice(0, 80));
@@ -7772,8 +7886,14 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
7772
7886
  scopeId = '__stx_scope_spa_' + (++window.stx._scopeCounter);
7773
7887
  el.setAttribute('data-stx-scope', scopeId);
7774
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');
7775
7895
  console.log('[stx:load] initializing x-data scope:', scopeId, xdata.substring(0, 40));
7776
- window.__stx_reactive.initScope(el, xdata, [], {}, null);
7896
+ window.__stx_reactive.initScope(el, xdata, [], {}, xinit || null);
7777
7897
  });
7778
7898
  }
7779
7899
 
@@ -7864,8 +7984,10 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
7864
7984
  if (el.nodeType !== Node.ELEMENT_NODE) return;
7865
7985
  // Skip scoped elements - they were already processed
7866
7986
  if (el.hasAttribute && el.hasAttribute('data-stx-scope')) return;
7867
- // Detached chain members are owned by bindIfChain \u2014 skip (#1734).
7868
- 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;
7869
7991
  // Process this element's directives without recursing into children
7870
7992
  // (we'll handle children manually to skip scoped ones)
7871
7993
  const hasFor = el.hasAttribute && (el.hasAttribute('@for') || el.hasAttribute(':for') || el.hasAttribute('x-for'));
package/dist/ssg.d.ts CHANGED
@@ -46,6 +46,29 @@
46
46
  export interface SSGConfig {
47
47
  /** Directory containing pages (default: 'pages') */
48
48
  pagesDir?: string;
49
+ /**
50
+ * Directory `@include` resolves against. Defaults to the config's
51
+ * `partialsDir`.
52
+ *
53
+ * `pagesDir`, `publicDir` and `outputDir` were all overridable here and these
54
+ * two were not, so a caller could point the build at one directory layout and
55
+ * silently get another for its partials (stacksjs/stx#1921).
56
+ */
57
+ partialsDir?: string;
58
+ /** Directory components resolve against. Defaults to the config's `componentsDir`. */
59
+ componentsDir?: string;
60
+ /**
61
+ * Fail the build when an `@include` cannot be resolved (default: true).
62
+ *
63
+ * A failed include renders the error into the page and, before this, left the
64
+ * build reporting `Failed: 0` with exit 0 — so the only way to find it was to
65
+ * read the emitted HTML, which is exactly what nobody does when the build says
66
+ * success. An unresolvable include is not a recoverable condition in a build:
67
+ * it produces a file you would never knowingly deploy.
68
+ *
69
+ * Set `false` for the old behaviour.
70
+ */
71
+ failOnIncludeError?: boolean;
49
72
  /** Output directory for generated files (default: 'dist') */
50
73
  outputDir?: string;
51
74
  /** Base URL for the site (default: '/') */