@pyreon/router 0.12.3 → 0.12.5

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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"6db74575-1","name":"loader.ts"},{"uid":"6db74575-3","name":"match.ts"},{"uid":"6db74575-5","name":"scroll.ts"},{"uid":"6db74575-7","name":"types.ts"},{"uid":"6db74575-9","name":"router.ts"},{"uid":"6db74575-11","name":"components.tsx"},{"uid":"6db74575-13","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"6db74575-1":{"renderedLength":2855,"gzipLength":1243,"brotliLength":0,"metaUid":"6db74575-0"},"6db74575-3":{"renderedLength":12203,"gzipLength":3691,"brotliLength":0,"metaUid":"6db74575-2"},"6db74575-5":{"renderedLength":1367,"gzipLength":576,"brotliLength":0,"metaUid":"6db74575-4"},"6db74575-7":{"renderedLength":385,"gzipLength":246,"brotliLength":0,"metaUid":"6db74575-6"},"6db74575-9":{"renderedLength":18583,"gzipLength":5270,"brotliLength":0,"metaUid":"6db74575-8"},"6db74575-11":{"renderedLength":6581,"gzipLength":2480,"brotliLength":0,"metaUid":"6db74575-10"},"6db74575-13":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"6db74575-12"}},"nodeMetas":{"6db74575-0":{"id":"/src/loader.ts","moduleParts":{"index.js":"6db74575-1"},"imported":[{"uid":"6db74575-14"}],"importedBy":[{"uid":"6db74575-12"},{"uid":"6db74575-10"}]},"6db74575-2":{"id":"/src/match.ts","moduleParts":{"index.js":"6db74575-3"},"imported":[],"importedBy":[{"uid":"6db74575-12"},{"uid":"6db74575-8"}]},"6db74575-4":{"id":"/src/scroll.ts","moduleParts":{"index.js":"6db74575-5"},"imported":[],"importedBy":[{"uid":"6db74575-8"}]},"6db74575-6":{"id":"/src/types.ts","moduleParts":{"index.js":"6db74575-7"},"imported":[],"importedBy":[{"uid":"6db74575-12"},{"uid":"6db74575-8"}]},"6db74575-8":{"id":"/src/router.ts","moduleParts":{"index.js":"6db74575-9"},"imported":[{"uid":"6db74575-14"},{"uid":"6db74575-15"},{"uid":"6db74575-2"},{"uid":"6db74575-4"},{"uid":"6db74575-6"}],"importedBy":[{"uid":"6db74575-12"},{"uid":"6db74575-10"}]},"6db74575-10":{"id":"/src/components.tsx","moduleParts":{"index.js":"6db74575-11"},"imported":[{"uid":"6db74575-14"},{"uid":"6db74575-0"},{"uid":"6db74575-8"}],"importedBy":[{"uid":"6db74575-12"}]},"6db74575-12":{"id":"/src/index.ts","moduleParts":{"index.js":"6db74575-13"},"imported":[{"uid":"6db74575-10"},{"uid":"6db74575-0"},{"uid":"6db74575-2"},{"uid":"6db74575-8"},{"uid":"6db74575-6"}],"importedBy":[],"isEntry":true},"6db74575-14":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"6db74575-10"},{"uid":"6db74575-0"},{"uid":"6db74575-8"}]},"6db74575-15":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"6db74575-8"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"cec65230-1","name":"loader.ts"},{"uid":"cec65230-3","name":"match.ts"},{"uid":"cec65230-5","name":"scroll.ts"},{"uid":"cec65230-7","name":"types.ts"},{"uid":"cec65230-9","name":"router.ts"},{"uid":"cec65230-11","name":"components.tsx"},{"uid":"cec65230-13","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"cec65230-1":{"renderedLength":2855,"gzipLength":1243,"brotliLength":0,"metaUid":"cec65230-0"},"cec65230-3":{"renderedLength":12203,"gzipLength":3691,"brotliLength":0,"metaUid":"cec65230-2"},"cec65230-5":{"renderedLength":1816,"gzipLength":765,"brotliLength":0,"metaUid":"cec65230-4"},"cec65230-7":{"renderedLength":385,"gzipLength":246,"brotliLength":0,"metaUid":"cec65230-6"},"cec65230-9":{"renderedLength":22377,"gzipLength":6164,"brotliLength":0,"metaUid":"cec65230-8"},"cec65230-11":{"renderedLength":6957,"gzipLength":2564,"brotliLength":0,"metaUid":"cec65230-10"},"cec65230-13":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"cec65230-12"}},"nodeMetas":{"cec65230-0":{"id":"/src/loader.ts","moduleParts":{"index.js":"cec65230-1"},"imported":[{"uid":"cec65230-14"}],"importedBy":[{"uid":"cec65230-12"},{"uid":"cec65230-10"}]},"cec65230-2":{"id":"/src/match.ts","moduleParts":{"index.js":"cec65230-3"},"imported":[],"importedBy":[{"uid":"cec65230-12"},{"uid":"cec65230-8"}]},"cec65230-4":{"id":"/src/scroll.ts","moduleParts":{"index.js":"cec65230-5"},"imported":[],"importedBy":[{"uid":"cec65230-8"}]},"cec65230-6":{"id":"/src/types.ts","moduleParts":{"index.js":"cec65230-7"},"imported":[],"importedBy":[{"uid":"cec65230-12"},{"uid":"cec65230-8"}]},"cec65230-8":{"id":"/src/router.ts","moduleParts":{"index.js":"cec65230-9"},"imported":[{"uid":"cec65230-14"},{"uid":"cec65230-15"},{"uid":"cec65230-2"},{"uid":"cec65230-4"},{"uid":"cec65230-6"}],"importedBy":[{"uid":"cec65230-12"},{"uid":"cec65230-10"}]},"cec65230-10":{"id":"/src/components.tsx","moduleParts":{"index.js":"cec65230-11"},"imported":[{"uid":"cec65230-14"},{"uid":"cec65230-0"},{"uid":"cec65230-8"}],"importedBy":[{"uid":"cec65230-12"}]},"cec65230-12":{"id":"/src/index.ts","moduleParts":{"index.js":"cec65230-13"},"imported":[{"uid":"cec65230-10"},{"uid":"cec65230-0"},{"uid":"cec65230-2"},{"uid":"cec65230-8"},{"uid":"cec65230-6"}],"importedBy":[],"isEntry":true},"cec65230-14":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"cec65230-10"},{"uid":"cec65230-0"},{"uid":"cec65230-8"}]},"cec65230-15":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"cec65230-8"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, createRef, h, onUnmount, provide, useContext } from "@pyreon/core";
1
+ import { ErrorBoundary, createContext, createRef, h, onUnmount, provide, useContext } from "@pyreon/core";
2
2
  import { computed, signal } from "@pyreon/reactivity";
3
3
 
4
4
  //#region src/loader.ts
@@ -519,6 +519,22 @@ var ScrollManager = class {
519
519
  this._applyResult(behavior, to.path);
520
520
  }
521
521
  _applyResult(result, toPath) {
522
+ const hashIdx = toPath.indexOf("#");
523
+ if (hashIdx >= 0) {
524
+ const id = toPath.slice(hashIdx + 1);
525
+ if (id) {
526
+ requestAnimationFrame(() => {
527
+ const el = document.getElementById(id);
528
+ if (el) {
529
+ el.scrollIntoView({ behavior: "smooth" });
530
+ return;
531
+ }
532
+ const namedEl = document.querySelector(`[name="${CSS.escape(id)}"]`);
533
+ if (namedEl) namedEl.scrollIntoView({ behavior: "smooth" });
534
+ });
535
+ return;
536
+ }
537
+ }
522
538
  if (result === "none") return;
523
539
  if (result === "top" || result === void 0) {
524
540
  window.scrollTo({
@@ -702,9 +718,29 @@ function matchSegments(current, pattern, exact) {
702
718
  if (ps.length > cs.length) return false;
703
719
  return ps.every((seg, i) => seg.startsWith(":") || seg === cs[i]);
704
720
  }
721
+ /**
722
+ * Read and write URL search params reactively.
723
+ *
724
+ * @example Basic (untyped)
725
+ * ```ts
726
+ * const [params, setParams] = useSearchParams({ page: "1" })
727
+ * params().page // "1"
728
+ * setParams({ page: "2" }) // updates URL
729
+ * ```
730
+ *
731
+ * @example Typed with schema
732
+ * ```ts
733
+ * const [params, setParams] = useSearchParams({
734
+ * page: 'number',
735
+ * sort: 'string',
736
+ * desc: 'boolean',
737
+ * })
738
+ * params().page // number (auto-coerced)
739
+ * params().desc // boolean
740
+ * ```
741
+ */
705
742
  function useSearchParams(defaults) {
706
- const router = useContext(RouterContext) ?? _activeRouter;
707
- if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
743
+ const router = _getRouter();
708
744
  const get = () => {
709
745
  const query = router.currentRoute().query;
710
746
  if (!defaults) return query;
@@ -723,6 +759,90 @@ function useSearchParams(defaults) {
723
759
  };
724
760
  return [get, set];
725
761
  }
762
+ /**
763
+ * Typed search params with auto-coercion.
764
+ *
765
+ * Schema values define the type: `'string'`, `'number'`, or `'boolean'`.
766
+ * Query string values are automatically coerced to the declared type.
767
+ *
768
+ * @example
769
+ * ```ts
770
+ * const [params, setParams] = useTypedSearchParams({
771
+ * page: 'number',
772
+ * sort: 'string',
773
+ * desc: 'boolean',
774
+ * })
775
+ * params().page // number (coerced from "3" → 3)
776
+ * params().desc // boolean (coerced from "true" → true)
777
+ * setParams({ page: 2 }) // updates URL with ?page=2
778
+ * ```
779
+ */
780
+ function useTypedSearchParams(schema) {
781
+ const router = _getRouter();
782
+ const get = () => {
783
+ const query = router.currentRoute().query;
784
+ const result = {};
785
+ for (const [key, type] of Object.entries(schema)) {
786
+ const raw = query[key];
787
+ if (type === "number") result[key] = raw !== void 0 ? Number(raw) : 0;
788
+ else if (type === "boolean") result[key] = raw === "true" || raw === "1";
789
+ else result[key] = raw ?? "";
790
+ }
791
+ return result;
792
+ };
793
+ const set = (updates) => {
794
+ const current = get();
795
+ const merged = {};
796
+ for (const [k, v] of Object.entries({
797
+ ...current,
798
+ ...updates
799
+ })) merged[k] = String(v);
800
+ const path = router.currentRoute().path + stringifyQuery(merged);
801
+ return router.replace(path);
802
+ };
803
+ return [get, set];
804
+ }
805
+ function _getRouter() {
806
+ const router = useContext(RouterContext) ?? _activeRouter;
807
+ if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
808
+ return router;
809
+ }
810
+ /**
811
+ * Returns true while a navigation is in progress (guards + loaders running).
812
+ * Use this to show loading indicators during route transitions.
813
+ *
814
+ * @example
815
+ * ```tsx
816
+ * const isNavigating = useTransition()
817
+ * <Show when={isNavigating}>
818
+ * <LoadingBar />
819
+ * </Show>
820
+ * ```
821
+ */
822
+ function useTransition() {
823
+ const router = _getRouter();
824
+ return () => router._loadingSignal() > 0;
825
+ }
826
+ /**
827
+ * Read data accumulated by route middleware.
828
+ *
829
+ * @example
830
+ * ```ts
831
+ * // In middleware:
832
+ * const authMiddleware: RouteMiddleware = async (ctx) => {
833
+ * ctx.data.user = await getUser(ctx.to)
834
+ * if (!ctx.data.user) return '/login'
835
+ * }
836
+ *
837
+ * // In component:
838
+ * const data = useMiddlewareData()
839
+ * const user = () => data().user as User
840
+ * ```
841
+ */
842
+ function useMiddlewareData() {
843
+ const router = _getRouter();
844
+ return () => router.currentRoute()._middlewareData ?? {};
845
+ }
726
846
  function createRouter(options) {
727
847
  const opts = Array.isArray(options) ? { routes: options } : options;
728
848
  const { routes, mode = "hash", scrollBehavior, onError, maxCacheSize = 100, trailingSlash = "strip" } = opts;
@@ -866,10 +986,16 @@ function createRouter(options) {
866
986
  }
867
987
  function commitNavigation(path, replace, to, from) {
868
988
  scrollManager.save(from.path);
869
- currentPath.set(path);
870
- syncBrowserUrl(path, replace);
871
- if (_isBrowser && to.meta.title) document.title = to.meta.title;
872
- for (const record of router._loaderData.keys()) if (!to.matched.includes(record)) router._loaderData.delete(record);
989
+ const doCommit = () => {
990
+ currentPath.set(path);
991
+ syncBrowserUrl(path, replace);
992
+ if (_isBrowser && to.meta.title) document.title = to.meta.title;
993
+ for (const record of router._loaderData.keys()) if (!to.matched.includes(record)) router._loaderData.delete(record);
994
+ };
995
+ if (_isBrowser && to.meta.viewTransition !== false && typeof document.startViewTransition === "function") document.startViewTransition(() => {
996
+ doCommit();
997
+ });
998
+ else doCommit();
873
999
  for (const hook of afterHooks) try {
874
1000
  hook(to, from);
875
1001
  } catch (err) {
@@ -884,6 +1010,29 @@ function createRouter(options) {
884
1010
  }
885
1011
  return "continue";
886
1012
  }
1013
+ /** Run per-route middleware chain. Middleware from all matched routes execute in order. */
1014
+ async function runMiddleware(to, from, gen) {
1015
+ const ctx = {
1016
+ to,
1017
+ from,
1018
+ data: {}
1019
+ };
1020
+ for (const record of to.matched) {
1021
+ if (!record.middleware) continue;
1022
+ const mws = Array.isArray(record.middleware) ? record.middleware : [record.middleware];
1023
+ for (const mw of mws) {
1024
+ if (gen !== _navGen) return { action: "cancel" };
1025
+ const result = await mw(ctx);
1026
+ if (result === false) return { action: "cancel" };
1027
+ if (typeof result === "string") return {
1028
+ action: "redirect",
1029
+ target: result
1030
+ };
1031
+ }
1032
+ }
1033
+ to._middlewareData = ctx.data;
1034
+ return { action: "continue" };
1035
+ }
887
1036
  async function navigate(rawPath, replace, redirectDepth = 0) {
888
1037
  if (redirectDepth > 10) {
889
1038
  if (__DEV__) console.warn(`[Pyreon] Navigation to "${rawPath}" aborted: redirect depth exceeded 10 levels. This likely indicates a redirect loop in your route configuration.`);
@@ -903,6 +1052,12 @@ function createRouter(options) {
903
1052
  loadingSignal.update((n) => n - 1);
904
1053
  return;
905
1054
  }
1055
+ const mwResult = await runMiddleware(to, from, gen);
1056
+ if (mwResult.action !== "continue") {
1057
+ loadingSignal.update((n) => n - 1);
1058
+ if (mwResult.action === "redirect") return navigate(sanitizePath(mwResult.target), replace, redirectDepth + 1);
1059
+ return;
1060
+ }
906
1061
  const guardOutcome = await runAllGuards(to, from, gen);
907
1062
  if (guardOutcome.action !== "continue") {
908
1063
  loadingSignal.update((n) => n - 1);
@@ -1217,7 +1372,17 @@ function renderWithLoader(router, record, Comp, route) {
1217
1372
  query: route.query,
1218
1373
  meta: route.meta
1219
1374
  };
1375
+ if (record.errorComponent) return h(ErrorBoundary, {
1376
+ fallback: (error) => h(record.errorComponent, {
1377
+ ...routeProps,
1378
+ error
1379
+ }),
1380
+ children: record.loader ? renderLoaderContent(router, record, Comp, routeProps) : h(Comp, routeProps)
1381
+ });
1220
1382
  if (!record.loader) return h(Comp, routeProps);
1383
+ return renderLoaderContent(router, record, Comp, routeProps);
1384
+ }
1385
+ function renderLoaderContent(router, record, Comp, routeProps) {
1221
1386
  const data = router._loaderData.get(record);
1222
1387
  if (data === void 0 && record.errorComponent) return h(record.errorComponent, routeProps);
1223
1388
  return h(LoaderDataProvider, {
@@ -1264,5 +1429,5 @@ function isStaleChunk(err) {
1264
1429
  }
1265
1430
 
1266
1431
  //#endregion
1267
- export { RouterContext, RouterLink, RouterProvider, RouterView, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useIsActive, useLoaderData, useRoute, useRouter, useSearchParams };
1432
+ export { RouterContext, RouterLink, RouterProvider, RouterView, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useIsActive, useLoaderData, useMiddlewareData, useRoute, useRouter, useSearchParams, useTransition, useTypedSearchParams };
1268
1433
  //# sourceMappingURL=index.js.map