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

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 (113) hide show
  1. package/dist/cdn/core.global.js +2 -2
  2. package/dist/src/cdn/{chunk-MDJLC3PE.mjs → chunk-7XM7Q5VR.mjs} +271 -229
  3. package/dist/src/cdn/core.cjs +360 -253
  4. package/dist/src/cdn/core.mjs +92 -20
  5. package/dist/src/cdn/{router-JYFZP3EZ.mjs → router-VOY43YNM.mjs} +7 -3
  6. package/dist/src/index.cjs +548 -232
  7. package/dist/src/index.mjs +549 -224
  8. package/dist/src/lib/client/chunk-TSXM2PNY.mjs +53 -0
  9. package/dist/src/lib/client/index.cjs +310 -176
  10. package/dist/src/lib/client/index.d.ts.map +1 -1
  11. package/dist/src/lib/client/index.js +33 -18
  12. package/dist/src/lib/client/index.mjs +86 -15
  13. package/dist/src/lib/client/{router-PHLUYVQU.mjs → router-BDVPLLYX.mjs} +244 -99
  14. package/dist/src/lib/context/context.cjs +35 -72
  15. package/dist/src/lib/context/context.d.ts +45 -291
  16. package/dist/src/lib/context/context.d.ts.map +1 -1
  17. package/dist/src/lib/context/context.js +65 -397
  18. package/dist/src/lib/context/context.mjs +38 -73
  19. package/dist/src/lib/context/index.cjs +35 -72
  20. package/dist/src/lib/context/index.d.ts +2 -2
  21. package/dist/src/lib/context/index.d.ts.map +1 -1
  22. package/dist/src/lib/context/index.js +1 -5
  23. package/dist/src/lib/context/index.mjs +38 -73
  24. package/dist/src/lib/core.cjs +547 -231
  25. package/dist/src/lib/core.d.ts +5 -3
  26. package/dist/src/lib/core.d.ts.map +1 -1
  27. package/dist/src/lib/core.js +6 -4
  28. package/dist/src/lib/core.mjs +548 -223
  29. package/dist/src/lib/env/index.cjs +1 -17
  30. package/dist/src/lib/env/index.d.ts +2 -14
  31. package/dist/src/lib/env/index.d.ts.map +1 -1
  32. package/dist/src/lib/env/index.js +2 -26
  33. package/dist/src/lib/env/index.mjs +1 -17
  34. package/dist/src/lib/i18n/index.cjs +17 -2
  35. package/dist/src/lib/i18n/index.d.ts +10 -0
  36. package/dist/src/lib/i18n/index.d.ts.map +1 -1
  37. package/dist/src/lib/i18n/index.js +17 -1
  38. package/dist/src/lib/i18n/index.mjs +17 -2
  39. package/dist/src/lib/index.cjs +64 -99
  40. package/dist/src/lib/index.mjs +43 -76
  41. package/dist/src/lib/isomorphic.cjs +9 -4
  42. package/dist/src/lib/isomorphic.d.ts +9 -2
  43. package/dist/src/lib/isomorphic.d.ts.map +1 -1
  44. package/dist/src/lib/isomorphic.js +11 -4
  45. package/dist/src/lib/isomorphic.mjs +9 -4
  46. package/dist/src/lib/jsx-runtime/index.cjs +1 -1
  47. package/dist/src/lib/jsx-runtime/index.mjs +1 -1
  48. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +1 -1
  49. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +1 -1
  50. package/dist/src/lib/plugins/fluixi-routes-plugin.cjs +2 -2
  51. package/dist/src/lib/plugins/fluixi-routes-plugin.mjs +2 -2
  52. package/dist/src/lib/plugins/index.cjs +3 -3
  53. package/dist/src/lib/plugins/index.mjs +3 -3
  54. package/dist/src/lib/plugins/route-codegen.cjs +2 -2
  55. package/dist/src/lib/plugins/route-codegen.js +1 -1
  56. package/dist/src/lib/plugins/route-codegen.mjs +2 -2
  57. package/dist/src/lib/render/index.cjs +1 -1
  58. package/dist/src/lib/render/index.mjs +1 -1
  59. package/dist/src/lib/render/versions.cjs +1 -1
  60. package/dist/src/lib/render/versions.mjs +1 -1
  61. package/dist/src/lib/router/data.cjs +113 -115
  62. package/dist/src/lib/router/data.d.ts +63 -10
  63. package/dist/src/lib/router/data.d.ts.map +1 -1
  64. package/dist/src/lib/router/data.js +158 -68
  65. package/dist/src/lib/router/data.mjs +112 -112
  66. package/dist/src/lib/router/index.cjs +260 -163
  67. package/dist/src/lib/router/index.d.ts +22 -2
  68. package/dist/src/lib/router/index.d.ts.map +1 -1
  69. package/dist/src/lib/router/index.js +165 -22
  70. package/dist/src/lib/router/index.mjs +244 -143
  71. package/dist/src/lib/server/index.cjs +1 -1
  72. package/dist/src/lib/server/index.mjs +1 -1
  73. package/dist/src/lib/storage/cache.cjs +51 -0
  74. package/dist/src/lib/storage/cache.d.ts +18 -0
  75. package/dist/src/lib/storage/cache.d.ts.map +1 -0
  76. package/dist/src/lib/storage/cache.js +42 -0
  77. package/dist/src/lib/storage/cache.mjs +30 -0
  78. package/dist/src/lib/storage/index.cjs +246 -0
  79. package/dist/src/lib/storage/index.d.ts +83 -0
  80. package/dist/src/lib/storage/index.d.ts.map +1 -0
  81. package/dist/src/lib/storage/index.js +179 -0
  82. package/dist/src/lib/storage/index.mjs +231 -0
  83. package/dist/src/lib/storage/persisted.cjs +91 -0
  84. package/dist/src/lib/storage/persisted.d.ts +53 -0
  85. package/dist/src/lib/storage/persisted.d.ts.map +1 -0
  86. package/dist/src/lib/storage/persisted.js +75 -0
  87. package/dist/src/lib/storage/persisted.mjs +70 -0
  88. package/dist/src/lib/theme/color-scheme.cjs +281 -0
  89. package/dist/src/lib/theme/color-scheme.d.ts +61 -0
  90. package/dist/src/lib/theme/color-scheme.d.ts.map +1 -0
  91. package/dist/src/lib/theme/color-scheme.js +120 -0
  92. package/dist/src/lib/theme/color-scheme.mjs +265 -0
  93. package/dist/src/lib/theme/index.cjs +194 -37
  94. package/dist/src/lib/theme/index.d.ts +23 -11
  95. package/dist/src/lib/theme/index.d.ts.map +1 -1
  96. package/dist/src/lib/theme/index.js +73 -32
  97. package/dist/src/lib/theme/index.mjs +197 -35
  98. package/dist/src/lib/theme/provider.cjs +346 -0
  99. package/dist/src/lib/theme/provider.d.ts +34 -0
  100. package/dist/src/lib/theme/provider.d.ts.map +1 -0
  101. package/dist/src/lib/theme/provider.js +60 -0
  102. package/dist/src/lib/theme/provider.mjs +332 -0
  103. package/dist/src/routes.cjs +1 -1
  104. package/dist/src/routes.mjs +1 -1
  105. package/dist/src/utils.cjs +1 -1
  106. package/dist/src/utils.mjs +1 -1
  107. package/dist/src/version.generated.cjs +1 -1
  108. package/dist/src/version.generated.d.ts +1 -1
  109. package/dist/src/version.generated.js +1 -1
  110. package/dist/src/version.generated.mjs +1 -1
  111. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  112. package/package.json +18 -11
  113. package/dist/src/lib/client/chunk-VB567ABU.mjs +0 -133
@@ -1,4 +1,4 @@
1
- /*! @fluixi/core v1.0.0-alpha.92 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
1
+ /*! @fluixi/core v1.0.0-alpha.94 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -21,85 +21,6 @@ var __copyProps = (to, from, except, desc) => {
21
21
  };
22
22
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
23
 
24
- // src/lib/server/hydration.ts
25
- function isHydrating() {
26
- return hydrating;
27
- }
28
- function setHydrating(state) {
29
- hydrating = state;
30
- }
31
- function setHydrationData(key, data) {
32
- hydrationDataMap.set(key, data);
33
- }
34
- function getHydrationData(key) {
35
- return hydrationDataMap.get(key);
36
- }
37
- function clearHydrationData() {
38
- hydrationDataMap.clear();
39
- }
40
- function deserializeHydrationData() {
41
- if (typeof window === "undefined") {
42
- return false;
43
- }
44
- const script = document.getElementById("__FLUIXI_HYDRATION__");
45
- if (!script || script.getAttribute("type") !== "application/json") {
46
- return false;
47
- }
48
- try {
49
- const json = script.textContent || "{}";
50
- const data = JSON.parse(json);
51
- for (const [key, value] of Object.entries(data)) {
52
- hydrationDataMap.set(key, value);
53
- }
54
- return true;
55
- } catch (error) {
56
- console.error("Failed to deserialize hydration data:", error);
57
- return false;
58
- }
59
- }
60
- function cleanupHydration() {
61
- if (typeof window === "undefined") {
62
- return;
63
- }
64
- const script = document.getElementById("__FLUIXI_HYDRATION__");
65
- if (script) {
66
- script.remove();
67
- }
68
- const walker = document.createTreeWalker(
69
- document.body,
70
- NodeFilter.SHOW_COMMENT,
71
- null
72
- );
73
- const comments = [];
74
- let node;
75
- while (node = walker.nextNode()) {
76
- const comment = node;
77
- const text = comment.nodeValue || "";
78
- if (text.startsWith("#") || text.startsWith("/")) {
79
- comments.push(comment);
80
- }
81
- }
82
- comments.forEach((comment) => comment.remove());
83
- clearHydrationData();
84
- }
85
- function isServerEnvironment() {
86
- return (0, import_dom3.isServer)();
87
- }
88
- var import_dom3, hydrating, hydrationDataMap;
89
- var init_hydration = __esm({
90
- "src/lib/server/hydration.ts"() {
91
- "use strict";
92
- import_dom3 = require("@fluixi/dom");
93
- hydrating = false;
94
- hydrationDataMap = /* @__PURE__ */ new Map();
95
- if (typeof window !== "undefined") {
96
- window.addEventListener("load", () => {
97
- setTimeout(cleanupHydration, 100);
98
- });
99
- }
100
- }
101
- });
102
-
103
24
  // src/lib/render/lazy.ts
104
25
  function fromChunk(value) {
105
26
  if (value != null && (typeof value === "object" || typeof value === "function")) {
@@ -206,107 +127,130 @@ var init_transition = __esm({
206
127
  // src/lib/context/context.ts
207
128
  function createContext3(defaultValue, name) {
208
129
  const context = (0, import_signal7.createContext)(defaultValue, name);
209
- const Provider = (props) => {
210
- if (props.debug === true) {
211
- }
212
- return context.Provider({
130
+ const Provider = (props) => (
131
+ // Children stay a getter: the reactive Provider reads them after stamping the value on
132
+ // its owner. Reading `props.children` here would render them, and any <Outlet/> in them,
133
+ // before the context exists.
134
+ context.Provider({
213
135
  value: props.value,
214
136
  get children() {
215
137
  return props.children;
216
138
  },
217
139
  debug: props.debug
218
- });
219
- };
220
- if (typeof Symbol !== "undefined" && Symbol.for) {
221
- Provider.$$typeof = /* @__PURE__ */ Symbol.for("react.provider");
222
- }
223
- Provider.$$typeof = /* @__PURE__ */ Symbol.for("react.provider");
140
+ })
141
+ );
224
142
  return {
225
143
  id: context.id,
226
144
  Provider: (0, import_dom11.asProvider)(Provider),
227
145
  defaultValue
228
146
  };
229
147
  }
230
- var import_dom11, import_signal7, import_signal8;
148
+ var import_dom11, import_signal7;
231
149
  var init_context = __esm({
232
150
  "src/lib/context/context.ts"() {
233
151
  "use strict";
234
152
  import_dom11 = require("@fluixi/dom");
235
153
  import_signal7 = require("@fluixi/reactive/signal");
236
- import_signal8 = require("@fluixi/reactive/signal");
237
154
  }
238
155
  });
239
156
 
240
157
  // src/lib/router/data.ts
241
- function cache(fn, name) {
242
- return async (...args) => {
158
+ function onRevalidate(listener) {
159
+ revalidationListeners.add(listener);
160
+ return () => void revalidationListeners.delete(listener);
161
+ }
162
+ function cache(fn, name, options = {}) {
163
+ const { ttl = Infinity, staleWhileRevalidate = true } = options;
164
+ return (...args) => {
165
+ const store = options.cache ?? (0, import_signal9.currentResourceCache)();
243
166
  const key = name + ":" + JSON.stringify(args);
244
- if (isHydrating() || typeof window !== "undefined") {
245
- const hydrated = getHydrationData(key);
246
- if (hydrated !== void 0) {
247
- return hydrated;
248
- }
249
- }
250
- if (globalCache.has(key)) {
251
- return globalCache.get(key);
252
- }
253
- if (inflightRequests.has(key)) {
254
- return inflightRequests.get(key);
255
- }
256
- const promise = Promise.resolve(fn(...args)).then(
257
- (val) => {
258
- globalCache.set(key, val);
259
- inflightRequests.delete(key);
260
- if (isServerEnvironment()) {
261
- setHydrationData(key, val);
167
+ const fetchFresh = () => {
168
+ const running = store.inflight.get(key);
169
+ if (running) return running;
170
+ const promise = Promise.resolve(fn(...args)).then(
171
+ (value) => {
172
+ store.set(key, { value, timestamp: Date.now() });
173
+ store.inflight.delete(key);
174
+ return value;
175
+ },
176
+ (error) => {
177
+ store.inflight.delete(key);
178
+ throw error;
262
179
  }
263
- return val;
264
- },
265
- (err) => {
266
- inflightRequests.delete(key);
267
- throw err;
268
- }
269
- );
270
- inflightRequests.set(key, promise);
271
- return promise;
180
+ );
181
+ store.inflight.set(key, promise);
182
+ return promise;
183
+ };
184
+ const hit = store.get(key);
185
+ if (hit && (ttl === Infinity || Date.now() - hit.timestamp <= ttl)) return Promise.resolve(hit.value);
186
+ if (hit && staleWhileRevalidate) {
187
+ fetchFresh().then(reloadMounted, () => void 0);
188
+ return Promise.resolve(hit.value);
189
+ }
190
+ return fetchFresh();
272
191
  };
273
192
  }
274
- function revalidate(key) {
275
- const predicate = typeof key === "string" ? (k) => k.startsWith(key) : key;
276
- for (const k of globalCache.keys()) {
277
- if (predicate(k)) {
278
- globalCache.delete(k);
279
- revalidationKeys.add(k);
280
- }
193
+ function revalidate(key, store) {
194
+ const names = typeof key === "string" ? [key] : Array.isArray(key) ? key : null;
195
+ drop(names ?? key, store ?? (0, import_signal9.currentResourceCache)());
196
+ if (names && !store) tellOtherTabs(names);
197
+ }
198
+ function drop(what, store) {
199
+ const matches = Array.isArray(what) ? (k) => what.some((n) => k === n || k.startsWith(n + ":")) : what;
200
+ for (const k of [...store.keys()]) if (matches(k)) store.delete(k);
201
+ reloadMounted();
202
+ }
203
+ function tellOtherTabs(names) {
204
+ try {
205
+ localStorage.setItem(TABS_KEY, JSON.stringify({ names, at: Date.now() + Math.random() }));
206
+ } catch {
281
207
  }
282
- setRevalidationVersion((v) => v + 1);
283
208
  }
284
209
  function createAsync(fetcher, source) {
285
- const [data] = (0, import_signal9.createResource)(source || (() => true), fetcher);
210
+ const [data] = (0, import_signal8.createResource)(source || (() => true), fetcher);
286
211
  return data;
287
212
  }
288
213
  function getSubmissionSignal(url) {
289
214
  if (!submissionState.has(url)) {
290
215
  submissionState.set(
291
216
  url,
292
- (0, import_signal9.createSignal)(void 0)
217
+ (0, import_signal8.createSignal)(void 0)
293
218
  );
294
219
  }
295
220
  return submissionState.get(url);
296
221
  }
297
- function action(fn, name) {
222
+ function getSubmissionsSignal(url) {
223
+ if (!submissionsState.has(url)) submissionsState.set(url, (0, import_signal8.createSignal)([]));
224
+ return submissionsState.get(url);
225
+ }
226
+ function action(fn, name, options = {}) {
298
227
  const actionUrl = name || fn.name || "action";
299
228
  const submit = async (input) => {
300
229
  const [, setSubmission] = getSubmissionSignal(actionUrl);
301
- const clear = () => setSubmission(void 0);
302
- const retry = () => submit(input);
303
- setSubmission({
230
+ const [, setSubmissions] = getSubmissionsSignal(actionUrl);
231
+ let entry;
232
+ const drop2 = () => setSubmissions((list) => list.filter((s) => s !== entry));
233
+ const settle = (next) => {
234
+ setSubmissions((list) => list.map((s) => s === entry ? next : s));
235
+ entry = next;
236
+ };
237
+ const clear = () => {
238
+ setSubmission(void 0);
239
+ drop2();
240
+ };
241
+ const retry = () => {
242
+ drop2();
243
+ return submit(input);
244
+ };
245
+ entry = {
304
246
  input,
305
247
  // Approximate type for tracking
306
248
  pending: true,
307
249
  clear,
308
250
  retry
309
- });
251
+ };
252
+ setSubmission(entry);
253
+ setSubmissions((list) => [...list, entry]);
310
254
  try {
311
255
  const result = await fn(input);
312
256
  setSubmission({
@@ -316,41 +260,63 @@ function action(fn, name) {
316
260
  clear,
317
261
  retry
318
262
  });
263
+ drop2();
264
+ const stale = typeof options.invalidates === "function" ? options.invalidates(result) : options.invalidates;
265
+ for (const key of stale ?? []) revalidate(key);
319
266
  return result;
320
267
  } catch (error) {
321
- setSubmission({
268
+ const failed = {
322
269
  input,
323
270
  error,
324
271
  pending: false,
325
272
  clear,
326
273
  retry
327
- });
274
+ };
275
+ setSubmission(failed);
276
+ settle(failed);
328
277
  throw error;
329
278
  }
330
279
  };
331
280
  submit.url = actionUrl;
332
- submit.with = (args) => action((...a) => fn(args, ...a), name);
281
+ submit.with = (args) => action((...a) => fn(args, ...a), name, options);
333
282
  return submit;
334
283
  }
335
284
  function useSubmission(actionFn) {
336
285
  const [submission] = getSubmissionSignal(actionFn.url);
337
286
  return submission;
338
287
  }
288
+ function useSubmissions(actionFn) {
289
+ const [submissions] = getSubmissionsSignal(actionFn.url);
290
+ return submissions;
291
+ }
339
292
  function useAction(actionFn) {
340
293
  return actionFn;
341
294
  }
342
- var import_signal9, globalCache, inflightRequests, revalidationVersion, setRevalidationVersion, revalidationKeys, submissionState, RouteDataContext, RouteDataProvider;
295
+ var import_signal8, import_signal9, revalidationListeners, reloadMounted, TABS_KEY, submissionState, submissionsState, RouteDataContext, RouteDataProvider;
343
296
  var init_data = __esm({
344
297
  "src/lib/router/data.ts"() {
345
298
  "use strict";
299
+ import_signal8 = require("@fluixi/reactive/signal");
346
300
  import_signal9 = require("@fluixi/reactive/signal");
347
- init_hydration();
348
301
  init_context();
349
- globalCache = /* @__PURE__ */ new Map();
350
- inflightRequests = /* @__PURE__ */ new Map();
351
- [revalidationVersion, setRevalidationVersion] = (0, import_signal9.createSignal)(0);
352
- revalidationKeys = /* @__PURE__ */ new Set();
302
+ revalidationListeners = /* @__PURE__ */ new Set();
303
+ reloadMounted = () => {
304
+ for (const listener of [...revalidationListeners]) listener();
305
+ };
306
+ TABS_KEY = "fluixi:revalidate";
307
+ if (typeof window !== "undefined") {
308
+ window.addEventListener("storage", (event) => {
309
+ if (event.key !== TABS_KEY || !event.newValue) return;
310
+ try {
311
+ const names = JSON.parse(event.newValue).names;
312
+ if (Array.isArray(names) && names.every((n) => typeof n === "string")) drop(names, (0, import_signal9.currentResourceCache)());
313
+ } catch {
314
+ }
315
+ });
316
+ }
317
+ globalThis[/* @__PURE__ */ Symbol.for("fluixi.revalidate")] = (names) => revalidate(names);
353
318
  submissionState = /* @__PURE__ */ new Map();
319
+ submissionsState = /* @__PURE__ */ new Map();
354
320
  RouteDataContext = createContext3(void 0, "route-data");
355
321
  RouteDataProvider = RouteDataContext.Provider;
356
322
  }
@@ -380,6 +346,7 @@ __export(router_exports, {
380
346
  select: () => select,
381
347
  supportsViewTransitions: () => supportsViewTransitions,
382
348
  useAction: () => useAction,
349
+ useIsRouting: () => useIsRouting,
383
350
  useLevelParams: () => useLevelParams,
384
351
  useLinkNavigation: () => useLinkNavigation,
385
352
  useLocation: () => useLocation,
@@ -391,7 +358,8 @@ __export(router_exports, {
391
358
  useRouteData: () => useRouteData,
392
359
  useRouter: () => useRouter,
393
360
  useSearchParams: () => useSearchParams,
394
- useSubmission: () => useSubmission
361
+ useSubmission: () => useSubmission,
362
+ useSubmissions: () => useSubmissions
395
363
  });
396
364
  function renderLayer(m, depth, base) {
397
365
  let data;
@@ -402,10 +370,32 @@ function renderLayer(m, depth, base) {
402
370
  cancelled = true;
403
371
  });
404
372
  const owner = (0, import_signal10.getOwner)();
405
- const [resource] = (0, import_signal10.createResource)(
406
- () => loader({ params: base.params, location: base.location }, owner, () => cancelled)
407
- );
408
- data = resource;
373
+ const router = (0, import_signal11.useContext)(RouterContext);
374
+ let watching = false;
375
+ const trackDeps = (deps) => {
376
+ if (watching || cancelled) return;
377
+ watching = true;
378
+ (0, import_signal10.runWithOwner)(owner, () => {
379
+ let first = true;
380
+ (0, import_signal10.createEffect)(() => {
381
+ deps();
382
+ if (first) first = false;
383
+ else (0, import_signal10.untrack)(() => void refetch());
384
+ });
385
+ });
386
+ };
387
+ const [resource, { refetch }] = (0, import_signal10.createResource)(() => {
388
+ const result = loader({ params: base.params, location: base.location }, owner, () => cancelled, trackDeps);
389
+ return router ? router.trackLoad(result) : result;
390
+ });
391
+ (0, import_signal10.onCleanup)(onRevalidate(() => void refetch()));
392
+ data = () => {
393
+ const error = resource.error;
394
+ if (error !== void 0 && error !== null) throw error;
395
+ return resource();
396
+ };
397
+ Object.defineProperty(data, "loading", { get: () => resource.loading });
398
+ Object.defineProperty(data, "state", { get: () => resource.state });
409
399
  }
410
400
  const buildPage = () => RouteDataContext2.Provider({
411
401
  value: data,
@@ -423,12 +413,15 @@ function renderLayer(m, depth, base) {
423
413
  });
424
414
  }
425
415
  });
426
- if (!data) return buildPage();
416
+ if (!data) return { view: buildPage(), ready: () => true };
427
417
  const res = data;
428
- const isLoading = () => res.loading;
418
+ const isLoading = () => {
419
+ const state = res.state;
420
+ return state === void 0 ? res.loading : state === "pending" || state === "unresolved";
421
+ };
429
422
  const [ready, setReady] = (0, import_signal10.createSignal)((0, import_signal10.untrack)(() => !isLoading()));
430
423
  (0, import_signal10.createEffect)(() => setReady(!isLoading()));
431
- return (0, import_signal10.createMemo)(() => ready() ? buildPage() : null);
424
+ return { view: (0, import_signal10.createMemo)(() => ready() ? buildPage() : null), ready };
432
425
  }
433
426
  function renderChild(ctx) {
434
427
  const childIdx = ctx.depth + 1;
@@ -439,17 +432,43 @@ function renderChild(ctx) {
439
432
  const c = ctx.matched()[childIdx]?.component;
440
433
  if ((0, import_signal10.untrack)(childComp) !== c) setChildComp(() => c);
441
434
  });
442
- return (0, import_signal10.createMemo)(() => {
435
+ const owner = (0, import_signal10.getOwner)();
436
+ let shown = null;
437
+ let pending = null;
438
+ const drop2 = (layer) => layer?.dispose();
439
+ (0, import_signal10.onCleanup)(() => {
440
+ drop2(shown);
441
+ drop2(pending);
442
+ shown = pending = null;
443
+ });
444
+ const incoming = (0, import_signal10.createMemo)(() => {
443
445
  const component = childComp();
444
- if (!component) {
446
+ if (!component) return null;
447
+ const m = (0, import_signal10.untrack)(() => ctx.matched())[childIdx];
448
+ return (0, import_signal10.runWithOwner)(
449
+ owner,
450
+ () => (0, import_signal10.createRoot)((dispose) => ({
451
+ ...renderLayer(m, childIdx, { matched: ctx.matched, params: ctx.params, location: ctx.location }),
452
+ dispose
453
+ }))
454
+ );
455
+ });
456
+ return (0, import_signal10.createMemo)(() => {
457
+ const next = incoming();
458
+ if (!next) {
459
+ drop2(shown);
460
+ drop2(pending);
461
+ shown = pending = null;
445
462
  return ctx.notFound ? ctx.notFound() : null;
446
463
  }
447
- const m = (0, import_signal10.untrack)(() => ctx.matched())[childIdx];
448
- return renderLayer(m, childIdx, {
449
- matched: ctx.matched,
450
- params: ctx.params,
451
- location: ctx.location
452
- });
464
+ if (next === shown) return shown.view;
465
+ if (pending && pending !== next) drop2(pending);
466
+ pending = next;
467
+ if (shown && !next.ready()) return shown.view;
468
+ drop2(shown);
469
+ shown = next;
470
+ pending = null;
471
+ return shown.view;
453
472
  });
454
473
  }
455
474
  function renderNotFound(notFound) {
@@ -483,6 +502,8 @@ function Router(props) {
483
502
  history
484
503
  });
485
504
  const [state, setState] = (0, import_signal10.createSignal)(router.state());
505
+ const [navigating, setNavigating] = (0, import_signal10.createSignal)(false);
506
+ const [loads, setLoads] = (0, import_signal10.createSignal)(0);
486
507
  let navigationId = 0;
487
508
  const takeMode = () => {
488
509
  const mode = pendingTransitionMode ?? (props.transition ?? "auto");
@@ -492,12 +513,15 @@ function Router(props) {
492
513
  const unsub = router.subscribe((next) => {
493
514
  const id = ++navigationId;
494
515
  const mode = takeMode();
516
+ setNavigating(true);
495
517
  const prepared = prepareComponents(
496
518
  (next.match?.matched ?? []).map((m) => m.component).filter(Boolean)
497
519
  );
498
520
  const commit = () => {
499
521
  setState(next);
500
- return (0, import_signal10.flush)();
522
+ const flushed = (0, import_signal10.flush)();
523
+ if (id === navigationId) setNavigating(false);
524
+ return flushed;
501
525
  };
502
526
  if (!prepared) {
503
527
  void runTransition(mode, commit);
@@ -519,7 +543,18 @@ function Router(props) {
519
543
  const ctx = {
520
544
  router,
521
545
  location: (0, import_signal10.createMemo)(() => state().location),
522
- params: (0, import_signal10.createMemo)(() => state().match?.params ?? {})
546
+ params: (0, import_signal10.createMemo)(() => state().match?.params ?? {}),
547
+ isRouting: () => navigating() || loads() > 0,
548
+ // Counted by the promise, not by an owner: a load a newer navigation superseded still
549
+ // settles and uncounts itself, and a resource seeded from the server never calls it.
550
+ trackLoad(result) {
551
+ if (result && typeof result.finally === "function") {
552
+ setLoads((n) => n + 1);
553
+ void result.finally(() => setLoads((n) => n - 1)).catch(() => {
554
+ });
555
+ }
556
+ return result;
557
+ }
523
558
  };
524
559
  const root = {
525
560
  depth: -1,
@@ -542,6 +577,21 @@ function Outlet() {
542
577
  if (!ctx) return null;
543
578
  return renderChild(ctx);
544
579
  }
580
+ function useIsRouting(options = {}) {
581
+ const routing = (0, import_signal11.useContext)(RouterContext)?.isRouting ?? (() => false);
582
+ const delay = options.delay ?? 0;
583
+ if (delay <= 0) return routing;
584
+ const [late, setLate] = (0, import_signal10.createSignal)(false);
585
+ (0, import_signal10.createEffect)(() => {
586
+ if (!routing()) {
587
+ setLate(false);
588
+ return;
589
+ }
590
+ const timer = setTimeout(() => setLate(true), delay);
591
+ (0, import_signal10.onCleanup)(() => clearTimeout(timer));
592
+ });
593
+ return () => routing() && late();
594
+ }
545
595
  function Redirect(props) {
546
596
  const ctx = (0, import_signal11.useContext)(RouterContext);
547
597
  if (ctx && typeof window !== "undefined") {
@@ -655,18 +705,31 @@ function useRouteData() {
655
705
  }
656
706
  function Link(props) {
657
707
  const ctx = (0, import_signal11.useContext)(RouterContext);
658
- const navigate = () => ctx?.router.navigate(read(props.href) ?? "", { replace: read(props.replace) });
708
+ const navigate = () => ctx.router.navigate(read(props.href) ?? "", { replace: read(props.replace) });
659
709
  const a = (0, import_dom12.createNativeElement)("a");
660
710
  (0, import_dom12.setAttribute)(a, "href", read(props.href) ?? "");
661
711
  if (props.slot) (0, import_dom12.setAttribute)(a, "slot", read(props.slot));
662
712
  for (const k of Object.keys(props)) {
663
713
  if (LINK_OWN_PROPS.has(k)) continue;
664
714
  const v = props[k];
665
- if (v != null && typeof v !== "function") (0, import_dom12.setAttribute)(a, k, String(v));
715
+ if (typeof v === "function") {
716
+ if (/^on[A-Z]/.test(k)) continue;
717
+ const read2 = v;
718
+ const apply = () => {
719
+ const value = read2();
720
+ if (value == null || value === false) a.removeAttribute?.(k);
721
+ else (0, import_dom12.setAttribute)(a, k, String(value));
722
+ };
723
+ if (typeof a.addEventListener === "function") (0, import_signal10.createEffect)(apply);
724
+ else apply();
725
+ continue;
726
+ }
727
+ if (v != null) (0, import_dom12.setAttribute)(a, k, String(v));
666
728
  }
667
729
  if (typeof a.addEventListener === "function") {
668
730
  a.addEventListener("click", (e) => {
669
731
  if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
732
+ if (!ctx) return;
670
733
  e.preventDefault();
671
734
  navigate();
672
735
  });
@@ -737,6 +800,7 @@ var init_router = __esm({
737
800
  import_object = require("@fluixi/utils/object");
738
801
  import_compare = require("@fluixi/utils/compare");
739
802
  import_router = require("@fluixi/router");
803
+ init_data();
740
804
  import_router2 = require("@fluixi/router");
741
805
  init_data();
742
806
  init_lazy();
@@ -750,7 +814,9 @@ var init_router = __esm({
750
814
  router: { navigate: () => {
751
815
  } },
752
816
  location: () => ({ pathname: "", search: "", hash: "", query: {} }),
753
- params: () => ({})
817
+ params: () => ({}),
818
+ isRouting: () => false,
819
+ trackLoad: (result) => result
754
820
  };
755
821
  read = (v) => typeof v === "function" ? v() : v;
756
822
  LINK_OWN_PROPS = /* @__PURE__ */ new Set([
@@ -781,14 +847,72 @@ var import_dom13 = require("@fluixi/dom");
781
847
  var import_dom = require("@fluixi/dom");
782
848
 
783
849
  // src/version.generated.ts
784
- var VERSION = "1.0.0-alpha.92";
850
+ var VERSION = "1.0.0-alpha.94";
785
851
 
786
852
  // src/lib/render/versions.ts
787
853
  var import_dom2 = require("@fluixi/dom");
788
854
  (0, import_dom.registerCoreVersion)(VERSION);
789
855
 
790
- // src/lib/client/index.ts
791
- init_hydration();
856
+ // src/lib/server/hydration.ts
857
+ var import_dom3 = require("@fluixi/dom");
858
+ var hydrating = false;
859
+ var hydrationDataMap = /* @__PURE__ */ new Map();
860
+ function setHydrating(state) {
861
+ hydrating = state;
862
+ }
863
+ function clearHydrationData() {
864
+ hydrationDataMap.clear();
865
+ }
866
+ function deserializeHydrationData() {
867
+ if (typeof window === "undefined") {
868
+ return false;
869
+ }
870
+ const script = document.getElementById("__FLUIXI_HYDRATION__");
871
+ if (!script || script.getAttribute("type") !== "application/json") {
872
+ return false;
873
+ }
874
+ try {
875
+ const json = script.textContent || "{}";
876
+ const data = JSON.parse(json);
877
+ for (const [key, value] of Object.entries(data)) {
878
+ hydrationDataMap.set(key, value);
879
+ }
880
+ return true;
881
+ } catch (error) {
882
+ console.error("Failed to deserialize hydration data:", error);
883
+ return false;
884
+ }
885
+ }
886
+ function cleanupHydration() {
887
+ if (typeof window === "undefined") {
888
+ return;
889
+ }
890
+ const script = document.getElementById("__FLUIXI_HYDRATION__");
891
+ if (script) {
892
+ script.remove();
893
+ }
894
+ const walker = document.createTreeWalker(
895
+ document.body,
896
+ NodeFilter.SHOW_COMMENT,
897
+ null
898
+ );
899
+ const comments = [];
900
+ let node;
901
+ while (node = walker.nextNode()) {
902
+ const comment = node;
903
+ const text = comment.nodeValue || "";
904
+ if (text.startsWith("#") || text.startsWith("/")) {
905
+ comments.push(comment);
906
+ }
907
+ }
908
+ comments.forEach((comment) => comment.remove());
909
+ clearHydrationData();
910
+ }
911
+ if (typeof window !== "undefined") {
912
+ window.addEventListener("load", () => {
913
+ setTimeout(cleanupHydration, 100);
914
+ });
915
+ }
792
916
 
793
917
  // src/lib/render/index.ts
794
918
  var import_dom9 = require("@fluixi/dom");
@@ -921,16 +1045,26 @@ async function startClient(App, options = {}) {
921
1045
  (0, import_head.setActiveHead)(head);
922
1046
  (0, import_head.stripHeadMarker)(container);
923
1047
  if (shouldHydrate) {
924
- if (options.routes && options.routes.length) {
925
- const { preloadRoutes: preloadRoutes2 } = await Promise.resolve().then(() => (init_router(), router_exports));
926
- await preloadRoutes2(options.routes, window.location.pathname);
1048
+ (0, import_dom13.beginAppHydration)();
1049
+ try {
1050
+ if (options.routes && options.routes.length) {
1051
+ const { preloadRoutes: preloadRoutes2 } = await Promise.resolve().then(() => (init_router(), router_exports));
1052
+ await preloadRoutes2(options.routes, window.location.pathname);
1053
+ }
1054
+ deserializeHydrationData();
1055
+ setHydrating(true);
1056
+ (0, import_dom13.hydrate)(rootOf(App), container);
1057
+ replayEvents();
1058
+ } finally {
1059
+ (0, import_dom13.endAppHydration)();
927
1060
  }
928
- deserializeHydrationData();
929
- setHydrating(true);
930
- (0, import_dom13.hydrate)(rootOf(App), container);
931
- replayEvents();
932
1061
  setTimeout(() => {
933
1062
  setHydrating(false);
1063
+ const removed = (0, import_dom13.removeUnclaimedKeyed)(container);
1064
+ if (removed && typeof process !== "undefined" && false) {
1065
+ console.warn(`[fluixi] hydration: removed ${removed} server element(s) the client did not render. Check for markup that depends on client-only state.`);
1066
+ }
1067
+ (0, import_dom13.clearHydrationKeys)();
934
1068
  cleanupHydration();
935
1069
  }, 100);
936
1070
  } else {