@fluixi/core 1.0.0-alpha.94 → 1.0.0-alpha.96

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 (63) hide show
  1. package/LICENSE +1 -1
  2. package/dist/cdn/core.global.js +2 -2
  3. package/dist/src/cdn/{chunk-7XM7Q5VR.mjs → chunk-RKBFEFZP.mjs} +125 -10
  4. package/dist/src/cdn/core.cjs +132 -12
  5. package/dist/src/cdn/core.mjs +4 -4
  6. package/dist/src/cdn/{router-VOY43YNM.mjs → router-V2THK27H.mjs} +3 -3
  7. package/dist/src/index.cjs +122 -7
  8. package/dist/src/index.mjs +122 -7
  9. package/dist/src/lib/client/{chunk-TSXM2PNY.mjs → chunk-RNRJHQFG.mjs} +1 -1
  10. package/dist/src/lib/client/index.cjs +133 -13
  11. package/dist/src/lib/client/index.d.ts.map +1 -1
  12. package/dist/src/lib/client/index.js +4 -1
  13. package/dist/src/lib/client/index.mjs +5 -5
  14. package/dist/src/lib/client/{router-BDVPLLYX.mjs → router-F3Y4BIVJ.mjs} +127 -12
  15. package/dist/src/lib/core.cjs +121 -6
  16. package/dist/src/lib/core.mjs +121 -6
  17. package/dist/src/lib/i18n/index.cjs +1 -1
  18. package/dist/src/lib/i18n/index.mjs +1 -1
  19. package/dist/src/lib/index.cjs +1 -1
  20. package/dist/src/lib/index.mjs +1 -1
  21. package/dist/src/lib/isomorphic.cjs +1 -1
  22. package/dist/src/lib/isomorphic.d.ts +5 -0
  23. package/dist/src/lib/isomorphic.d.ts.map +1 -1
  24. package/dist/src/lib/isomorphic.js +1 -1
  25. package/dist/src/lib/isomorphic.mjs +1 -1
  26. package/dist/src/lib/jsx-runtime/index.cjs +1 -1
  27. package/dist/src/lib/jsx-runtime/index.mjs +1 -1
  28. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +1 -1
  29. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +1 -1
  30. package/dist/src/lib/plugins/index.cjs +1 -1
  31. package/dist/src/lib/plugins/index.mjs +1 -1
  32. package/dist/src/lib/render/index.cjs +1 -1
  33. package/dist/src/lib/render/index.mjs +1 -1
  34. package/dist/src/lib/render/versions.cjs +1 -1
  35. package/dist/src/lib/render/versions.mjs +1 -1
  36. package/dist/src/lib/router/data.cjs +4 -4
  37. package/dist/src/lib/router/data.d.ts.map +1 -1
  38. package/dist/src/lib/router/data.js +9 -8
  39. package/dist/src/lib/router/data.mjs +4 -4
  40. package/dist/src/lib/router/index.cjs +126 -11
  41. package/dist/src/lib/router/index.d.ts +8 -5
  42. package/dist/src/lib/router/index.d.ts.map +1 -1
  43. package/dist/src/lib/router/index.js +94 -7
  44. package/dist/src/lib/router/index.mjs +126 -11
  45. package/dist/src/lib/router/scroll.cjs +79 -0
  46. package/dist/src/lib/router/scroll.d.ts +19 -0
  47. package/dist/src/lib/router/scroll.d.ts.map +1 -0
  48. package/dist/src/lib/router/scroll.js +68 -0
  49. package/dist/src/lib/router/scroll.mjs +58 -0
  50. package/dist/src/lib/server/index.cjs +1 -1
  51. package/dist/src/lib/server/index.mjs +1 -1
  52. package/dist/src/lib/storage/index.cjs +1 -1
  53. package/dist/src/lib/storage/index.mjs +1 -1
  54. package/dist/src/routes.cjs +1 -1
  55. package/dist/src/routes.mjs +1 -1
  56. package/dist/src/utils.cjs +1 -1
  57. package/dist/src/utils.mjs +1 -1
  58. package/dist/src/version.generated.cjs +1 -1
  59. package/dist/src/version.generated.d.ts +1 -1
  60. package/dist/src/version.generated.js +1 -1
  61. package/dist/src/version.generated.mjs +1 -1
  62. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  63. package/package.json +16 -16
@@ -114,7 +114,10 @@ export async function startClient(App, options = {}) {
114
114
  // component. Called directly, the root is the one component the runtime never hears
115
115
  // about: nothing holds its return value, so no node is made and a graph of the
116
116
  // application starts at whatever the root happened to render.
117
- domHydrate(rootOf(App), container);
117
+ // Built inside hydrate, not as its argument: a memo runs when it is made, and made out
118
+ // here the root would render before hydration is on, without the key scope the server
119
+ // opened for it. Every key below would then be one level off and nothing adopted by key.
120
+ domHydrate(() => rootOf(App), container);
118
121
  // Handlers are now wired (delegated $$listeners attach during the hydration
119
122
  // render): replay any events the user fired before hydration.
120
123
  replayEvents();
@@ -1,5 +1,5 @@
1
- /*! @fluixi/core v1.0.0-alpha.94 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
- import "./chunk-TSXM2PNY.mjs";
1
+ /*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+ import "./chunk-RNRJHQFG.mjs";
3
3
 
4
4
  // src/lib/client/index.ts
5
5
  import {
@@ -15,7 +15,7 @@ import {
15
15
  import { registerCoreVersion } from "@fluixi/dom";
16
16
 
17
17
  // src/version.generated.ts
18
- var VERSION = "1.0.0-alpha.94";
18
+ var VERSION = "1.0.0-alpha.96";
19
19
 
20
20
  // src/lib/render/versions.ts
21
21
  import { stampVersions, warnOnVersionSkew, versions } from "@fluixi/dom";
@@ -246,12 +246,12 @@ async function startClient(App, options = {}) {
246
246
  beginAppHydration();
247
247
  try {
248
248
  if (options.routes && options.routes.length) {
249
- const { preloadRoutes } = await import("./router-BDVPLLYX.mjs");
249
+ const { preloadRoutes } = await import("./router-F3Y4BIVJ.mjs");
250
250
  await preloadRoutes(options.routes, window.location.pathname);
251
251
  }
252
252
  deserializeHydrationData();
253
253
  setHydrating(true);
254
- domHydrate(rootOf(App), container);
254
+ domHydrate(() => rootOf(App), container);
255
255
  replayEvents();
256
256
  } finally {
257
257
  endAppHydration();
@@ -1,7 +1,7 @@
1
- /*! @fluixi/core v1.0.0-alpha.94 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  import {
3
3
  lazy
4
- } from "./chunk-TSXM2PNY.mjs";
4
+ } from "./chunk-RNRJHQFG.mjs";
5
5
 
6
6
  // src/lib/router/index.ts
7
7
  import {
@@ -75,6 +75,64 @@ import {
75
75
  import { createContext as createContext2, useContext as useContext2 } from "@fluixi/reactive/signal";
76
76
  import { get } from "@fluixi/utils/object";
77
77
  import { deepEqual } from "@fluixi/utils/compare";
78
+
79
+ // src/lib/router/scroll.ts
80
+ var STORE = "fx:scroll";
81
+ var KEEP = 100;
82
+ function keepScroll(initial) {
83
+ const read2 = () => {
84
+ try {
85
+ return JSON.parse(sessionStorage.getItem(STORE) ?? "{}");
86
+ } catch {
87
+ return {};
88
+ }
89
+ };
90
+ const positions = read2();
91
+ const write = () => {
92
+ const keys = Object.keys(positions);
93
+ for (const key of keys.slice(0, Math.max(0, keys.length - KEEP))) delete positions[key];
94
+ try {
95
+ sessionStorage.setItem(STORE, JSON.stringify(positions));
96
+ } catch {
97
+ }
98
+ };
99
+ let current = initial.key;
100
+ const save = () => {
101
+ if (current === void 0) return;
102
+ delete positions[current];
103
+ positions[current] = window.scrollY;
104
+ write();
105
+ };
106
+ const restoration = history.scrollRestoration;
107
+ history.scrollRestoration = "manual";
108
+ window.addEventListener("pagehide", save);
109
+ const to = (top) => window.scrollTo({ top, left: 0, behavior: "instant" });
110
+ const toHash = (hash) => {
111
+ if (!hash || hash === "#") return false;
112
+ const target = document.getElementById(decodeURIComponent(hash.slice(1)));
113
+ target?.scrollIntoView({ behavior: "instant" });
114
+ return target !== null;
115
+ };
116
+ return {
117
+ leave: save,
118
+ arrive(location, from) {
119
+ current = location.key;
120
+ if (location.action === "pop" || from === void 0) {
121
+ const y = location.key === void 0 ? void 0 : positions[location.key];
122
+ if (y !== void 0) to(y);
123
+ return;
124
+ }
125
+ if (location.action === "replace" && location.pathname === from.pathname) return;
126
+ if (!toHash(location.hash)) to(0);
127
+ },
128
+ dispose() {
129
+ window.removeEventListener("pagehide", save);
130
+ history.scrollRestoration = restoration;
131
+ }
132
+ };
133
+ }
134
+
135
+ // src/lib/router/index.ts
78
136
  import {
79
137
  createRouter as createCoreRouter,
80
138
  createBrowserHistory,
@@ -206,7 +264,8 @@ function getSubmissionsSignal(url) {
206
264
  }
207
265
  function action(fn, name, options = {}) {
208
266
  const actionUrl = name || fn.name || "action";
209
- const submit = async (input) => {
267
+ const submit = async (...args) => {
268
+ const input = args.length > 1 ? args : args[0];
210
269
  const [, setSubmission] = getSubmissionSignal(actionUrl);
211
270
  const [, setSubmissions] = getSubmissionsSignal(actionUrl);
212
271
  let entry;
@@ -221,11 +280,10 @@ function action(fn, name, options = {}) {
221
280
  };
222
281
  const retry = () => {
223
282
  drop2();
224
- return submit(input);
283
+ return submit(...args);
225
284
  };
226
285
  entry = {
227
286
  input,
228
- // Approximate type for tracking
229
287
  pending: true,
230
288
  clear,
231
289
  retry
@@ -233,7 +291,7 @@ function action(fn, name, options = {}) {
233
291
  setSubmission(entry);
234
292
  setSubmissions((list) => [...list, entry]);
235
293
  try {
236
- const result = await fn(input);
294
+ const result = await fn(...args);
237
295
  setSubmission({
238
296
  input,
239
297
  result,
@@ -311,14 +369,49 @@ function renderLayer(m, depth, base) {
311
369
  });
312
370
  });
313
371
  };
372
+ let readsLocation;
314
373
  const [resource, { refetch }] = createResource2(() => {
315
- const result = loader({ params: base.params, location: base.location }, owner, () => cancelled, trackDeps);
374
+ const params = untrack(base.params);
375
+ const location = untrack(base.location);
376
+ readsLocation = false;
377
+ const result = loader(
378
+ {
379
+ params: () => params,
380
+ location: () => {
381
+ readsLocation = true;
382
+ return location;
383
+ }
384
+ },
385
+ owner,
386
+ () => cancelled,
387
+ trackDeps
388
+ );
316
389
  return router ? router.trackLoad(result) : result;
317
390
  });
318
- onCleanup(onRevalidate(() => void refetch()));
391
+ onCleanup(
392
+ onRevalidate(() => {
393
+ if (untrack(() => base.matched()[depth]?.route) !== m.route) return;
394
+ void refetch();
395
+ })
396
+ );
397
+ let loadedFor = untrack(() => JSON.stringify(base.matched()[depth]?.params ?? {}));
398
+ let searchedFor = untrack(() => base.location().search);
399
+ createEffect(() => {
400
+ const now = base.matched()[depth];
401
+ const search = base.location().search;
402
+ if (now?.route !== m.route) return;
403
+ const params = JSON.stringify(now.params ?? {});
404
+ const paramsMoved = params !== loadedFor;
405
+ const searchMoved = readsLocation !== false && search !== searchedFor;
406
+ loadedFor = params;
407
+ searchedFor = search;
408
+ if (paramsMoved || searchMoved) untrack(() => void refetch());
409
+ });
319
410
  data = () => {
320
411
  const error = resource.error;
321
412
  if (error !== void 0 && error !== null) throw error;
413
+ const r = resource;
414
+ if (r.state === "refreshing") return r.latest;
322
415
  return resource();
323
416
  };
324
417
  Object.defineProperty(data, "loading", { get: () => resource.loading });
@@ -423,16 +516,19 @@ function buildDefaultNotFound() {
423
516
  }
424
517
  var pendingTransitionMode;
425
518
  function Router(props) {
426
- const history = props.history ?? createBrowserHistory();
519
+ const history2 = props.history ?? createBrowserHistory();
427
520
  const router = createCoreRouter({
428
521
  routes: props.routes,
429
522
  base: props.base,
430
- history
523
+ history: history2
431
524
  });
432
525
  const [state, setState] = createSignal2(router.state());
433
526
  const [navigating, setNavigating] = createSignal2(false);
434
527
  const [loads, setLoads] = createSignal2(0);
435
528
  let navigationId = 0;
529
+ const scroll = history2.window ? keepScroll(router.state().location) : void 0;
530
+ let arriving = scroll ? { id: 0, location: router.state().location, from: void 0 } : null;
531
+ const [arrived, setArrived] = createSignal2(0);
436
532
  const takeMode = () => {
437
533
  const mode = pendingTransitionMode ?? (props.transition ?? "auto");
438
534
  pendingTransitionMode = void 0;
@@ -442,6 +538,10 @@ function Router(props) {
442
538
  const id = ++navigationId;
443
539
  const mode = takeMode();
444
540
  setNavigating(true);
541
+ if (scroll) {
542
+ scroll.leave();
543
+ arriving = { id, location: next.location, from: untrack(state).location };
544
+ }
445
545
  const prepared = prepareComponents(
446
546
  (next.match?.matched ?? []).map((m) => m.component).filter(Boolean)
447
547
  );
@@ -449,6 +549,7 @@ function Router(props) {
449
549
  setState(next);
450
550
  const flushed = flush();
451
551
  if (id === navigationId) setNavigating(false);
552
+ if (scroll) setArrived(id);
452
553
  return flushed;
453
554
  };
454
555
  if (!prepared) {
@@ -462,6 +563,7 @@ function Router(props) {
462
563
  });
463
564
  onCleanup(() => {
464
565
  unsub();
566
+ scroll?.dispose();
465
567
  router.destroy();
466
568
  navigationId++;
467
569
  });
@@ -484,6 +586,19 @@ function Router(props) {
484
586
  return result;
485
587
  }
486
588
  };
589
+ if (scroll) {
590
+ const nextFrame = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 0);
591
+ createEffect(() => {
592
+ const id = arrived();
593
+ if (ctx.isRouting()) return;
594
+ const next = arriving;
595
+ if (!next || next.id !== id || id !== navigationId) return;
596
+ arriving = null;
597
+ nextFrame(() => {
598
+ if (id === navigationId) scroll.arrive(next.location, next.from);
599
+ });
600
+ });
601
+ }
487
602
  const root = {
488
603
  depth: -1,
489
604
  matched,
@@ -699,7 +814,7 @@ function Link(props) {
699
814
  return a;
700
815
  }
701
816
  var A = Link;
702
- function Form(props) {
817
+ function ActionForm(props) {
703
818
  const action2 = props.action;
704
819
  const cls = props.className ?? props.class;
705
820
  const url = action2.url ?? "";
@@ -739,7 +854,7 @@ async function preloadRoutes(routes, pathname) {
739
854
  }
740
855
  export {
741
856
  A,
742
- Form,
857
+ ActionForm,
743
858
  Link,
744
859
  Outlet,
745
860
  Redirect,
@@ -152,7 +152,7 @@ var import_signal6 = require("@fluixi/reactive/signal");
152
152
  var import_dom = require("@fluixi/dom");
153
153
 
154
154
  // src/version.generated.ts
155
- var VERSION = "1.0.0-alpha.94";
155
+ var VERSION = "1.0.0-alpha.96";
156
156
 
157
157
  // src/lib/render/versions.ts
158
158
  var import_dom2 = require("@fluixi/dom");
@@ -762,7 +762,7 @@ function createIsomorphicStore(key, options) {
762
762
  }
763
763
  return initPromise;
764
764
  };
765
- return { value, fields, set, setField, ensureInit, key };
765
+ return { value, fields, set, setField, ensureInit, key, seeded };
766
766
  }
767
767
 
768
768
  // src/lib/env/index.ts
@@ -1177,6 +1177,64 @@ var import_dom15 = require("@fluixi/dom");
1177
1177
  var import_signal16 = require("@fluixi/reactive/signal");
1178
1178
  var import_object = require("@fluixi/utils/object");
1179
1179
  var import_compare = require("@fluixi/utils/compare");
1180
+
1181
+ // src/lib/router/scroll.ts
1182
+ var STORE = "fx:scroll";
1183
+ var KEEP = 100;
1184
+ function keepScroll(initial) {
1185
+ const read3 = () => {
1186
+ try {
1187
+ return JSON.parse(sessionStorage.getItem(STORE) ?? "{}");
1188
+ } catch {
1189
+ return {};
1190
+ }
1191
+ };
1192
+ const positions = read3();
1193
+ const write = () => {
1194
+ const keys = Object.keys(positions);
1195
+ for (const key of keys.slice(0, Math.max(0, keys.length - KEEP))) delete positions[key];
1196
+ try {
1197
+ sessionStorage.setItem(STORE, JSON.stringify(positions));
1198
+ } catch {
1199
+ }
1200
+ };
1201
+ let current = initial.key;
1202
+ const save = () => {
1203
+ if (current === void 0) return;
1204
+ delete positions[current];
1205
+ positions[current] = window.scrollY;
1206
+ write();
1207
+ };
1208
+ const restoration = history.scrollRestoration;
1209
+ history.scrollRestoration = "manual";
1210
+ window.addEventListener("pagehide", save);
1211
+ const to = (top) => window.scrollTo({ top, left: 0, behavior: "instant" });
1212
+ const toHash = (hash) => {
1213
+ if (!hash || hash === "#") return false;
1214
+ const target = document.getElementById(decodeURIComponent(hash.slice(1)));
1215
+ target?.scrollIntoView({ behavior: "instant" });
1216
+ return target !== null;
1217
+ };
1218
+ return {
1219
+ leave: save,
1220
+ arrive(location, from) {
1221
+ current = location.key;
1222
+ if (location.action === "pop" || from === void 0) {
1223
+ const y = location.key === void 0 ? void 0 : positions[location.key];
1224
+ if (y !== void 0) to(y);
1225
+ return;
1226
+ }
1227
+ if (location.action === "replace" && location.pathname === from.pathname) return;
1228
+ if (!toHash(location.hash)) to(0);
1229
+ },
1230
+ dispose() {
1231
+ window.removeEventListener("pagehide", save);
1232
+ history.scrollRestoration = restoration;
1233
+ }
1234
+ };
1235
+ }
1236
+
1237
+ // src/lib/router/index.ts
1180
1238
  var import_router = require("@fluixi/router");
1181
1239
 
1182
1240
  // src/lib/router/data.ts
@@ -1251,14 +1309,49 @@ function renderLayer(m, depth, base) {
1251
1309
  });
1252
1310
  });
1253
1311
  };
1312
+ let readsLocation;
1254
1313
  const [resource2, { refetch }] = (0, import_signal15.createResource)(() => {
1255
- const result = loader({ params: base.params, location: base.location }, owner, () => cancelled, trackDeps);
1314
+ const params = (0, import_signal15.untrack)(base.params);
1315
+ const location = (0, import_signal15.untrack)(base.location);
1316
+ readsLocation = false;
1317
+ const result = loader(
1318
+ {
1319
+ params: () => params,
1320
+ location: () => {
1321
+ readsLocation = true;
1322
+ return location;
1323
+ }
1324
+ },
1325
+ owner,
1326
+ () => cancelled,
1327
+ trackDeps
1328
+ );
1256
1329
  return router ? router.trackLoad(result) : result;
1257
1330
  });
1258
- (0, import_signal15.onCleanup)(onRevalidate(() => void refetch()));
1331
+ (0, import_signal15.onCleanup)(
1332
+ onRevalidate(() => {
1333
+ if ((0, import_signal15.untrack)(() => base.matched()[depth]?.route) !== m.route) return;
1334
+ void refetch();
1335
+ })
1336
+ );
1337
+ let loadedFor = (0, import_signal15.untrack)(() => JSON.stringify(base.matched()[depth]?.params ?? {}));
1338
+ let searchedFor = (0, import_signal15.untrack)(() => base.location().search);
1339
+ (0, import_signal15.createEffect)(() => {
1340
+ const now = base.matched()[depth];
1341
+ const search = base.location().search;
1342
+ if (now?.route !== m.route) return;
1343
+ const params = JSON.stringify(now.params ?? {});
1344
+ const paramsMoved = params !== loadedFor;
1345
+ const searchMoved = readsLocation !== false && search !== searchedFor;
1346
+ loadedFor = params;
1347
+ searchedFor = search;
1348
+ if (paramsMoved || searchMoved) (0, import_signal15.untrack)(() => void refetch());
1349
+ });
1259
1350
  data = () => {
1260
1351
  const error = resource2.error;
1261
1352
  if (error !== void 0 && error !== null) throw error;
1353
+ const r = resource2;
1354
+ if (r.state === "refreshing") return r.latest;
1262
1355
  return resource2();
1263
1356
  };
1264
1357
  Object.defineProperty(data, "loading", { get: () => resource2.loading });
@@ -1363,16 +1456,19 @@ function buildDefaultNotFound() {
1363
1456
  }
1364
1457
  var pendingTransitionMode;
1365
1458
  function Router(props) {
1366
- const history = props.history ?? (0, import_router.createBrowserHistory)();
1459
+ const history2 = props.history ?? (0, import_router.createBrowserHistory)();
1367
1460
  const router = (0, import_router.createRouter)({
1368
1461
  routes: props.routes,
1369
1462
  base: props.base,
1370
- history
1463
+ history: history2
1371
1464
  });
1372
1465
  const [state, setState] = (0, import_signal15.createSignal)(router.state());
1373
1466
  const [navigating, setNavigating] = (0, import_signal15.createSignal)(false);
1374
1467
  const [loads, setLoads] = (0, import_signal15.createSignal)(0);
1375
1468
  let navigationId = 0;
1469
+ const scroll = history2.window ? keepScroll(router.state().location) : void 0;
1470
+ let arriving = scroll ? { id: 0, location: router.state().location, from: void 0 } : null;
1471
+ const [arrived, setArrived] = (0, import_signal15.createSignal)(0);
1376
1472
  const takeMode = () => {
1377
1473
  const mode = pendingTransitionMode ?? (props.transition ?? "auto");
1378
1474
  pendingTransitionMode = void 0;
@@ -1382,6 +1478,10 @@ function Router(props) {
1382
1478
  const id = ++navigationId;
1383
1479
  const mode = takeMode();
1384
1480
  setNavigating(true);
1481
+ if (scroll) {
1482
+ scroll.leave();
1483
+ arriving = { id, location: next.location, from: (0, import_signal15.untrack)(state).location };
1484
+ }
1385
1485
  const prepared = prepareComponents(
1386
1486
  (next.match?.matched ?? []).map((m) => m.component).filter(Boolean)
1387
1487
  );
@@ -1389,6 +1489,7 @@ function Router(props) {
1389
1489
  setState(next);
1390
1490
  const flushed = (0, import_signal15.flush)();
1391
1491
  if (id === navigationId) setNavigating(false);
1492
+ if (scroll) setArrived(id);
1392
1493
  return flushed;
1393
1494
  };
1394
1495
  if (!prepared) {
@@ -1402,6 +1503,7 @@ function Router(props) {
1402
1503
  });
1403
1504
  (0, import_signal15.onCleanup)(() => {
1404
1505
  unsub();
1506
+ scroll?.dispose();
1405
1507
  router.destroy();
1406
1508
  navigationId++;
1407
1509
  });
@@ -1424,6 +1526,19 @@ function Router(props) {
1424
1526
  return result;
1425
1527
  }
1426
1528
  };
1529
+ if (scroll) {
1530
+ const nextFrame = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 0);
1531
+ (0, import_signal15.createEffect)(() => {
1532
+ const id = arrived();
1533
+ if (ctx.isRouting()) return;
1534
+ const next = arriving;
1535
+ if (!next || next.id !== id || id !== navigationId) return;
1536
+ arriving = null;
1537
+ nextFrame(() => {
1538
+ if (id === navigationId) scroll.arrive(next.location, next.from);
1539
+ });
1540
+ });
1541
+ }
1427
1542
  const root = {
1428
1543
  depth: -1,
1429
1544
  matched,
@@ -38,7 +38,7 @@ import { createRoot as createRoot2 } from "@fluixi/reactive/signal";
38
38
  import { registerCoreVersion } from "@fluixi/dom";
39
39
 
40
40
  // src/version.generated.ts
41
- var VERSION = "1.0.0-alpha.94";
41
+ var VERSION = "1.0.0-alpha.96";
42
42
 
43
43
  // src/lib/render/versions.ts
44
44
  import { stampVersions, warnOnVersionSkew, versions } from "@fluixi/dom";
@@ -690,7 +690,7 @@ function createIsomorphicStore(key, options) {
690
690
  }
691
691
  return initPromise;
692
692
  };
693
- return { value, fields, set, setField, ensureInit, key };
693
+ return { value, fields, set, setField, ensureInit, key, seeded };
694
694
  }
695
695
 
696
696
  // src/lib/env/index.ts
@@ -1126,6 +1126,64 @@ import {
1126
1126
  import { createContext as createContext5, useContext as useContext5 } from "@fluixi/reactive/signal";
1127
1127
  import { get } from "@fluixi/utils/object";
1128
1128
  import { deepEqual } from "@fluixi/utils/compare";
1129
+
1130
+ // src/lib/router/scroll.ts
1131
+ var STORE = "fx:scroll";
1132
+ var KEEP = 100;
1133
+ function keepScroll(initial) {
1134
+ const read3 = () => {
1135
+ try {
1136
+ return JSON.parse(sessionStorage.getItem(STORE) ?? "{}");
1137
+ } catch {
1138
+ return {};
1139
+ }
1140
+ };
1141
+ const positions = read3();
1142
+ const write = () => {
1143
+ const keys = Object.keys(positions);
1144
+ for (const key of keys.slice(0, Math.max(0, keys.length - KEEP))) delete positions[key];
1145
+ try {
1146
+ sessionStorage.setItem(STORE, JSON.stringify(positions));
1147
+ } catch {
1148
+ }
1149
+ };
1150
+ let current = initial.key;
1151
+ const save = () => {
1152
+ if (current === void 0) return;
1153
+ delete positions[current];
1154
+ positions[current] = window.scrollY;
1155
+ write();
1156
+ };
1157
+ const restoration = history.scrollRestoration;
1158
+ history.scrollRestoration = "manual";
1159
+ window.addEventListener("pagehide", save);
1160
+ const to = (top) => window.scrollTo({ top, left: 0, behavior: "instant" });
1161
+ const toHash = (hash) => {
1162
+ if (!hash || hash === "#") return false;
1163
+ const target = document.getElementById(decodeURIComponent(hash.slice(1)));
1164
+ target?.scrollIntoView({ behavior: "instant" });
1165
+ return target !== null;
1166
+ };
1167
+ return {
1168
+ leave: save,
1169
+ arrive(location, from) {
1170
+ current = location.key;
1171
+ if (location.action === "pop" || from === void 0) {
1172
+ const y = location.key === void 0 ? void 0 : positions[location.key];
1173
+ if (y !== void 0) to(y);
1174
+ return;
1175
+ }
1176
+ if (location.action === "replace" && location.pathname === from.pathname) return;
1177
+ if (!toHash(location.hash)) to(0);
1178
+ },
1179
+ dispose() {
1180
+ window.removeEventListener("pagehide", save);
1181
+ history.scrollRestoration = restoration;
1182
+ }
1183
+ };
1184
+ }
1185
+
1186
+ // src/lib/router/index.ts
1129
1187
  import {
1130
1188
  createRouter as createCoreRouter,
1131
1189
  createBrowserHistory,
@@ -1213,14 +1271,49 @@ function renderLayer(m, depth, base) {
1213
1271
  });
1214
1272
  });
1215
1273
  };
1274
+ let readsLocation;
1216
1275
  const [resource2, { refetch }] = createResource5(() => {
1217
- const result = loader({ params: base.params, location: base.location }, owner, () => cancelled, trackDeps);
1276
+ const params = untrack4(base.params);
1277
+ const location = untrack4(base.location);
1278
+ readsLocation = false;
1279
+ const result = loader(
1280
+ {
1281
+ params: () => params,
1282
+ location: () => {
1283
+ readsLocation = true;
1284
+ return location;
1285
+ }
1286
+ },
1287
+ owner,
1288
+ () => cancelled,
1289
+ trackDeps
1290
+ );
1218
1291
  return router ? router.trackLoad(result) : result;
1219
1292
  });
1220
- onCleanup4(onRevalidate(() => void refetch()));
1293
+ onCleanup4(
1294
+ onRevalidate(() => {
1295
+ if (untrack4(() => base.matched()[depth]?.route) !== m.route) return;
1296
+ void refetch();
1297
+ })
1298
+ );
1299
+ let loadedFor = untrack4(() => JSON.stringify(base.matched()[depth]?.params ?? {}));
1300
+ let searchedFor = untrack4(() => base.location().search);
1301
+ createEffect4(() => {
1302
+ const now = base.matched()[depth];
1303
+ const search = base.location().search;
1304
+ if (now?.route !== m.route) return;
1305
+ const params = JSON.stringify(now.params ?? {});
1306
+ const paramsMoved = params !== loadedFor;
1307
+ const searchMoved = readsLocation !== false && search !== searchedFor;
1308
+ loadedFor = params;
1309
+ searchedFor = search;
1310
+ if (paramsMoved || searchMoved) untrack4(() => void refetch());
1311
+ });
1221
1312
  data = () => {
1222
1313
  const error = resource2.error;
1223
1314
  if (error !== void 0 && error !== null) throw error;
1315
+ const r = resource2;
1316
+ if (r.state === "refreshing") return r.latest;
1224
1317
  return resource2();
1225
1318
  };
1226
1319
  Object.defineProperty(data, "loading", { get: () => resource2.loading });
@@ -1325,16 +1418,19 @@ function buildDefaultNotFound() {
1325
1418
  }
1326
1419
  var pendingTransitionMode;
1327
1420
  function Router(props) {
1328
- const history = props.history ?? createBrowserHistory();
1421
+ const history2 = props.history ?? createBrowserHistory();
1329
1422
  const router = createCoreRouter({
1330
1423
  routes: props.routes,
1331
1424
  base: props.base,
1332
- history
1425
+ history: history2
1333
1426
  });
1334
1427
  const [state, setState] = createSignal8(router.state());
1335
1428
  const [navigating, setNavigating] = createSignal8(false);
1336
1429
  const [loads, setLoads] = createSignal8(0);
1337
1430
  let navigationId = 0;
1431
+ const scroll = history2.window ? keepScroll(router.state().location) : void 0;
1432
+ let arriving = scroll ? { id: 0, location: router.state().location, from: void 0 } : null;
1433
+ const [arrived, setArrived] = createSignal8(0);
1338
1434
  const takeMode = () => {
1339
1435
  const mode = pendingTransitionMode ?? (props.transition ?? "auto");
1340
1436
  pendingTransitionMode = void 0;
@@ -1344,6 +1440,10 @@ function Router(props) {
1344
1440
  const id = ++navigationId;
1345
1441
  const mode = takeMode();
1346
1442
  setNavigating(true);
1443
+ if (scroll) {
1444
+ scroll.leave();
1445
+ arriving = { id, location: next.location, from: untrack4(state).location };
1446
+ }
1347
1447
  const prepared = prepareComponents(
1348
1448
  (next.match?.matched ?? []).map((m) => m.component).filter(Boolean)
1349
1449
  );
@@ -1351,6 +1451,7 @@ function Router(props) {
1351
1451
  setState(next);
1352
1452
  const flushed = flush();
1353
1453
  if (id === navigationId) setNavigating(false);
1454
+ if (scroll) setArrived(id);
1354
1455
  return flushed;
1355
1456
  };
1356
1457
  if (!prepared) {
@@ -1364,6 +1465,7 @@ function Router(props) {
1364
1465
  });
1365
1466
  onCleanup4(() => {
1366
1467
  unsub();
1468
+ scroll?.dispose();
1367
1469
  router.destroy();
1368
1470
  navigationId++;
1369
1471
  });
@@ -1386,6 +1488,19 @@ function Router(props) {
1386
1488
  return result;
1387
1489
  }
1388
1490
  };
1491
+ if (scroll) {
1492
+ const nextFrame = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 0);
1493
+ createEffect4(() => {
1494
+ const id = arrived();
1495
+ if (ctx.isRouting()) return;
1496
+ const next = arriving;
1497
+ if (!next || next.id !== id || id !== navigationId) return;
1498
+ arriving = null;
1499
+ nextFrame(() => {
1500
+ if (id === navigationId) scroll.arrive(next.location, next.from);
1501
+ });
1502
+ });
1503
+ }
1389
1504
  const root = {
1390
1505
  depth: -1,
1391
1506
  matched,
@@ -1,4 +1,4 @@
1
- /*! @fluixi/core v1.0.0-alpha.94 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/core v1.0.0-alpha.96 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;