@stacksjs/stx 0.2.155 → 0.2.157

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 (241) hide show
  1. package/dist/a11y.d.ts +87 -46
  2. package/dist/animation.d.ts +96 -42
  3. package/dist/appearance-bootstrap.d.ts +49 -15
  4. package/dist/asset-roots.d.ts +32 -0
  5. package/dist/asset-roots.js +7 -0
  6. package/dist/assets.d.ts +9 -7
  7. package/dist/async-components.d.ts +78 -56
  8. package/dist/auth.d.ts +3 -0
  9. package/dist/build-externalize.d.ts +40 -0
  10. package/dist/build-externalize.js +63 -0
  11. package/dist/build-id.d.ts +22 -22
  12. package/dist/build-message.d.ts +73 -2
  13. package/dist/build-message.js +47 -8
  14. package/dist/builtins/attrs.d.ts +14 -0
  15. package/dist/builtins/drawer.d.ts +22 -0
  16. package/dist/builtins/icon.d.ts +16 -2
  17. package/dist/builtins/index.d.ts +5 -15
  18. package/dist/builtins/modal.d.ts +24 -0
  19. package/dist/builtins/safe-image.d.ts +42 -0
  20. package/dist/builtins/stx-image.d.ts +27 -0
  21. package/dist/builtins/stx-link.d.ts +9 -0
  22. package/dist/builtins/stx-loading-indicator.d.ts +11 -0
  23. package/dist/builtins/suspense.d.ts +28 -0
  24. package/dist/builtins/toast.d.ts +17 -0
  25. package/dist/builtins/transition-group.d.ts +25 -0
  26. package/dist/caching.d.ts +12 -15
  27. package/dist/cli.js +1798 -908
  28. package/dist/client/directive.d.ts +7 -0
  29. package/dist/client/index.d.ts +0 -1
  30. package/dist/client-script-bundler.d.ts +63 -5
  31. package/dist/client-script-bundler.js +30 -2
  32. package/dist/client-script.d.ts +102 -47
  33. package/dist/client-script.js +15 -3
  34. package/dist/codemod.d.ts +56 -0
  35. package/dist/codemod.js +192 -0
  36. package/dist/color-mode-boot.d.ts +83 -65
  37. package/dist/component-client-factories.d.ts +1 -6
  38. package/dist/component-processing.d.ts +58 -49
  39. package/dist/component-processing.js +1 -1
  40. package/dist/component-registry.d.ts +94 -31
  41. package/dist/component-renderer.d.ts +18 -0
  42. package/dist/composable-loader.d.ts +24 -0
  43. package/dist/composable-loader.js +12 -9
  44. package/dist/composables/index.d.ts +7 -0
  45. package/dist/composables/index.js +1 -0
  46. package/dist/composables/use-fetch.js +30 -2
  47. package/dist/composables/use-form.d.ts +91 -0
  48. package/dist/composables/use-form.js +112 -0
  49. package/dist/composables.d.ts +64 -60
  50. package/dist/computed.d.ts +86 -92
  51. package/dist/conditionals.d.ts +24 -8
  52. package/dist/conditionals.js +16 -0
  53. package/dist/config.d.ts +34 -21
  54. package/dist/config.js +35 -14
  55. package/dist/craft.js +1476 -768
  56. package/dist/crosswind-config.d.ts +58 -0
  57. package/dist/crosswind-config.js +40 -0
  58. package/dist/csp.d.ts +107 -41
  59. package/dist/defer.d.ts +24 -0
  60. package/dist/dev-server/crosswind.d.ts +35 -26
  61. package/dist/dev-server/crosswind.js +2 -18
  62. package/dist/dev-server/serve-app.js +4 -3
  63. package/dist/dev-server/terminal-colors.d.ts +32 -32
  64. package/dist/directive-api.d.ts +86 -43
  65. package/dist/document-shell.d.ts +54 -41
  66. package/dist/dynamic-components.d.ts +9 -0
  67. package/dist/error-boundaries.d.ts +47 -41
  68. package/dist/error-handling.d.ts +1 -1
  69. package/dist/errors/codes.d.ts +51 -43
  70. package/dist/errors/formatter.d.ts +58 -38
  71. package/dist/errors/index.d.ts +5 -61
  72. package/dist/errors/logger.d.ts +114 -40
  73. package/dist/errors/sanitizer.d.ts +53 -23
  74. package/dist/errors/types.d.ts +44 -31
  75. package/dist/events.d.ts +86 -25
  76. package/dist/expressions.d.ts +50 -42
  77. package/dist/forms-validation.d.ts +138 -123
  78. package/dist/forms-validation.js +1 -90
  79. package/dist/forms.d.ts +120 -66
  80. package/dist/forms.js +31 -11
  81. package/dist/fragment-container.d.ts +57 -0
  82. package/dist/fragment-container.js +109 -0
  83. package/dist/framework-composables.d.ts +32 -0
  84. package/dist/head.d.ts +156 -114
  85. package/dist/heatmap.d.ts +152 -72
  86. package/dist/html-masking.d.ts +40 -34
  87. package/dist/hydration-runtime.d.ts +20 -24
  88. package/dist/i18n.d.ts +93 -32
  89. package/dist/image-optimization/auto-sibling.d.ts +32 -4
  90. package/dist/includes.d.ts +69 -7
  91. package/dist/index.d.ts +66 -0
  92. package/dist/index.js +23 -0
  93. package/dist/internal-markdown.d.ts +12 -8
  94. package/dist/island-chunking.d.ts +30 -13
  95. package/dist/keep-alive.d.ts +72 -52
  96. package/dist/layout-types.d.ts +45 -0
  97. package/dist/layout-types.js +56 -0
  98. package/dist/lazy-module.d.ts +10 -0
  99. package/dist/loops.d.ts +66 -0
  100. package/dist/loops.js +47 -6
  101. package/dist/markdown.d.ts +0 -1
  102. package/dist/media/image/component.d.ts +25 -9
  103. package/dist/media/image/directive.d.ts +15 -1
  104. package/dist/media/image/placeholder.d.ts +20 -9
  105. package/dist/media/image/processor/cache.d.ts +40 -18
  106. package/dist/media/image/processor/index.d.ts +1 -10
  107. package/dist/media/image/processor/optimizer.d.ts +12 -1
  108. package/dist/media/image/processor/responsive.d.ts +16 -6
  109. package/dist/media/image/srcset.d.ts +47 -50
  110. package/dist/media/shared/cache.d.ts +51 -31
  111. package/dist/media/shared/hash.d.ts +7 -0
  112. package/dist/media/types.d.ts +1235 -754
  113. package/dist/media/video/directive.d.ts +15 -1
  114. package/dist/media/video/index.d.ts +39 -17
  115. package/dist/media/video/processor/cache.d.ts +42 -19
  116. package/dist/media/video/processor/index.d.ts +1 -18
  117. package/dist/media/video/processor/streaming.d.ts +17 -10
  118. package/dist/media/video/processor/thumbnail.d.ts +21 -15
  119. package/dist/media/video/processor/transcoder.d.ts +11 -1
  120. package/dist/method-spoofing.d.ts +7 -0
  121. package/dist/middleware.d.ts +60 -0
  122. package/dist/owned-routes.d.ts +42 -0
  123. package/dist/owned-routes.js +26 -0
  124. package/dist/page-meta.d.ts +38 -0
  125. package/dist/page-meta.js +16 -0
  126. package/dist/parser/directive-parser.d.ts +57 -45
  127. package/dist/parser/expression-parser.d.ts +33 -35
  128. package/dist/parser/index.d.ts +4 -39
  129. package/dist/parser/tokenizer.d.ts +61 -49
  130. package/dist/partial-hydration.d.ts +82 -50
  131. package/dist/performance-utils.d.ts +217 -124
  132. package/dist/placeholder.d.ts +18 -9
  133. package/dist/plugin-system.d.ts +264 -115
  134. package/dist/process.d.ts +11 -6
  135. package/dist/process.js +21 -4
  136. package/dist/production-builder.js +7 -0
  137. package/dist/prop-sentinels.d.ts +11 -11
  138. package/dist/public-env.d.ts +14 -0
  139. package/dist/pwa/directives.d.ts +12 -3
  140. package/dist/pwa/inject.d.ts +10 -0
  141. package/dist/pwa/precache.d.ts +22 -16
  142. package/dist/pwa/service-worker.d.ts +12 -0
  143. package/dist/pwa.js +1318 -610
  144. package/dist/reactive-bindings.d.ts +34 -11
  145. package/dist/reactive.d.ts +58 -30
  146. package/dist/reactive.js +56 -31
  147. package/dist/render.d.ts +0 -14
  148. package/dist/render.js +17 -0
  149. package/dist/route-middleware.d.ts +2 -35
  150. package/dist/route-types.d.ts +134 -0
  151. package/dist/route-types.js +24 -0
  152. package/dist/router.d.ts +5 -7
  153. package/dist/routes.d.ts +1 -3
  154. package/dist/runtime-globals.d.ts +147 -24
  155. package/dist/runtime-globals.js +16 -1
  156. package/dist/runtime-injection.d.ts +8 -0
  157. package/dist/runtime-injection.js +33 -24
  158. package/dist/script-emit.d.ts +30 -0
  159. package/dist/script-json.d.ts +36 -0
  160. package/dist/script-json.js +6 -0
  161. package/dist/script-validation.d.ts +21 -4
  162. package/dist/seo.d.ts +118 -47
  163. package/dist/server-bindings.d.ts +21 -0
  164. package/dist/sfc-template.d.ts +8 -8
  165. package/dist/signal-processing.d.ts +35 -15
  166. package/dist/signals-api.d.ts +117 -58
  167. package/dist/signals-api.js +7 -4
  168. package/dist/signals.d.ts +24 -17
  169. package/dist/signals.js +765 -60
  170. package/dist/site-builder/build.js +8 -0
  171. package/dist/site-url.d.ts +46 -0
  172. package/dist/site-url.js +70 -0
  173. package/dist/slots.d.ts +86 -20
  174. package/dist/source-signature.d.ts +45 -0
  175. package/dist/source-signature.js +20 -0
  176. package/dist/ssg.d.ts +285 -142
  177. package/dist/ssg.js +1268 -556
  178. package/dist/state-dir.d.ts +4 -4
  179. package/dist/state-management.d.ts +0 -12
  180. package/dist/state-management.js +8 -25
  181. package/dist/store-imports.d.ts +23 -0
  182. package/dist/store-imports.js +3 -0
  183. package/dist/store-loader.d.ts +10 -0
  184. package/dist/store-loader.js +10 -9
  185. package/dist/story/analytics.d.ts +138 -60
  186. package/dist/story/bookmarks.d.ts +87 -34
  187. package/dist/story/cli.d.ts +123 -69
  188. package/dist/story/collect/scanner.d.ts +4 -0
  189. package/dist/story/collect/tree.d.ts +8 -1
  190. package/dist/story/commands/build.d.ts +11 -6
  191. package/dist/story/commands/dev.d.ts +15 -8
  192. package/dist/story/commands/preview.d.ts +13 -7
  193. package/dist/story/config-watcher.d.ts +20 -13
  194. package/dist/story/config.d.ts +11 -8
  195. package/dist/story/context.d.ts +14 -8
  196. package/dist/story/hmr.d.ts +24 -26
  197. package/dist/story/hot-swap.d.ts +34 -23
  198. package/dist/story/keyboard-shortcuts.d.ts +23 -18
  199. package/dist/story/output.d.ts +46 -44
  200. package/dist/story/performance.d.ts +107 -45
  201. package/dist/story/presets.d.ts +107 -41
  202. package/dist/story/renderer.d.ts +41 -27
  203. package/dist/story/search.d.ts +23 -12
  204. package/dist/story/server.d.ts +21 -11
  205. package/dist/story/setup.d.ts +23 -15
  206. package/dist/story/testing.d.ts +65 -40
  207. package/dist/story/types.d.ts +262 -160
  208. package/dist/streaming.d.ts +119 -57
  209. package/dist/stx-virtual-ts.d.ts +165 -0
  210. package/dist/stx-virtual-ts.js +372 -0
  211. package/dist/stx.d.ts +78 -1
  212. package/dist/style-scoping.d.ts +17 -4
  213. package/dist/suspense.d.ts +67 -52
  214. package/dist/teleport.d.ts +22 -0
  215. package/dist/template-tag-balance.d.ts +22 -6
  216. package/dist/transitions.d.ts +66 -55
  217. package/dist/typecheck.d.ts +77 -0
  218. package/dist/typecheck.js +156 -0
  219. package/dist/types/component-types.d.ts +187 -122
  220. package/dist/types/config-types.d.ts +758 -297
  221. package/dist/types/context-types.d.ts +46 -24
  222. package/dist/types/csp-types.d.ts +70 -66
  223. package/dist/types/directive-types.d.ts +211 -155
  224. package/dist/types/index.d.ts +6 -109
  225. package/dist/types/pwa-types.d.ts +273 -148
  226. package/dist/types.d.ts +0 -1
  227. package/dist/typescript-templates.d.ts +11 -0
  228. package/dist/unresolved-identifiers.d.ts +24 -22
  229. package/dist/unresolved-identifiers.js +2 -0
  230. package/dist/utils.d.ts +24 -21
  231. package/dist/validator.d.ts +28 -26
  232. package/dist/variable-extractor.d.ts +48 -13
  233. package/dist/variable-extractor.js +27 -4
  234. package/dist/view-composers.d.ts +80 -81
  235. package/dist/virtual-scrolling.d.ts +101 -81
  236. package/dist/vue-template.d.ts +23 -0
  237. package/dist/web-components/css-scoping.d.ts +21 -11
  238. package/dist/web-components.d.ts +81 -58
  239. package/dist/x-element.d.ts +0 -1
  240. package/package.json +5 -5
  241. package/stx.d.ts +78 -1
package/dist/signals.js CHANGED
@@ -3,13 +3,13 @@ import { runtimeHandledXAttrsLiteral } from "./runtime-globals.js";
3
3
 
4
4
  export * from "./signals-api.js";
5
5
  export function generateSignalsRuntime() {
6
+ const stripped = stripConsoleLog(generateSignalsRuntimeDev());
6
7
  try {
7
- const stripped = stripConsoleLog(generateSignalsRuntimeDev());
8
8
  let minified = new Bun.Transpiler({ loader: "js", minifyWhitespace: !0 }).transformSync(stripped);
9
9
  minified = minified.replace(/\}(var |let |const |function )/g, "};$1");
10
10
  return minified;
11
11
  } catch {
12
- return generateSignalsRuntimeDev();
12
+ return stripped;
13
13
  }
14
14
  }
15
15
  function stripConsoleLog(src) {
@@ -107,15 +107,118 @@ console.log('[stx] entering IIFE');
107
107
  } catch (e) { return '[unserializable]'; }
108
108
  }
109
109
  // Wraps fetch with timing + a timeline record (the data layer's queries).
110
+ // Turn a non-ok Response into an Error, reading the body FIRST.
111
+ //
112
+ // Every data primitive threw a synthesized 'HTTP nnn: statusText' before
113
+ // touching the body, so whatever the server actually said \u2014 a validation
114
+ // map, a rate-limit reason, an auth message \u2014 was unreachable from any stx
115
+ // primitive, with no escape hatch (stacksjs/stx#1848). That is the single
116
+ // most cited reason an app keeps writing raw fetch() calls.
117
+ //
118
+ // The body is attached rather than only interpolated: err.data is what a
119
+ // caller needs to render field errors, and err.status is what it needs to
120
+ // tell 401 from 500 without parsing the message.
121
+ async function __stxHttpError(response) {
122
+ var body = null;
123
+ var text = '';
124
+ try { text = await response.text(); } catch (e) { text = ''; }
125
+ if (text) {
126
+ try { body = JSON.parse(text); } catch (e) { body = text; }
127
+ }
128
+ var detail = '';
129
+ if (body && typeof body === 'object') detail = body.message || body.error || '';
130
+ else if (typeof body === 'string') detail = body.slice(0, 200);
131
+ var message = 'HTTP ' + response.status;
132
+ if (response.statusText) message += ': ' + response.statusText;
133
+ if (detail) message += ' - ' + detail;
134
+ var err = new Error(message);
135
+ err.status = response.status;
136
+ err.statusText = response.statusText;
137
+ err.data = body;
138
+ err.response = response;
139
+ return err;
140
+ }
141
+
142
+ // One place an app can reach the data layer. Every stx data primitive goes
143
+ // through __stxFetch, the only fetch call in this runtime, so a single pair
144
+ // of hooks covers useFetch, useQuery and useMutation (#1855).
145
+ //
146
+ // Without it there is no way to attach an Authorization header or notice a
147
+ // 401 in one place, which is the reason an app keeps hand-written fetch
148
+ // calls sitting next to the primitives that were meant to replace them.
149
+ //
150
+ // Hooks mutate the context they are handed and their return value is
151
+ // ignored \u2014 the ofetch shape most people will already know. Throwing from a
152
+ // hook fails the request, which is what a token-refresh flow wants.
153
+ var __stxFetchHooks = { onRequest: null, onResponse: null, onResponseError: null };
154
+
155
+ function configureFetch(cfg) {
156
+ cfg = cfg || {};
157
+ // Replace rather than stack: a module re-evaluated by hot reload or by a
158
+ // SPA script re-run must not end up with the same interceptor installed
159
+ // twice, sending two Authorization headers or counting a 401 twice.
160
+ __stxFetchHooks.onRequest = typeof cfg.onRequest === 'function' ? cfg.onRequest : null;
161
+ __stxFetchHooks.onResponse = typeof cfg.onResponse === 'function' ? cfg.onResponse : null;
162
+ // Fires for a non-ok response and for a thrown fetch (network error). If it
163
+ // returns a Response, that Response replaces the original \u2014 this is how a
164
+ // 401 -> refresh -> retry is expressed once instead of per call site. The
165
+ // retry itself belongs to the hook: it should refresh the token and re-issue
166
+ // the request with a plain fetch() (not this data layer) so it does not
167
+ // re-enter the hook and loop. Returning nothing lets the error stand.
168
+ __stxFetchHooks.onResponseError = typeof cfg.onResponseError === 'function' ? cfg.onResponseError : null;
169
+ }
170
+
171
+ // headers may be a function, so it can be evaluated per request rather than
172
+ // captured once \u2014 a token that changes between calls. Object spread over a
173
+ // function copies no own enumerable properties, so passing one used to
174
+ // contribute nothing at all, in silence.
175
+ function __stxHeaders(h) {
176
+ return (typeof h === 'function' ? h() : h) || {};
177
+ }
178
+
110
179
  async function __stxFetch(source, url, opts) {
111
180
  var t0 = __stxDevtoolsNow();
181
+ opts = opts || {};
182
+ if (__stxFetchHooks.onRequest) {
183
+ // Normalised first so a hook can write ctx.options.headers.Authorization
184
+ // without having to create the bag itself.
185
+ opts.headers = __stxHeaders(opts.headers);
186
+ var reqCtx = { source: source, url: String(url), options: opts };
187
+ await __stxFetchHooks.onRequest(reqCtx);
188
+ // Reassignable, so a hook can prefix a baseURL or swap the options
189
+ // wholesale instead of only mutating them.
190
+ if (reqCtx.url) url = reqCtx.url;
191
+ opts = reqCtx.options || opts;
192
+ }
112
193
  var method = (opts && opts.method) || 'GET';
194
+ // A hook may hand back a Response to use in place of the original. Anything
195
+ // else (a truthy non-Response, undefined) leaves the outcome unchanged.
196
+ function __stxIsResponse(v) { return !!v && typeof v.ok === 'boolean'; }
113
197
  try {
114
198
  var r = await fetch(url, opts);
115
199
  __stxDevtoolsRecordQuery({ source: source, url: String(url), method: method, status: r.status, ok: r.ok, ms: __stxDevtoolsNow() - t0 });
200
+ if (__stxFetchHooks.onResponse) {
201
+ await __stxFetchHooks.onResponse({ source: source, url: String(url), options: opts, response: r });
202
+ }
203
+ if (!r.ok && __stxFetchHooks.onResponseError) {
204
+ var recovered = await __stxFetchHooks.onResponseError({ source: source, url: String(url), options: opts, response: r, error: null });
205
+ if (__stxIsResponse(recovered)) {
206
+ __stxDevtoolsRecordQuery({ source: source, url: String(url), method: method, status: recovered.status, ok: recovered.ok, retried: true, ms: __stxDevtoolsNow() - t0 });
207
+ return recovered;
208
+ }
209
+ }
116
210
  return r;
117
211
  } catch (e) {
118
212
  __stxDevtoolsRecordQuery({ source: source, url: String(url), method: method, status: 0, ok: false, error: String((e && e.message) || e), ms: __stxDevtoolsNow() - t0 });
213
+ // A network-level failure (no Response) also reaches the hook, so a retry
214
+ // can cover a dropped connection, not only an auth status.
215
+ if (__stxFetchHooks.onResponseError) {
216
+ var recoveredErr = await __stxFetchHooks.onResponseError({ source: source, url: String(url), options: opts, response: null, error: e });
217
+ if (__stxIsResponse(recoveredErr)) {
218
+ __stxDevtoolsRecordQuery({ source: source, url: String(url), method: method, status: recoveredErr.status, ok: recoveredErr.ok, retried: true, ms: __stxDevtoolsNow() - t0 });
219
+ return recoveredErr;
220
+ }
221
+ }
119
222
  throw e;
120
223
  }
121
224
  }
@@ -303,6 +406,40 @@ else {
303
406
  });
304
407
  }
305
408
 
409
+ // A proxy for the WRITE side of x-model, kept separate from the read proxy
410
+ // on purpose.
411
+ //
412
+ // The read proxy returns a plain object as-is, which is right for reading \u2014
413
+ // ordinary data should stay ordinary. But it means an assignment lands on the
414
+ // raw object, so a signal held there gets REPLACED by the typed value rather
415
+ // than set (#1883). Descending through a write proxy instead keeps set-trap
416
+ // semantics all the way down, so the signal at the end of any path is set.
417
+ function createModelWriteProxy(target) {
418
+ return new Proxy(target, {
419
+ get(t, prop) {
420
+ var val = t[prop];
421
+ if (val && typeof val === 'function' && (val._isSignal || val._isDerived)) {
422
+ // Descend INTO the signal's value: x-model="form.email" over
423
+ // state({email}) must reach the object, not stamp a property onto
424
+ // the signal function itself.
425
+ var inner = val();
426
+ return (inner && typeof inner === 'object') ? createModelWriteProxy(inner) : inner;
427
+ }
428
+ if (val && typeof val === 'object') return createModelWriteProxy(val);
429
+ return val;
430
+ },
431
+ set(t, prop, value) {
432
+ var cur = t[prop];
433
+ if (cur && typeof cur === 'function' && cur._isSignal && typeof cur.set === 'function') {
434
+ cur.set(value);
435
+ return true;
436
+ }
437
+ t[prop] = value;
438
+ return true;
439
+ },
440
+ });
441
+ }
442
+
306
443
  // ==========================================================================
307
444
  // Pipe Syntax Support (Feature #2)
308
445
  // ==========================================================================
@@ -607,7 +744,16 @@ finally {
607
744
  activeEffect = runEffect;
608
745
  effectStack.push(runEffect);
609
746
  try {
610
- cleanup = fn();
747
+ // Only a FUNCTION is a cleanup. An effect body is an expression as
748
+ // often as it is a block, and an expression has a value: an effect
749
+ // written as an arrow assigning to textContent returns the assigned
750
+ // string. Storing that and calling it on the next run threw
751
+ // "cleanup is not a function" from the line above \u2014 outside this try,
752
+ // so it escaped runEffect entirely and propagated out of whatever
753
+ // .set() triggered the notification, taking unrelated subscribers with
754
+ // it. Only ever on the second run, which is why it read as correct.
755
+ var __result = fn();
756
+ cleanup = typeof __result === 'function' ? __result : undefined;
611
757
  }
612
758
  catch (e) {
613
759
  // Auto-dispose effects that fail \u2014 prevents zombie subscriptions
@@ -745,6 +891,38 @@ finally {
745
891
  destroyCallbacks.push(fn);
746
892
  }
747
893
 
894
+ // Run mount callbacks and keep whatever teardown they hand back.
895
+ //
896
+ // A mount callback may return its own cleanup \u2014 the shape LifecycleCallback
897
+ // has always declared, and the shape every peer framework uses. Only the two
898
+ // component-factory paths honoured it; the other flush sites called fn() and
899
+ // dropped the result on the floor, so the teardown never ran and nothing said
900
+ // so. A listener or timer registered that way outlived its element and leaked
901
+ // once per re-render (#1857).
902
+ //
903
+ // The sink argument is where a returned cleanup is parked: a scoped flush
904
+ // passes that scope's __destroyCallbacks, a global flush passes the module
905
+ // queue. (No backticks in this comment \u2014 it lives inside a template literal.)
906
+ function runMountCallbacks(list, sink) {
907
+ if (!list || !list.length) return;
908
+ var target = sink || destroyCallbacks;
909
+ for (var i = 0; i < list.length; i++) {
910
+ try {
911
+ var cleanup = list[i]();
912
+ if (typeof cleanup === 'function') target.push(cleanup);
913
+ }
914
+ catch (e) { console.error('[stx] onMount error:', e); }
915
+ }
916
+ }
917
+
918
+ // The scope's own teardown queue, created on demand so a scope that never
919
+ // returns a cleanup does not carry an empty array around.
920
+ function scopeDestroySink(scopeVars) {
921
+ if (!scopeVars) return destroyCallbacks;
922
+ scopeVars.__destroyCallbacks = scopeVars.__destroyCallbacks || [];
923
+ return scopeVars.__destroyCallbacks;
924
+ }
925
+
748
926
  // Drain any early mount/destroy calls captured by the pre-initialization shim
749
927
  if (window.__stx_early_mounts) { window.__stx_early_mounts.forEach(function(fn) { mountCallbacks.push(fn); }); window.__stx_early_mounts = null; }
750
928
  if (window.__stx_early_destroys) { window.__stx_early_destroys.forEach(function(fn) { destroyCallbacks.push(fn); }); window.__stx_early_destroys = null; }
@@ -890,7 +1068,16 @@ finally {
890
1068
  const error = state(null);
891
1069
  if (options.suspense) registerSuspense(loading, error);
892
1070
 
1071
+ // A superseded or unmounted request must not resolve into a torn-down
1072
+ // scope. Each run gets its own AbortController; a newer run (refetch or a
1073
+ // reactive URL change) aborts the one before it, and onDestroy aborts the
1074
+ // last in-flight one. State writes are guarded on signal.aborted (#1871).
1075
+ let __stxAbort = null;
1076
+
893
1077
  const fetchData = async () => {
1078
+ if (__stxAbort) __stxAbort.abort();
1079
+ __stxAbort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
1080
+ const __signal = __stxAbort ? __stxAbort.signal : undefined;
894
1081
  loading.set(true);
895
1082
  error.set(null);
896
1083
 
@@ -905,8 +1092,9 @@ finally {
905
1092
  method: options.method || 'GET',
906
1093
  headers: {
907
1094
  'Content-Type': 'application/json',
908
- ...(options.headers || {})
909
- }
1095
+ ...__stxHeaders(options.headers)
1096
+ },
1097
+ signal: __signal
910
1098
  };
911
1099
 
912
1100
  if (options.body) {
@@ -916,18 +1104,21 @@ finally {
916
1104
  }
917
1105
 
918
1106
  const response = await __stxFetch('useFetch', url, fetchOptions);
1107
+ if (__signal && __signal.aborted) return;
919
1108
 
920
- if (!response.ok) {
921
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
922
- }
1109
+ if (!response.ok) throw await __stxHttpError(response);
923
1110
 
924
1111
  const result = await response.json();
1112
+ if (__signal && __signal.aborted) return;
925
1113
 
926
1114
  // Apply transform if provided
927
1115
  const transformed = options.transform ? options.transform(result) : result;
928
1116
  data.set(transformed);
929
1117
  }
930
1118
  catch (e) {
1119
+ // A superseded/unmounted request rejects with AbortError \u2014 expected,
1120
+ // not a failure to surface. The run that superseded it owns the state.
1121
+ if (e && e.name === 'AbortError') return;
931
1122
  console.error('[STX] useFetch error:', e);
932
1123
  // Store Error objects (not just messages) so consumers can use
933
1124
  // .message, .stack, instanceof Error. Matches the composable's
@@ -936,7 +1127,8 @@ catch (e) {
936
1127
  if (options.onError) options.onError(e);
937
1128
  }
938
1129
  finally {
939
- loading.set(false);
1130
+ // If a newer run (or onDestroy) aborted this one, that run owns loading.
1131
+ if (!__signal || !__signal.aborted) loading.set(false);
940
1132
  }
941
1133
  };
942
1134
 
@@ -962,6 +1154,10 @@ else if (immediate) {
962
1154
  onMount(fetchData);
963
1155
  }
964
1156
 
1157
+ // Abort an in-flight request when the owning scope is destroyed, so its
1158
+ // resolution can't write into a torn-down component (#1871).
1159
+ if (typeof onDestroy === 'function') onDestroy(function() { if (__stxAbort) __stxAbort.abort(); });
1160
+
965
1161
  return {
966
1162
  data,
967
1163
  loading,
@@ -1015,6 +1211,33 @@ else if (immediate) {
1015
1211
  else window.location.href = url;
1016
1212
  }
1017
1213
 
1214
+ // Re-run the CURRENT route against the server and swap the result, keeping
1215
+ // every client signal on the page alive.
1216
+ //
1217
+ // The router grew refresh()/invalidate() for this, but they lived only on
1218
+ // window.stxRouter \u2014 nothing in the authoring surface reached them, so a
1219
+ // <script client> block still had no way to say "the mutation changed what
1220
+ // the server rendered". Apps kept calling location.reload() and threw away
1221
+ // the state the SPA exists to preserve. See #1850.
1222
+ //
1223
+ // Degrades to a real reload when the router is absent (a page that never
1224
+ // booted it, or a mid-boot call), mirroring how navigate() falls through to
1225
+ // window.location.
1226
+ function refresh() {
1227
+ if (window.stxRouter && typeof window.stxRouter.refresh === 'function')
1228
+ return window.stxRouter.refresh();
1229
+ window.location.reload();
1230
+ return Promise.resolve(false);
1231
+ }
1232
+
1233
+ // Expire a cached route so the NEXT visit re-fetches it, without navigating
1234
+ // now. Pairs with refresh(): refresh the page you are on, invalidate the ones
1235
+ // your mutation also changed.
1236
+ function invalidateRoute(url) {
1237
+ if (window.stxRouter && typeof window.stxRouter.invalidate === 'function')
1238
+ window.stxRouter.invalidate(url);
1239
+ }
1240
+
1018
1241
  function goBack() { window.history.back(); }
1019
1242
  function goForward() { window.history.forward(); }
1020
1243
 
@@ -1060,6 +1283,20 @@ else if (immediate) {
1060
1283
  };
1061
1284
  }
1062
1285
 
1286
+ // Exported from src/runtime.ts, whose whole stated purpose is client-facing
1287
+ // APIs that hide window internals \u2014 but never published on window.stx, so a
1288
+ // client script importing them destructured undefined and threw on use. The
1289
+ // capability was always here behind useRoute().params; only the documented
1290
+ // names were unreachable (stacksjs/stx#1843).
1291
+ function useRouteParams() {
1292
+ return _routeParams();
1293
+ }
1294
+
1295
+ function useRouteParam(name, fallback) {
1296
+ var value = _routeParams()[name];
1297
+ return value === undefined ? fallback : value;
1298
+ }
1299
+
1063
1300
  function useSearchParams() {
1064
1301
  var params = state(Object.fromEntries(new URLSearchParams(window.location.search)));
1065
1302
  var syncFromUrl = function() {
@@ -1136,6 +1373,13 @@ else if (immediate) {
1136
1373
  // ==========================================================================
1137
1374
 
1138
1375
  var _queryCache = {};
1376
+ // Requests currently in flight, keyed exactly like _queryCache. _queryCache
1377
+ // is written only after the body parses, so the whole in-flight window was a
1378
+ // cache miss and N components mounting together issued N identical requests
1379
+ // (#1869). Refcounted: a caller that loses interest must not cancel a
1380
+ // response another caller is still waiting on.
1381
+ var _queryInflight = {};
1382
+ var __stxQueryInstance = 0;
1139
1383
 
1140
1384
  // Suspense registry (#1742). A query created with { suspense: true } pushes
1141
1385
  // { el, loading, error } here at creation time (which runs during partial-scope
@@ -1165,6 +1409,20 @@ else if (immediate) {
1165
1409
  var isStale = state(false);
1166
1410
  if (options.suspense) registerSuspense(loading, error);
1167
1411
 
1412
+ // Per-run AbortController: a newer run aborts the previous in-flight one and
1413
+ // onDestroy aborts the last, so a resolved request can't write into a
1414
+ // torn-down scope (#1871).
1415
+ var __stxAbort = null;
1416
+ // Identity, so dedup can tell "another component wants this key" from
1417
+ // "this component asked again". They must behave differently: joining is
1418
+ // right for the first, and wrong for the second \u2014 refetch() after a
1419
+ // mutation has to hit the network, not adopt the pre-mutation request that
1420
+ // happens to still be open (#1869).
1421
+ var __instanceId = ++__stxQueryInstance;
1422
+ // The shared entry this instance is currently attached to, so superseding
1423
+ // can release its claim without cancelling a request someone else joined.
1424
+ var __joined = null;
1425
+
1168
1426
  var fetchData = async function() {
1169
1427
  var resolvedUrl = typeof url === 'function' ? url() : url;
1170
1428
  if (!resolvedUrl) { loading.set(false); return; }
@@ -1186,13 +1444,69 @@ else if (immediate) {
1186
1444
  isStale.set(true);
1187
1445
  }
1188
1446
 
1447
+ // Join a request ANOTHER instance already has open for this key. Not one
1448
+ // of our own: this instance calling refetch() means "I want fresh data",
1449
+ // and adopting its own pre-mutation request would silently return stale
1450
+ // data \u2014 so an own-instance run still supersedes, exactly as #1871 says.
1451
+ var shared = _queryInflight[key];
1452
+ if (shared && shared.owner !== __instanceId) {
1453
+ shared.refs++;
1454
+ __joined = shared;
1455
+ loading.set(true);
1456
+ error.set(null);
1457
+ try {
1458
+ var joinedResult = await shared.promise;
1459
+ var joinedTransformed = options.transform ? options.transform(joinedResult) : joinedResult;
1460
+ data.set(joinedTransformed);
1461
+ isStale.set(false);
1462
+ if (options.onSuccess) options.onSuccess(joinedTransformed);
1463
+ }
1464
+ catch (e) {
1465
+ if (e && e.name === 'AbortError') return;
1466
+ error.set(e.message || 'Query failed');
1467
+ if (options.onError) options.onError(e);
1468
+ }
1469
+ finally {
1470
+ shared.refs--;
1471
+ if (__joined === shared) __joined = null;
1472
+ loading.set(false);
1473
+ }
1474
+ return;
1475
+ }
1476
+
1477
+ // Superseding this instance's own run. Abort ONLY if nobody joined it \u2014
1478
+ // cancelling a request another component is waiting on would leave it
1479
+ // loading forever, which would turn #1871's fix into a worse bug.
1480
+ if (__stxAbort) {
1481
+ if (__joined && __joined.refs > 1) __joined.refs--;
1482
+ else __stxAbort.abort();
1483
+ }
1484
+ __joined = null;
1485
+ __stxAbort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
1486
+ var __signal = __stxAbort ? __stxAbort.signal : undefined;
1189
1487
  loading.set(true);
1190
1488
  error.set(null);
1191
1489
  try {
1192
- var fetchOpts = { method: 'GET', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } };
1193
- var response = await __stxFetch('useQuery', resolvedUrl, fetchOpts);
1194
- if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText);
1195
- var result = await response.json();
1490
+ var fetchOpts = { method: 'GET', headers: { 'Content-Type': 'application/json', ...__stxHeaders(options.headers) }, signal: __signal };
1491
+ // Published BEFORE it is awaited, so a caller starting in the same tick
1492
+ // finds it. The network part only \u2014 transform, cache write and
1493
+ // onSuccess stay per-caller, since each may transform differently.
1494
+ var netPromise = (async function() {
1495
+ var response = await __stxFetch('useQuery', resolvedUrl, fetchOpts);
1496
+ if (!response.ok) throw await __stxHttpError(response);
1497
+ return await response.json();
1498
+ })();
1499
+ var entry = { owner: __instanceId, refs: 1, controller: __stxAbort, promise: netPromise };
1500
+ _queryInflight[key] = entry;
1501
+ __joined = entry;
1502
+ // Cleared on settle, both ways. A settled entry left behind would turn
1503
+ // dedup into a permanent cache and no request for this key would ever
1504
+ // be issued again.
1505
+ var releaseEntry = function() { if (_queryInflight[key] === entry) delete _queryInflight[key]; };
1506
+ netPromise.then(releaseEntry, releaseEntry);
1507
+
1508
+ var result = await netPromise;
1509
+ if (__signal && __signal.aborted) return;
1196
1510
  var transformed = options.transform ? options.transform(result) : result;
1197
1511
  data.set(transformed);
1198
1512
  isStale.set(false);
@@ -1202,11 +1516,14 @@ else if (immediate) {
1202
1516
  setTimeout(function() { delete _queryCache[key]; }, cacheTime);
1203
1517
  }
1204
1518
  catch (e) {
1519
+ // A superseded/unmounted request rejects with AbortError \u2014 expected;
1520
+ // the run that superseded it owns the state (#1871).
1521
+ if (e && e.name === 'AbortError') return;
1205
1522
  error.set(e.message || 'Query failed');
1206
1523
  if (options.onError) options.onError(e);
1207
1524
  }
1208
1525
  finally {
1209
- loading.set(false);
1526
+ if (!__signal || !__signal.aborted) loading.set(false);
1210
1527
  }
1211
1528
  };
1212
1529
 
@@ -1226,12 +1543,25 @@ finally {
1226
1543
  onDestroy(function() { document.removeEventListener('visibilitychange', onVisibility); });
1227
1544
  }
1228
1545
 
1229
- // refetchInterval
1546
+ // refetchInterval: poll only while the tab is visible and (if given)
1547
+ // enabled holds; optionally stop after an error so a broken endpoint is
1548
+ // not hammered every interval in a backgrounded tab (#1870).
1230
1549
  if (options.refetchInterval) {
1231
- var intervalId = setInterval(fetchData, options.refetchInterval);
1550
+ var intervalId = setInterval(function() {
1551
+ if (typeof document !== 'undefined' && document.hidden) return;
1552
+ var en = options.enabled;
1553
+ if (typeof en === 'function' ? !en() : en === false) return;
1554
+ var p = fetchData();
1555
+ if (options.stopOnError && p && typeof p.then === 'function') {
1556
+ p.then(function() { if (error()) clearInterval(intervalId); });
1557
+ }
1558
+ }, options.refetchInterval);
1232
1559
  onDestroy(function() { clearInterval(intervalId); });
1233
1560
  }
1234
1561
 
1562
+ // Abort an in-flight request when the owning scope is destroyed (#1871).
1563
+ onDestroy(function() { if (__stxAbort) __stxAbort.abort(); });
1564
+
1235
1565
  return {
1236
1566
  data: data,
1237
1567
  loading: loading,
@@ -1267,11 +1597,11 @@ finally {
1267
1597
  var resolvedUrl = typeof url === 'function' ? url() : url;
1268
1598
  var fetchOpts = {
1269
1599
  method: options.method || 'POST',
1270
- headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
1600
+ headers: { 'Content-Type': 'application/json', ...__stxHeaders(options.headers) },
1271
1601
  body: typeof body === 'string' ? body : JSON.stringify(body)
1272
1602
  };
1273
1603
  var response = await __stxFetch('useMutation', resolvedUrl, fetchOpts);
1274
- if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText);
1604
+ if (!response.ok) throw await __stxHttpError(response);
1275
1605
  var result = await response.json();
1276
1606
  var transformed = options.transform ? options.transform(result) : result;
1277
1607
  data.set(transformed);
@@ -1611,7 +1941,7 @@ catch (e) {
1611
1941
  var sv = sid && window.stx._scopes ? window.stx._scopes[sid] : null;
1612
1942
  if (sv && sv.__mountCallbacks && !sv.__mounted) {
1613
1943
  sv.__mounted = true;
1614
- sv.__mountCallbacks.forEach(function(fn) { try { fn(); } catch (e) { console.error('[stx] onMount error:', e); } });
1944
+ runMountCallbacks(sv.__mountCallbacks, scopeDestroySink(sv));
1615
1945
  }
1616
1946
  window.dispatchEvent(new CustomEvent('stx:hydrated', { detail: { el: el, trigger: trigger } }));
1617
1947
  };
@@ -2697,8 +3027,22 @@ catch (e) {
2697
3027
  capturedScope[expr].set(val);
2698
3028
  }
2699
3029
  else {
2700
- const fn = new Function(...Object.keys(capturedScope), 'v', expr + ' = v');
2701
- fn(...Object.values(capturedScope), val);
3030
+ // Write through the SAME auto-unwrap proxy the read path uses.
3031
+ //
3032
+ // This used to destructure the scope into plain parameters, so an
3033
+ // lvalue like store.title assigned onto the raw object and REPLACED
3034
+ // the signal with a raw string. Everything else bound to that signal
3035
+ // stopped updating, its .set was gone so the next write threw
3036
+ // TypeError, and nothing warned \u2014 the field the user typed into was
3037
+ // the only thing on the page that still looked right (#1883).
3038
+ //
3039
+ // The proxy's set trap already did the correct thing (call .set on a
3040
+ // signal-valued property) and was simply never reached from here.
3041
+ // Routing through it also drops the old requirement that every scope
3042
+ // key be a valid identifier, since nothing is destructured now.
3043
+ var writeScope = createModelWriteProxy(capturedScope);
3044
+ var writeFn = new Function('__scope__', '__v__', 'with(__scope__) { ' + expr + ' = __v__ }');
3045
+ writeFn(writeScope, val);
2702
3046
  }
2703
3047
  }
2704
3048
  catch (e) {
@@ -3178,10 +3522,7 @@ else if (typeof value === 'string') {
3178
3522
  });
3179
3523
  if (scopeVars.__mountCallbacks && !scopeVars.__mounted) {
3180
3524
  scopeVars.__mounted = true;
3181
- scopeVars.__mountCallbacks.forEach(function(fn) {
3182
- try { fn(); }
3183
- catch (e) { console.error('[stx] onMount error:', e); }
3184
- });
3525
+ runMountCallbacks(scopeVars.__mountCallbacks, scopeDestroySink(scopeVars));
3185
3526
  }
3186
3527
  });
3187
3528
  }
@@ -3631,6 +3972,27 @@ catch (e) {
3631
3972
  const newGroups = [];
3632
3973
  const usedKeys = new Set();
3633
3974
 
3975
+ // Minimal-move reconciliation.
3976
+ //
3977
+ // Every reused row used to be round-tripped through a DocumentFragment,
3978
+ // and appendChild into a fragment DETACHES the node from the document
3979
+ // first. Removing a focused node blurs it and resets activeElement, so a
3980
+ // list could not contain a field the user was typing in: any write to the
3981
+ // backing array kicked them out of it, losing caret, selection and undo
3982
+ // history. It happened for every row unconditionally, including rows that
3983
+ // had not moved, and unkeyed loops took the same path because getItemKey
3984
+ // falls back to the index (#1882).
3985
+ //
3986
+ // The cursor walks the existing rendering in order. A node already at the
3987
+ // cursor is left ENTIRELY alone \u2014 an unchanged list now performs zero DOM
3988
+ // operations \u2014 and a node that genuinely has to move is moved with
3989
+ // insertBefore, which relocates it without a detach/reattach cycle.
3990
+ let cursor = null;
3991
+ for (let ci = 0; ci < currentGroups.length && !cursor; ci++) {
3992
+ const seed = liveGroupNodes(currentGroups[ci] || []);
3993
+ if (seed.length) cursor = seed[0];
3994
+ }
3995
+
3634
3996
  for (let i = 0; i < list.length; i++) {
3635
3997
  const key = newKeys[i];
3636
3998
  const existing = oldKeyMap.get(key);
@@ -3643,9 +4005,20 @@ catch (e) {
3643
4005
  // must not resurrect that hidden node.
3644
4006
  const group = existing.shift();
3645
4007
  const liveNodes = liveGroupNodes(group);
3646
- const fragment = document.createDocumentFragment();
3647
- liveNodes.forEach(groupNode => fragment.appendChild(groupNode));
3648
- parent.insertBefore(fragment, placeholder);
4008
+ for (let li = 0; li < liveNodes.length; li++) {
4009
+ const groupNode = liveNodes[li];
4010
+ if (groupNode === cursor) {
4011
+ // Already exactly where it belongs. Touching it here is what
4012
+ // used to blur it, so the correct action is to do nothing.
4013
+ cursor = cursor.nextSibling;
4014
+ }
4015
+ else {
4016
+ // Moves without detaching. The cursor does not advance: this node
4017
+ // is now behind it, and the cursor still marks the next position
4018
+ // to fill.
4019
+ parent.insertBefore(groupNode, cursor || placeholder);
4020
+ }
4021
+ }
3649
4022
  newElements.push(...group);
3650
4023
  newGroups.push(group);
3651
4024
  usedKeys.add(key);
@@ -3661,7 +4034,10 @@ catch (e) {
3661
4034
  // New item \u2014 create DOM elements with a fresh item signal
3662
4035
  const elements = createItemElements(list[i], i, key);
3663
4036
  elements.forEach(el => {
3664
- parent.insertBefore(el, placeholder);
4037
+ // Before the cursor, not before the placeholder: a new row inserted
4038
+ // in the MIDDLE of the list must land at its own position, without
4039
+ // forcing every row after it to be moved to get back in order.
4040
+ parent.insertBefore(el, cursor || placeholder);
3665
4041
  if (tgReady && el.isConnected) tgEnter(el, tgName);
3666
4042
  });
3667
4043
  hydrateComponentScopes(elements, elements.find(function(node) {
@@ -4307,15 +4683,23 @@ else if (timer === null) {
4307
4683
  return throttled;
4308
4684
  }
4309
4685
 
4310
- function useInterval(interval, options) {
4686
+ function useInterval(interval, options, thirdOptions) {
4311
4687
  // Accept the callback-first form too \u2014 useInterval(fn, 1000) is what most
4312
4688
  // callers reach for, and silently ignoring the callback (the previous
4313
- // behaviour) produced a timer that ticked into the void.
4689
+ // behaviour) produced a timer that ticked into the void. An options object
4690
+ // may follow the interval \u2014 useInterval(fn, 1000, { enabled, whileVisible })
4691
+ // \u2014 or replace it \u2014 useInterval(fn, { enabled }) (#1870).
4314
4692
  var callback = null;
4315
4693
  if (typeof interval === 'function') {
4316
4694
  callback = interval;
4317
- interval = typeof options === 'number' ? options : 1000;
4318
- options = {};
4695
+ if (typeof options === 'number') {
4696
+ interval = options;
4697
+ options = thirdOptions || {};
4698
+ }
4699
+ else {
4700
+ interval = 1000;
4701
+ options = (options && typeof options === 'object') ? options : {};
4702
+ }
4319
4703
  }
4320
4704
  interval = interval || 1000;
4321
4705
  options = options || {};
@@ -4323,7 +4707,18 @@ else if (timer === null) {
4323
4707
  var id = null;
4324
4708
  var running = false;
4325
4709
  var listeners = [];
4710
+ // A tick is skipped (not counted, listeners not called) while the timer is
4711
+ // gated off: enabled is false / returns false, or whileVisible is set
4712
+ // and the document is hidden. This makes "run only while authorized and
4713
+ // visible" expressible without the caller hand-pausing the timer (#1870).
4714
+ function shouldTick() {
4715
+ if (options.whileVisible && typeof document !== 'undefined' && document.hidden) return false;
4716
+ var en = options.enabled;
4717
+ if (typeof en === 'function') return !!en();
4718
+ return en !== false;
4719
+ }
4326
4720
  function tick() {
4721
+ if (!shouldTick()) return;
4327
4722
  count++;
4328
4723
  listeners.forEach(function(fn) { fn(count); });
4329
4724
  }
@@ -4786,7 +5181,68 @@ catch (e) {} }
4786
5181
 
4787
5182
  // Vue-compat aliases
4788
5183
  var ref = state;
4789
- var reactive = state;
5184
+
5185
+ // reactive() used to be a bare alias of state(), which made it the most
5186
+ // dangerous name in the runtime. state({n:0}) returns a SIGNAL, so the Vue
5187
+ // shape everyone reaches for silently did nothing:
5188
+ //
5189
+ // const s = reactive({ count: 0 })
5190
+ // s.count // undefined \u2014 a signal has no such property
5191
+ // s.count++ // writes an expando onto a function object
5192
+ // {{ s.count }} // renders empty, forever, with no warning
5193
+ //
5194
+ // Backed by one signal PER PROPERTY, created on first touch. That is what
5195
+ // makes a read inside an effect track only the field it read, so writing
5196
+ // b does not re-run an effect that only read a (#1885).
5197
+ var __stxReactiveProxies = new WeakMap();
5198
+
5199
+ function reactive(target) {
5200
+ // Primitives have no properties to track. Returning them unchanged keeps
5201
+ // reactive(0) from silently becoming an object.
5202
+ if (!target || typeof target !== 'object') return target;
5203
+ var existing = __stxReactiveProxies.get(target);
5204
+ if (existing) return existing;
5205
+
5206
+ var propSignals = Object.create(null);
5207
+ function signalFor(prop) {
5208
+ if (!propSignals[prop]) propSignals[prop] = state(target[prop]);
5209
+ return propSignals[prop];
5210
+ }
5211
+
5212
+ var proxy = new Proxy(target, {
5213
+ get: function(t, prop, receiver) {
5214
+ // Symbols and inherited members are plumbing \u2014 toJSON, Symbol.iterator,
5215
+ // array methods. Tracking them would create a signal per method lookup
5216
+ // and make every effect depend on the whole object.
5217
+ if (typeof prop === 'symbol' || !Object.prototype.hasOwnProperty.call(t, prop))
5218
+ return Reflect.get(t, prop, receiver);
5219
+ var value = signalFor(prop)();
5220
+ // Nested objects are wrapped too, and cached in the WeakMap, so
5221
+ // identity is stable across reads \u2014 a fresh proxy each time would
5222
+ // break === comparisons and defeat keyed list reuse.
5223
+ return (value && typeof value === 'object') ? reactive(value) : value;
5224
+ },
5225
+ set: function(t, prop, value) {
5226
+ if (typeof prop === 'symbol') return Reflect.set(t, prop, value);
5227
+ var had = Object.prototype.hasOwnProperty.call(t, prop);
5228
+ t[prop] = value;
5229
+ // Write the plain target FIRST so a signal created later reads the
5230
+ // current value, then notify anyone already watching this property.
5231
+ if (propSignals[prop]) propSignals[prop].set(value);
5232
+ else if (had) signalFor(prop);
5233
+ return true;
5234
+ },
5235
+ deleteProperty: function(t, prop) {
5236
+ var had = Object.prototype.hasOwnProperty.call(t, prop);
5237
+ delete t[prop];
5238
+ if (had && propSignals[prop]) propSignals[prop].set(undefined);
5239
+ return true;
5240
+ },
5241
+ });
5242
+
5243
+ __stxReactiveProxies.set(target, proxy);
5244
+ return proxy;
5245
+ }
4790
5246
  var computed = derived;
4791
5247
  var watch = $watch;
4792
5248
  var watchEffect = function(fn) { return effect(fn); };
@@ -5321,7 +5777,22 @@ catch (e) {} }
5321
5777
  var confirmText = opts.confirmText || 'OK';
5322
5778
  var cancelText = opts.cancelText || 'Cancel';
5323
5779
  var id = 'stx-dialog-' + (++_dialogId);
5324
- var isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
5780
+ // Resolve dark from what the APP decided, not only from the OS. stx's own
5781
+ // color-mode boot writes a class (default 'dark') and/or an attribute
5782
+ // (e.g. data-theme) onto <html>, so an app pinned to light on a dark OS used
5783
+ // to get a dark dialog over a light page. An explicit opts.dark wins over
5784
+ // both, and the media query stays as the last resort. See #1875.
5785
+ var _root = document.documentElement;
5786
+ var _attrTheme = _root.getAttribute('data-theme') || _root.getAttribute('data-color-mode');
5787
+ var isDark = opts.dark != null
5788
+ ? !!opts.dark
5789
+ : _root.classList.contains('dark')
5790
+ ? true
5791
+ : _attrTheme === 'dark'
5792
+ ? true
5793
+ : _attrTheme === 'light'
5794
+ ? false
5795
+ : !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
5325
5796
  var bg = isDark ? '#1f2937' : '#ffffff';
5326
5797
  var textColor = isDark ? '#f3f4f6' : '#1f2937';
5327
5798
  var subColor = isDark ? '#9ca3af' : '#6b7280';
@@ -5344,23 +5815,45 @@ catch (e) {} }
5344
5815
 
5345
5816
  if (title) {
5346
5817
  var titleEl = document.createElement('h3');
5818
+ titleEl.id = id + '-title';
5347
5819
  titleEl.style.cssText = 'margin:0 0 0.5rem;font-size:1.125rem;font-weight:600';
5348
5820
  titleEl.textContent = title;
5349
5821
  panel.appendChild(titleEl);
5822
+ // role=alertdialog was announced with no accessible name at all.
5823
+ backdrop.setAttribute('aria-labelledby', titleEl.id);
5350
5824
  }
5351
5825
 
5352
5826
  var msgEl = document.createElement('p');
5827
+ msgEl.id = id + '-message';
5353
5828
  msgEl.style.cssText = 'margin:0 0 1.25rem;font-size:0.875rem;line-height:1.5;color:' + subColor;
5354
5829
  msgEl.textContent = message;
5355
5830
  panel.appendChild(msgEl);
5831
+ backdrop.setAttribute('aria-describedby', msgEl.id);
5832
+ if (!title)
5833
+ backdrop.setAttribute('aria-label', message);
5356
5834
 
5357
5835
  var btnRow = document.createElement('div');
5358
5836
  btnRow.style.cssText = 'display:flex;gap:0.75rem;justify-content:center';
5359
5837
 
5838
+ // Captured before the dialog steals focus, so it can be handed back on
5839
+ // close. Without this a keyboard or screen-reader user is dropped at the
5840
+ // top of the document every time a dialog closes.
5841
+ var previouslyFocused = document.activeElement;
5842
+ var closed = false;
5843
+
5360
5844
  function cleanup(result) {
5845
+ // Guarded: the escape handler used to be able to fire after a button had
5846
+ // already closed the dialog, because the listener outlived it.
5847
+ if (closed) return;
5848
+ closed = true;
5849
+ document.removeEventListener('keydown', keyHandler, true);
5361
5850
  backdrop.style.opacity = '0';
5362
5851
  panel.style.transform = 'scale(0.95)';
5363
5852
  setTimeout(function() { backdrop.remove(); }, 200);
5853
+ if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
5854
+ try { previouslyFocused.focus(); }
5855
+ catch (_e) { /* element may be gone; not worth failing the dialog over */ }
5856
+ }
5364
5857
  resolve(result);
5365
5858
  }
5366
5859
 
@@ -5392,11 +5885,45 @@ catch (e) {} }
5392
5885
  backdrop.style.opacity = '1';
5393
5886
  panel.style.transform = 'scale(1)';
5394
5887
 
5395
- // Escape key
5396
- var escHandler = function(e) {
5397
- if (e.key === 'Escape') { cleanup(isConfirm ? false : undefined); document.removeEventListener('keydown', escHandler); }
5888
+ // Escape, plus a real focus trap.
5889
+ //
5890
+ // The old handler removed itself ONLY inside the Escape branch, so every
5891
+ // dialog dismissed with a button left a permanent keydown listener on
5892
+ // document holding this whole closure \u2014 backdrop, panel and resolve \u2014 alive.
5893
+ // Removal now lives in cleanup(), which every exit path goes through.
5894
+ //
5895
+ // aria-modal="true" was already being claimed on the backdrop while Tab
5896
+ // walked straight out into the page behind it. Capture phase so the trap
5897
+ // wins over anything the page has bound.
5898
+ var keyHandler = function(e) {
5899
+ if (e.key === 'Escape') {
5900
+ e.preventDefault();
5901
+ cleanup(isConfirm ? false : undefined);
5902
+ return;
5903
+ }
5904
+ if (e.key !== 'Tab') return;
5905
+ var focusable = panel.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
5906
+ if (!focusable.length) return;
5907
+ var first = focusable[0];
5908
+ var last = focusable[focusable.length - 1];
5909
+ // Focus may sit outside the panel entirely (the page had it when the
5910
+ // dialog opened), in which case Tab must pull it back in rather than
5911
+ // continue through the document.
5912
+ if (!panel.contains(document.activeElement)) {
5913
+ e.preventDefault();
5914
+ (e.shiftKey ? last : first).focus();
5915
+ return;
5916
+ }
5917
+ if (e.shiftKey && document.activeElement === first) {
5918
+ e.preventDefault();
5919
+ last.focus();
5920
+ }
5921
+ else if (!e.shiftKey && document.activeElement === last) {
5922
+ e.preventDefault();
5923
+ first.focus();
5924
+ }
5398
5925
  };
5399
- document.addEventListener('keydown', escHandler);
5926
+ document.addEventListener('keydown', keyHandler, true);
5400
5927
 
5401
5928
  // Focus the primary button
5402
5929
  okBtn.focus();
@@ -5526,7 +6053,7 @@ catch (e) {} }
5526
6053
  el.__stx_disposers = trackEffects(function() { processElement(el, componentScope); });
5527
6054
  if (scopeVars.__mountCallbacks && !scopeVars.__mounted) {
5528
6055
  scopeVars.__mounted = true;
5529
- scopeVars.__mountCallbacks.forEach(function(fn) { try { fn(); } catch (e) { console.error('[stx] onMount error:', e); } });
6056
+ runMountCallbacks(scopeVars.__mountCallbacks, scopeDestroySink(scopeVars));
5530
6057
  }
5531
6058
  });
5532
6059
  // 3. Bind directives across the subtree.
@@ -5537,7 +6064,7 @@ catch (e) {} }
5537
6064
  if (mountCallbacks.length) {
5538
6065
  var cbs = mountCallbacks.slice();
5539
6066
  mountCallbacks.length = 0;
5540
- cbs.forEach(function(fn) { try { fn(); } catch (e) { console.error('[stx] mount error:', e); } });
6067
+ runMountCallbacks(cbs, destroyCallbacks);
5541
6068
  }
5542
6069
  window.dispatchEvent(new CustomEvent('stx:hydrated', { detail: { el: container, trigger: 'stream' } }));
5543
6070
  }
@@ -5571,13 +6098,18 @@ catch (e) {} }
5571
6098
  useFetch,
5572
6099
  useRef,
5573
6100
  navigate,
6101
+ refresh,
6102
+ invalidateRoute,
5574
6103
  goBack,
5575
6104
  goForward,
5576
6105
  useRoute,
6106
+ useRouteParams,
6107
+ useRouteParam,
5577
6108
  setRouteParams,
5578
6109
  useSearchParams,
5579
6110
  useQuery,
5580
6111
  useMutation,
6112
+ configureFetch,
5581
6113
  useOptimistic,
5582
6114
  provide,
5583
6115
  $computed,
@@ -5701,8 +6233,11 @@ catch (e) {} }
5701
6233
 
5702
6234
  defineStore: function(id, setupOrOptions, storeOptions) {
5703
6235
  console.log('[stx:store] defineStore called:', id, 'type:', typeof setupOrOptions === 'function' ? 'setup' : 'options', 'persist:', !!(storeOptions && storeOptions.persist));
5704
- // Return existing store if already defined
5705
- if (window.stx._stores.has(id)) {
6236
+ // Return existing store if already defined.
6237
+ //
6238
+ // During a store HMR pass the definition is deliberately re-run, so the
6239
+ // cached instance must NOT short-circuit it (#1877 ask 4).
6240
+ if (window.stx._stores.has(id) && !window.stx.__hmrStoreReplacing) {
5706
6241
  console.log('[stx:store] returning existing store:', id);
5707
6242
  return window.stx._stores.get(id);
5708
6243
  }
@@ -5842,18 +6377,72 @@ catch (e) {} }
5842
6377
  if (persistCfg) {
5843
6378
  var pOpts = persistCfg === true ? {} : persistCfg;
5844
6379
  var storageKey = pOpts.key || ('stx-store-' + id);
5845
- var storageType = pOpts.storage === 'sessionStorage' ? sessionStorage : localStorage;
5846
6380
  var pick = pOpts.pick || null;
5847
6381
 
6382
+ // Three additions, all of which apps previously had to hand-roll outside
6383
+ // the store entirely (#1872):
6384
+ //
6385
+ // keys map a field to its OWN storage key, and optionally its own
6386
+ // backend, instead of folding everything into one JSON blob
6387
+ // serialize / deserialize \u2014 the codec was hardcoded to JSON, so a
6388
+ // persisted string came back quoted. A signed-out token
6389
+ // stored as '""' is two characters and TRUTHY, which is a
6390
+ // real class of auth bug, and a legacy raw value could not
6391
+ // be read at all
6392
+ // cookie a backend the SERVER can read. Server-rendered,
6393
+ // owner-scoped pages authenticate from a cookie and cannot
6394
+ // see localStorage, so this previously meant a second
6395
+ // persisted signal plus a hand-written mirroring effect
6396
+ var fieldMap = pOpts.keys || null;
6397
+ var defaultSerialize = pOpts.serialize || function(value) { return JSON.stringify(value); };
6398
+ var defaultDeserialize = pOpts.deserialize || function(raw) { return JSON.parse(raw); };
6399
+
6400
+ // 'local' and 'localStorage' both accepted; the old code only recognised
6401
+ // 'sessionStorage', so 'session' silently fell through to localStorage.
6402
+ // Resolved through window when the bare global is absent, so persist also
6403
+ // works where the runtime is evaluated outside a page's global scope
6404
+ // (component test harnesses, embedded runtimes).
6405
+ function _backendFor(kind) {
6406
+ if (kind === 'cookie') return 'cookie';
6407
+ var scope = typeof window !== 'undefined' ? window : null;
6408
+ if (kind === 'session' || kind === 'sessionStorage')
6409
+ return typeof sessionStorage !== 'undefined' ? sessionStorage : (scope && scope.sessionStorage);
6410
+ return typeof localStorage !== 'undefined' ? localStorage : (scope && scope.localStorage);
6411
+ }
6412
+ var storageType = _backendFor(pOpts.storage);
6413
+
6414
+ // Resolved config for a field that has its own key, or null if it belongs
6415
+ // in the shared blob.
6416
+ function _mappedConfig(field) {
6417
+ if (!fieldMap || !Object.prototype.hasOwnProperty.call(fieldMap, field)) return null;
6418
+ var cfg = fieldMap[field];
6419
+ if (typeof cfg === 'string') cfg = { key: cfg };
6420
+ cfg = cfg || {};
6421
+ return {
6422
+ key: cfg.key || field,
6423
+ backend: _backendFor(cfg.storage === undefined ? pOpts.storage : cfg.storage),
6424
+ cookieOpts: cfg,
6425
+ serialize: cfg.serialize || defaultSerialize,
6426
+ deserialize: cfg.deserialize || defaultDeserialize
6427
+ };
6428
+ }
6429
+
6430
+ // A mapped field owns its key, so it must NOT also be written into the
6431
+ // blob \u2014 otherwise it round-trips through two places that can disagree.
6432
+ function _inBlob(field) {
6433
+ if (pick && pick.indexOf(field) === -1) return false;
6434
+ return !_mappedConfig(field);
6435
+ }
6436
+
5848
6437
  // Read persisted state (overrides hydration)
5849
6438
  try {
5850
- var saved = storageType.getItem(storageKey);
6439
+ var saved = storageType === 'cookie' ? null : storageType.getItem(storageKey);
5851
6440
  if (saved) {
5852
- var parsed = JSON.parse(saved);
6441
+ var parsed = defaultDeserialize(saved);
5853
6442
  console.log('[stx:store] restoring persisted state:', id, 'key:', storageKey, Object.keys(parsed));
5854
6443
  batch(function() {
5855
6444
  for (var pk in parsed) {
5856
- if (result[pk] && result[pk]._isSignal && (!pick || pick.indexOf(pk) !== -1)) {
6445
+ if (result[pk] && result[pk]._isSignal && _inBlob(pk)) {
5857
6446
  result[pk].set(parsed[pk]);
5858
6447
  }
5859
6448
  }
@@ -5863,21 +6452,73 @@ catch (e) {} }
5863
6452
  }
5864
6453
  } catch(e) { console.warn('[stx:store] persistence read error:', id, e); }
5865
6454
 
6455
+ // Per-field persistence: own key, own backend, own codec.
6456
+ //
6457
+ // The disposer swap is the same trick the blob effect below uses \u2014 these
6458
+ // effects must outlive the component that happened to instantiate the
6459
+ // store, or persistence would stop the first time that component
6460
+ // unmounted.
6461
+ var prevFieldDisposers = activeDisposers;
6462
+ activeDisposers = null;
6463
+ if (fieldMap) {
6464
+ for (var fk in fieldMap) {
6465
+ if (!Object.prototype.hasOwnProperty.call(fieldMap, fk)) continue;
6466
+ if (!result[fk] || !result[fk]._isSignal) {
6467
+ console.warn('[stx:store] persist.keys names a field that is not a signal:', id, fk);
6468
+ continue;
6469
+ }
6470
+ (function(field, cfg, signal) {
6471
+ if (cfg.backend === 'cookie') {
6472
+ // useCookie owns path / max-age / SameSite / Secure, so cookie
6473
+ // attributes do not have to be reimplemented here. It is a
6474
+ // string-valued signal, hence serialize on the way out.
6475
+ var cookieSignal = useCookie(cfg.key, cfg.cookieOpts);
6476
+ var initialCookie = cookieSignal();
6477
+ if (initialCookie !== '' && initialCookie != null) {
6478
+ try { signal.set(cfg.deserialize(initialCookie)); }
6479
+ catch(e) { console.warn('[stx:store] persist read error:', id, field, e); }
6480
+ }
6481
+ effect(function() {
6482
+ var next = signal();
6483
+ try { cookieSignal.set(next == null ? '' : String(cfg.serialize(next))); }
6484
+ catch(e) { console.warn('[stx:store] persist write error:', id, field, e); }
6485
+ });
6486
+ return;
6487
+ }
6488
+ try {
6489
+ var rawValue = cfg.backend.getItem(cfg.key);
6490
+ if (rawValue !== null) signal.set(cfg.deserialize(rawValue));
6491
+ } catch(e) { console.warn('[stx:store] persist read error:', id, field, e); }
6492
+ effect(function() {
6493
+ var value = signal();
6494
+ try { cfg.backend.setItem(cfg.key, String(cfg.serialize(value))); }
6495
+ catch(e) { console.warn('[stx:store] persist write error:', id, field, e); }
6496
+ });
6497
+ })(fk, _mappedConfig(fk), result[fk]);
6498
+ }
6499
+ }
6500
+ activeDisposers = prevFieldDisposers;
6501
+
5866
6502
  // Write on change (debounced)
5867
6503
  var writeTimer = null;
5868
6504
  var prevPersistDisposers = activeDisposers;
5869
6505
  activeDisposers = null;
5870
6506
  effect(function() {
5871
6507
  var snapshot = {};
6508
+ var wrote = false;
5872
6509
  for (var wk in result) {
5873
- if (result[wk] && result[wk]._isSignal && (!pick || pick.indexOf(wk) !== -1)) {
6510
+ if (result[wk] && result[wk]._isSignal && _inBlob(wk)) {
5874
6511
  snapshot[wk] = result[wk]();
6512
+ wrote = true;
5875
6513
  }
5876
6514
  }
6515
+ // With every field mapped to its own key there is no blob left, and
6516
+ // writing an empty object would clobber whatever else owns that key.
6517
+ if (!wrote || storageType === 'cookie') return;
5877
6518
  if (writeTimer) clearTimeout(writeTimer);
5878
6519
  writeTimer = setTimeout(function() {
5879
6520
  console.log('[stx:store] persisting:', id, 'key:', storageKey, Object.keys(snapshot));
5880
- try { storageType.setItem(storageKey, JSON.stringify(snapshot)); } catch(e) { console.warn('[stx:store] persist write error:', id, e); }
6521
+ try { storageType.setItem(storageKey, String(defaultSerialize(snapshot))); } catch(e) { console.warn('[stx:store] persist write error:', id, e); }
5881
6522
  }, 100);
5882
6523
  });
5883
6524
  activeDisposers = prevPersistDisposers;
@@ -5889,6 +6530,26 @@ catch (e) {} }
5889
6530
  // keep working); only template expression evaluation auto-unwraps.
5890
6531
  result._isStxStore = true;
5891
6532
 
6533
+ // Carry state across a hot replacement.
6534
+ //
6535
+ // The NEW store object is kept rather than patching the old one: a
6536
+ // setup-style store's actions close over the signals created in that
6537
+ // same run, so keeping the old signals would leave the new actions
6538
+ // writing to signals nothing is watching \u2014 an edit that appears to do
6539
+ // nothing, which is worse than the reload this replaces. Seeding the new
6540
+ // signals with the old values keeps state and behaviour consistent, and
6541
+ // the caller re-renders afterwards so components rebind to this object.
6542
+ var seed = window.stx.__hmrStoreSeed && window.stx.__hmrStoreSeed[id];
6543
+ if (seed) {
6544
+ for (var seedKey in seed) {
6545
+ var target = result[seedKey];
6546
+ if (target && target._isSignal && typeof target.set === 'function' && !target._isDerived) {
6547
+ try { target.set(seed[seedKey]); }
6548
+ catch (e) { /* shape changed across the edit \u2014 keep the new default */ }
6549
+ }
6550
+ }
6551
+ }
6552
+
5892
6553
  // Register globally
5893
6554
  window.stx._stores.set(id, result);
5894
6555
  window.__STX_STORES__ = window.__STX_STORES__ || {};
@@ -5898,6 +6559,55 @@ catch (e) {} }
5898
6559
  return result;
5899
6560
  },
5900
6561
 
6562
+ // Replace the store definitions in place, keeping their state \u2014 the
6563
+ // acceptHMRUpdate contract (#1877 ask 4). Called by the dev server's HMR
6564
+ // client when a file under storesDir changes, instead of reloading the
6565
+ // document and throwing away everything the SPA exists to preserve.
6566
+ __hmrReplaceStores: function(code) {
6567
+ var seed = {};
6568
+ window.stx._stores.forEach(function(store, id) {
6569
+ var values = {};
6570
+ for (var key in store) {
6571
+ var value = store[key];
6572
+ // Derived values recompute from state, so seeding them would fight
6573
+ // their own recomputation.
6574
+ if (value && value._isSignal && !value._isDerived) {
6575
+ try { values[key] = value.peek ? value.peek() : value(); }
6576
+ catch (e) { /* unreadable \u2014 let the new default stand */ }
6577
+ }
6578
+ }
6579
+ seed[id] = values;
6580
+ });
6581
+
6582
+ window.stx.__hmrStoreSeed = seed;
6583
+ window.stx.__hmrStoreReplacing = true;
6584
+ var ok = true;
6585
+ try {
6586
+ // Indirect eval, so the bundle evaluates at global scope the way it
6587
+ // does when inlined into the page.
6588
+ (0, eval)(code);
6589
+ }
6590
+ catch (e) {
6591
+ ok = false;
6592
+ console.error('[stx:hmr] store bundle failed to evaluate:', e);
6593
+ }
6594
+ finally {
6595
+ window.stx.__hmrStoreReplacing = false;
6596
+ window.stx.__hmrStoreSeed = null;
6597
+ }
6598
+
6599
+ if (!ok) return false;
6600
+
6601
+ // Components captured the PREVIOUS store object from useStore(), so
6602
+ // re-render the route to rebind them. Still not a document reload: the
6603
+ // swap keeps every non-store signal on the page alive.
6604
+ if (window.stxRouter && typeof window.stxRouter.refresh === 'function') {
6605
+ try { window.stxRouter.refresh(); }
6606
+ catch (e) { /* a refresh that cannot run is not worth a reload here */ }
6607
+ }
6608
+ return true;
6609
+ },
6610
+
5901
6611
  registerStoresClient: function(stores) {
5902
6612
  window.__STX_STORES__ = window.__STX_STORES__ || {};
5903
6613
  var names = Object.keys(stores || {});
@@ -6154,7 +6864,7 @@ else {
6154
6864
  window.defineStore = window.stx.defineStore;
6155
6865
  window.useStore = window.stx.useStore;
6156
6866
  window.ref = state;
6157
- window.reactive = state;
6867
+ window.reactive = reactive;
6158
6868
  window.computed = derived;
6159
6869
  window.watch = $watch;
6160
6870
  window.watchEffect = function(fn) { return effect(fn); };
@@ -6374,7 +7084,7 @@ else {
6374
7084
  el.removeAttribute('x-cloak');
6375
7085
  el.querySelectorAll('[x-cloak]').forEach(function(c) { c.removeAttribute('x-cloak'); });
6376
7086
 
6377
- mountCallbacks.forEach(fn => fn());
7087
+ runMountCallbacks(mountCallbacks, destroyCallbacks);
6378
7088
  });
6379
7089
 
6380
7090
  // Multi-root fragment fix: a no-body> setup page tags only the FIRST
@@ -6463,16 +7173,14 @@ else {
6463
7173
  var scopeDeferred = el.hasAttribute && el.hasAttribute('stx-hydrate') && !el.__stx_hydrated;
6464
7174
  if (scopeVars && scopeVars.__mountCallbacks && !scopeVars.__mounted && !scopeDeferred) {
6465
7175
  scopeVars.__mounted = true;
6466
- scopeVars.__mountCallbacks.forEach(fn => fn());
7176
+ runMountCallbacks(scopeVars.__mountCallbacks, scopeDestroySink(scopeVars));
6467
7177
  }
6468
7178
  });
6469
7179
 
6470
7180
  // Run global mount callbacks (from partial <script client> blocks that call onMount)
6471
7181
  // These are pushed during script execution, before DOMContentLoaded
6472
7182
  console.log('[stx] running global mountCallbacks:', mountCallbacks.length);
6473
- mountCallbacks.forEach(function(fn) {
6474
- try { fn(); } catch(e) { console.error('[stx] onMount error:', e); }
6475
- });
7183
+ runMountCallbacks(mountCallbacks, destroyCallbacks);
6476
7184
  mountCallbacks.length = 0;
6477
7185
 
6478
7186
  // Auto-process elements with data-stx-auto (skip already processed scoped elements)
@@ -7051,15 +7759,12 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
7051
7759
  var scopeVars = window.stx._scopes && window.stx._scopes[scopeId];
7052
7760
  if (scopeVars && scopeVars.__mountCallbacks && !scopeVars.__mounted) {
7053
7761
  scopeVars.__mounted = true;
7054
- scopeVars.__mountCallbacks.forEach(function(fn) { fn(); });
7762
+ runMountCallbacks(scopeVars.__mountCallbacks, scopeDestroySink(scopeVars));
7055
7763
  }
7056
7764
  });
7057
7765
 
7058
7766
  // Flush global mountCallbacks (from scripts re-executed after SPA content swap)
7059
- mountCallbacks.forEach(function(fn) {
7060
- try { fn(); }
7061
- catch (e) { console.warn('[stx] mount callback error:', e); }
7062
- });
7767
+ runMountCallbacks(mountCallbacks, destroyCallbacks);
7063
7768
  mountCallbacks.length = 0;
7064
7769
 
7065
7770
  // Everything that should have hydrated has now had its chance (#1773).