@ossido-labs/ossido-router 0.1.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/esm/components/CriticalCss.d.ts +13 -0
  4. package/dist/esm/components/CriticalCss.js +22 -0
  5. package/dist/esm/components/CriticalCss.js.map +1 -0
  6. package/dist/esm/components/Link.d.ts +30 -0
  7. package/dist/esm/components/Link.js +47 -0
  8. package/dist/esm/components/Link.js.map +1 -0
  9. package/dist/esm/components/Matches.d.ts +7 -0
  10. package/dist/esm/components/Matches.js +22 -0
  11. package/dist/esm/components/Matches.js.map +1 -0
  12. package/dist/esm/components/NotFound.d.ts +5 -0
  13. package/dist/esm/components/NotFound.js +22 -0
  14. package/dist/esm/components/NotFound.js.map +1 -0
  15. package/dist/esm/components/NotFoundDefaultContent.d.ts +7 -0
  16. package/dist/esm/components/NotFoundDefaultContent.js +29 -0
  17. package/dist/esm/components/NotFoundDefaultContent.js.map +1 -0
  18. package/dist/esm/components/OssidoErrorBoundary.d.ts +39 -0
  19. package/dist/esm/components/OssidoErrorBoundary.js +48 -0
  20. package/dist/esm/components/OssidoErrorBoundary.js.map +1 -0
  21. package/dist/esm/components/Redirect.d.ts +11 -0
  22. package/dist/esm/components/Redirect.js +21 -0
  23. package/dist/esm/components/Redirect.js.map +1 -0
  24. package/dist/esm/components/RouteDataLoader.d.ts +21 -0
  25. package/dist/esm/components/RouteDataLoader.js +27 -0
  26. package/dist/esm/components/RouteDataLoader.js.map +1 -0
  27. package/dist/esm/components/RouteMatch.d.ts +15 -0
  28. package/dist/esm/components/RouteMatch.js +81 -0
  29. package/dist/esm/components/RouteMatch.js.map +1 -0
  30. package/dist/esm/components/RouterContext.d.ts +56 -0
  31. package/dist/esm/components/RouterContext.js +32 -0
  32. package/dist/esm/components/RouterContext.js.map +1 -0
  33. package/dist/esm/components/RouterContextProvider.d.ts +10 -0
  34. package/dist/esm/components/RouterContextProvider.js +122 -0
  35. package/dist/esm/components/RouterContextProvider.js.map +1 -0
  36. package/dist/esm/components/RouterProvider.d.ts +15 -0
  37. package/dist/esm/components/RouterProvider.js +28 -0
  38. package/dist/esm/components/RouterProvider.js.map +1 -0
  39. package/dist/esm/data/resourceCache.d.ts +75 -0
  40. package/dist/esm/data/resourceCache.js +148 -0
  41. package/dist/esm/data/resourceCache.js.map +1 -0
  42. package/dist/esm/hooks/useRoute.d.ts +14 -0
  43. package/dist/esm/hooks/useRoute.js +22 -0
  44. package/dist/esm/hooks/useRoute.js.map +1 -0
  45. package/dist/esm/hooks/useRouter.d.ts +47 -0
  46. package/dist/esm/hooks/useRouter.js +49 -0
  47. package/dist/esm/hooks/useRouter.js.map +1 -0
  48. package/dist/esm/hot.d.ts +9 -0
  49. package/dist/esm/hot.js +32 -0
  50. package/dist/esm/hot.js.map +1 -0
  51. package/dist/esm/index.d.ts +9 -0
  52. package/dist/esm/index.js +9 -0
  53. package/dist/esm/route.d.ts +70 -0
  54. package/dist/esm/route.js +50 -0
  55. package/dist/esm/route.js.map +1 -0
  56. package/dist/esm/router.d.ts +27 -0
  57. package/dist/esm/router.js +52 -0
  58. package/dist/esm/router.js.map +1 -0
  59. package/dist/esm/types.d.ts +63 -0
  60. package/dist/esm/utils/from-url-to-parsed-location.d.ts +2 -0
  61. package/dist/esm/utils/from-url-to-parsed-location.js +15 -0
  62. package/dist/esm/utils/from-url-to-parsed-location.js.map +1 -0
  63. package/dist/esm/utils/match-route.d.ts +16 -0
  64. package/dist/esm/utils/match-route.js +72 -0
  65. package/dist/esm/utils/match-route.js.map +1 -0
  66. package/dist/esm/utils/preload-route-chain.d.ts +16 -0
  67. package/dist/esm/utils/preload-route-chain.js +31 -0
  68. package/dist/esm/utils/preload-route-chain.js.map +1 -0
  69. package/dist/esm/utils/view-transition.d.ts +18 -0
  70. package/dist/esm/utils/view-transition.js +35 -0
  71. package/dist/esm/utils/view-transition.js.map +1 -0
  72. package/dist/esm/utils.d.ts +6 -0
  73. package/dist/esm/utils.js +20 -0
  74. package/dist/esm/utils.js.map +1 -0
  75. package/package.json +64 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Schofield
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # ossido-router
2
+
3
+ This package holds the logic related to the client/server react routing.
4
+
5
+ Check [ossido](https://github.com/ossido-labs/ossido) for more.
@@ -0,0 +1,13 @@
1
+ import type { JSX } from 'react';
2
+ import type { Mode } from '../types';
3
+ interface CriticalCssProps {
4
+ routeFilePath?: string;
5
+ mode?: Mode;
6
+ }
7
+ /**
8
+ * Returns the critical CSS for the given route
9
+ * This is required in order to avoid FOUC during development
10
+ * since vite does not support CSS injection without JS waterfall
11
+ */
12
+ export declare function CriticalCss({ routeFilePath, mode, }: CriticalCssProps): JSX.Element | null;
13
+ export {};
@@ -0,0 +1,22 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+
3
+ //#region src/components/CriticalCss.tsx
4
+ const CRITICAL_CSS_PATH = "/vite-server/ossido_internal__critical_css";
5
+ const CRITICAL_CSS_ENABLED = typeof __OSSIDO_CRITICAL_CSS__ === "undefined" || __OSSIDO_CRITICAL_CSS__;
6
+ /**
7
+ * Returns the critical CSS for the given route
8
+ * This is required in order to avoid FOUC during development
9
+ * since vite does not support CSS injection without JS waterfall
10
+ */
11
+ function CriticalCss({ routeFilePath, mode }) {
12
+ if (!CRITICAL_CSS_ENABLED || !routeFilePath || mode !== "Dev") return null;
13
+ return /* @__PURE__ */ jsx("link", {
14
+ href: `${CRITICAL_CSS_PATH}?componentId=${routeFilePath}`,
15
+ precedence: "high",
16
+ rel: "stylesheet"
17
+ });
18
+ }
19
+
20
+ //#endregion
21
+ export { CriticalCss };
22
+ //# sourceMappingURL=CriticalCss.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CriticalCss.js","names":[],"sources":["../../../src/components/CriticalCss.tsx"],"sourcesContent":["import type { JSX } from 'react';\n\nimport type { Mode } from '../types';\n\nconst VITE_PROXY_PATH = '/vite-server';\nconst CRITICAL_CSS_PATH = VITE_PROXY_PATH + '/ossido_internal__critical_css';\n\n/**\n * Baked in by the vite `define` in ossido's build config, mirroring the\n * `dev.criticalCss` option. When the user disables critical CSS the vite\n * endpoint is disabled too, so the links must not render (they would be dead,\n * render-blocking requests). The `typeof` guard keeps this safe under\n * vitest/tsdown where the define is not applied.\n */\ndeclare const __OSSIDO_CRITICAL_CSS__: boolean;\nconst CRITICAL_CSS_ENABLED =\n typeof __OSSIDO_CRITICAL_CSS__ === 'undefined' || __OSSIDO_CRITICAL_CSS__;\n\ninterface CriticalCssProps {\n routeFilePath?: string;\n mode?: Mode;\n}\n\n/**\n * Returns the critical CSS for the given route\n * This is required in order to avoid FOUC during development\n * since vite does not support CSS injection without JS waterfall\n */\nexport function CriticalCss({\n routeFilePath,\n mode,\n}: CriticalCssProps): JSX.Element | null {\n if (!CRITICAL_CSS_ENABLED || !routeFilePath || mode !== 'Dev') {\n return null;\n }\n\n return (\n <link\n href={`${CRITICAL_CSS_PATH}?componentId=${routeFilePath}`}\n precedence=\"high\"\n rel=\"stylesheet\"\n />\n );\n}\n"],"mappings":";;;AAKA,MAAM,oBAAoB;AAU1B,MAAM,uBACJ,OAAO,4BAA4B,eAAe;;;;;;AAYpD,SAAgB,YAAY,EAC1B,eACA,QACuC;CACvC,IAAI,CAAC,wBAAwB,CAAC,iBAAiB,SAAS,OACtD,OAAO;CAGT,OACE,oBAAC,QAAD;EACE,MAAM,GAAG,kBAAkB,eAAe;EAC1C,YAAW;EACX,KAAI;CACL;AAEL"}
@@ -0,0 +1,30 @@
1
+ import type { JSX, Ref } from 'react';
2
+ interface OssidoLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
3
+ /**
4
+ * If "true" the route gets loaded when the link enters the viewport.
5
+ * @default true
6
+ */
7
+ preload?: boolean;
8
+ /**
9
+ * If "false" the scroll offset will be kept across page navigation.
10
+ * @default true
11
+ */
12
+ scroll?: boolean;
13
+ /**
14
+ * If "true" the history entry will be replaced instead of pushed.
15
+ * @default false
16
+ */
17
+ replace?: boolean;
18
+ /**
19
+ * Override the app's `viewTransitions` config for this link: `true` forces a
20
+ * View Transition, `false` skips it. Defaults to the config value.
21
+ */
22
+ viewTransition?: boolean;
23
+ /**
24
+ * Forwarded to the underlying `<a>`, merged with the internal preload ref.
25
+ * Lets wrappers (e.g. react-aria's `render`) connect their own ref.
26
+ */
27
+ ref?: Ref<HTMLAnchorElement>;
28
+ }
29
+ export declare function Link(componentProps: OssidoLinkProps): JSX.Element;
30
+ export {};
@@ -0,0 +1,47 @@
1
+ import { useRoute } from "../hooks/useRoute.js";
2
+ import { useRouter } from "../hooks/useRouter.js";
3
+ import { useCallback } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { useInView } from "react-intersection-observer";
6
+
7
+ //#region src/components/Link.tsx
8
+ function isEventModifierKeyActiveAndTargetDifferentFromSelf(event) {
9
+ const target = event.currentTarget.getAttribute("target");
10
+ return target && target !== "_self" || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
11
+ }
12
+ function Link(componentProps) {
13
+ const { preload = true, scroll = true, children, href, replace, viewTransition, onClick, ref: forwardedRef, ...rest } = componentProps;
14
+ const router = useRouter();
15
+ const route = useRoute(href);
16
+ const { ref: inViewRef } = useInView({
17
+ onChange(inView) {
18
+ if (inView && preload) route?.component.preload?.();
19
+ },
20
+ triggerOnce: true
21
+ });
22
+ const setRef = useCallback((node) => {
23
+ inViewRef(node);
24
+ if (typeof forwardedRef === "function") forwardedRef(node);
25
+ else if (forwardedRef) forwardedRef.current = node;
26
+ }, [inViewRef, forwardedRef]);
27
+ const handleTransition = (event) => {
28
+ onClick?.(event);
29
+ if (href?.startsWith("#") || isEventModifierKeyActiveAndTargetDifferentFromSelf(event)) return;
30
+ event.preventDefault();
31
+ router[replace ? "replace" : "push"](href || "", {
32
+ scroll,
33
+ viewTransition
34
+ });
35
+ };
36
+ return /* @__PURE__ */ jsx("a", {
37
+ ...rest,
38
+ href,
39
+ ref: setRef,
40
+ onClick: handleTransition,
41
+ children
42
+ });
43
+ }
44
+
45
+ //#endregion
46
+ export { Link };
47
+ //# sourceMappingURL=Link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Link.js","names":[],"sources":["../../../src/components/Link.tsx"],"sourcesContent":["import type { JSX, MouseEvent, Ref } from 'react';\nimport { useCallback } from 'react';\nimport { useInView } from 'react-intersection-observer';\n\nimport { useRouter } from '../hooks/useRouter';\nimport { useRoute } from '../hooks/useRoute';\n\ninterface OssidoLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n /**\n * If \"true\" the route gets loaded when the link enters the viewport.\n * @default true\n */\n preload?: boolean;\n\n /**\n * If \"false\" the scroll offset will be kept across page navigation.\n * @default true\n */\n scroll?: boolean;\n\n /**\n * If \"true\" the history entry will be replaced instead of pushed.\n * @default false\n */\n replace?: boolean;\n\n /**\n * Override the app's `viewTransitions` config for this link: `true` forces a\n * View Transition, `false` skips it. Defaults to the config value.\n */\n viewTransition?: boolean;\n\n /**\n * Forwarded to the underlying `<a>`, merged with the internal preload ref.\n * Lets wrappers (e.g. react-aria's `render`) connect their own ref.\n */\n ref?: Ref<HTMLAnchorElement>;\n}\n\nfunction isEventModifierKeyActiveAndTargetDifferentFromSelf(\n event: MouseEvent<HTMLAnchorElement>,\n): boolean {\n const target = event.currentTarget.getAttribute('target');\n return (\n (target && target !== '_self') ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey // triggers resource download\n );\n}\n\nexport function Link(componentProps: OssidoLinkProps): JSX.Element {\n const {\n preload = true,\n scroll = true,\n children,\n href,\n replace,\n viewTransition,\n onClick,\n ref: forwardedRef,\n ...rest\n } = componentProps;\n\n const router = useRouter();\n const route = useRoute(href);\n const { ref: inViewRef } = useInView({\n onChange(inView) {\n if (inView && preload) route?.component.preload?.();\n },\n triggerOnce: true,\n });\n\n // Merge the caller's ref (e.g. from react-aria's `render`) with the internal\n // in-view ref, so both receive the underlying `<a>` node. Memoised so the\n // callback ref identity is stable across renders.\n const setRef = useCallback(\n (node: HTMLAnchorElement | null): void => {\n inViewRef(node);\n if (typeof forwardedRef === 'function') {\n forwardedRef(node);\n } else if (forwardedRef) {\n forwardedRef.current = node;\n }\n },\n [inViewRef, forwardedRef],\n );\n\n const handleTransition: React.MouseEventHandler<HTMLAnchorElement> = (\n event,\n ) => {\n onClick?.(event);\n\n if (\n href?.startsWith('#') ||\n // If the user is pressing a modifier key or using the target attribute,\n // we fall back to default behaviour of `a` tag\n isEventModifierKeyActiveAndTargetDifferentFromSelf(event)\n ) {\n return;\n }\n\n event.preventDefault();\n\n const method = replace ? 'replace' : 'push';\n\n router[method](href || '', { scroll, viewTransition });\n };\n\n return (\n <a {...rest} href={href} ref={setRef} onClick={handleTransition}>\n {children}\n </a>\n );\n}\n"],"mappings":";;;;;;;AAuCA,SAAS,mDACP,OACS;CACT,MAAM,SAAS,MAAM,cAAc,aAAa,QAAQ;CACxD,OACG,UAAU,WAAW,WACtB,MAAM,WACN,MAAM,WACN,MAAM,YACN,MAAM;AAEV;AAEA,SAAgB,KAAK,gBAA8C;CACjE,MAAM,EACJ,UAAU,MACV,SAAS,MACT,UACA,MACA,SACA,gBACA,SACA,KAAK,cACL,GAAG,SACD;CAEJ,MAAM,SAAS,UAAU;CACzB,MAAM,QAAQ,SAAS,IAAI;CAC3B,MAAM,EAAE,KAAK,cAAc,UAAU;EACnC,SAAS,QAAQ;GACf,IAAI,UAAU,SAAS,OAAO,UAAU,UAAU;EACpD;EACA,aAAa;CACf,CAAC;CAKD,MAAM,SAAS,aACZ,SAAyC;EACxC,UAAU,IAAI;EACd,IAAI,OAAO,iBAAiB,YAC1B,aAAa,IAAI;OACZ,IAAI,cACT,aAAa,UAAU;CAE3B,GACA,CAAC,WAAW,YAAY,CAC1B;CAEA,MAAM,oBACJ,UACG;EACH,UAAU,KAAK;EAEf,IACE,MAAM,WAAW,GAAG,KAGpB,mDAAmD,KAAK,GAExD;EAGF,MAAM,eAAe;EAIrB,OAFe,UAAU,YAAY,OAEvB,CAAC,QAAQ,IAAI;GAAE;GAAQ;EAAe,CAAC;CACvD;CAEA,OACE,oBAAC,KAAD;EAAG,GAAI;EAAY;EAAM,KAAK;EAAQ,SAAS;EAC5C;CACA;AAEP"}
@@ -0,0 +1,7 @@
1
+ import type { JSX } from 'react';
2
+ import type { Mode } from '../types';
3
+ interface MatchesProps {
4
+ mode?: Mode;
5
+ }
6
+ export declare function Matches({ mode }: MatchesProps): JSX.Element;
7
+ export {};
@@ -0,0 +1,22 @@
1
+ import { useRouterContext } from "./RouterContext.js";
2
+ import { useRoute } from "../hooks/useRoute.js";
3
+ import { useRouteHotVersion } from "../hot.js";
4
+ import { RouteMatch } from "./RouteMatch.js";
5
+ import { NotFound } from "./NotFound.js";
6
+ import { jsx } from "react/jsx-runtime";
7
+
8
+ //#region src/components/Matches.tsx
9
+ function Matches({ mode }) {
10
+ const { location } = useRouterContext();
11
+ useRouteHotVersion();
12
+ const route = useRoute(location.pathname);
13
+ if (!route) return /* @__PURE__ */ jsx(NotFound, { mode });
14
+ return /* @__PURE__ */ jsx(RouteMatch, {
15
+ route,
16
+ mode
17
+ });
18
+ }
19
+
20
+ //#endregion
21
+ export { Matches };
22
+ //# sourceMappingURL=Matches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Matches.js","names":[],"sources":["../../../src/components/Matches.tsx"],"sourcesContent":["import type { JSX } from 'react';\n\nimport { useRoute } from '../hooks/useRoute';\n\nimport type { Mode } from '../types';\nimport { useRouteHotVersion } from '../hot';\n\nimport { RouteMatch } from './RouteMatch';\nimport { NotFound } from './NotFound';\nimport { useRouterContext } from './RouterContext';\n\ninterface MatchesProps {\n mode?: Mode;\n}\n\nexport function Matches({ mode }: MatchesProps): JSX.Element {\n const { location } = useRouterContext();\n\n // Dev-only: re-render the whole match tree when a route component is\n // hot-swapped (see `../hot`). Inert in prod — the store is never bumped.\n useRouteHotVersion();\n\n const route = useRoute(location.pathname);\n\n if (!route) {\n return <NotFound mode={mode} />;\n }\n\n return <RouteMatch route={route} mode={mode} />;\n}\n"],"mappings":";;;;;;;;AAeA,SAAgB,QAAQ,EAAE,QAAmC;CAC3D,MAAM,EAAE,aAAa,iBAAiB;CAItC,mBAAmB;CAEnB,MAAM,QAAQ,SAAS,SAAS,QAAQ;CAExC,IAAI,CAAC,OACH,OAAO,oBAAC,UAAD,EAAgB,KAAO;CAGhC,OAAO,oBAAC,YAAD;EAAmB;EAAa;CAAO;AAChD"}
@@ -0,0 +1,5 @@
1
+ import type { JSX } from 'react';
2
+ import type { Mode } from '../types';
3
+ export declare function NotFound({ mode }: {
4
+ mode?: Mode;
5
+ }): JSX.Element | null;
@@ -0,0 +1,22 @@
1
+ import { useRouterContext } from "./RouterContext.js";
2
+ import { CriticalCss } from "./CriticalCss.js";
3
+ import { ROOT_ROUTE_ID } from "../route.js";
4
+ import { NotFoundDefaultContent } from "./NotFoundDefaultContent.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+
7
+ //#region src/components/NotFound.tsx
8
+ function NotFound({ mode }) {
9
+ const { router } = useRouterContext();
10
+ const rootRoute = router.routesById[ROOT_ROUTE_ID];
11
+ const RootLayout = rootRoute?.component;
12
+ if (!RootLayout) return null;
13
+ const CustomNotFound = rootRoute?.options.notFoundComponent;
14
+ return /* @__PURE__ */ jsxs(RootLayout, { children: [/* @__PURE__ */ jsx(CriticalCss, {
15
+ routeFilePath: "__root__",
16
+ mode
17
+ }), CustomNotFound ? /* @__PURE__ */ jsx(CustomNotFound, {}) : /* @__PURE__ */ jsx(NotFoundDefaultContent, {})] });
18
+ }
19
+
20
+ //#endregion
21
+ export { NotFound };
22
+ //# sourceMappingURL=NotFound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NotFound.js","names":[],"sources":["../../../src/components/NotFound.tsx"],"sourcesContent":["import type { JSX } from 'react';\n\nimport { useRouterContext } from '../components/RouterContext';\nimport { ROOT_ROUTE_ID } from '../route';\n\nimport type { Mode } from '../types';\n\nimport { NotFoundDefaultContent } from './NotFoundDefaultContent';\nimport { CriticalCss } from './CriticalCss';\n\nexport function NotFound({ mode }: { mode?: Mode }): JSX.Element | null {\n const { router } = useRouterContext();\n\n const rootRoute = router.routesById[ROOT_ROUTE_ID];\n const RootLayout = rootRoute?.component;\n\n if (!RootLayout) return null;\n\n // The global not-found is the root `not-found.tsx`, rendered inside the root\n // layout (Next.js semantics). Falls back to the framework default.\n const CustomNotFound = rootRoute?.options.notFoundComponent;\n\n return (\n <RootLayout>\n <CriticalCss routeFilePath=\"__root__\" mode={mode} />\n {CustomNotFound ? <CustomNotFound /> : <NotFoundDefaultContent />}\n </RootLayout>\n );\n}\n"],"mappings":";;;;;;;AAUA,SAAgB,SAAS,EAAE,QAA6C;CACtE,MAAM,EAAE,WAAW,iBAAiB;CAEpC,MAAM,YAAY,OAAO,WAAW;CACpC,MAAM,aAAa,WAAW;CAE9B,IAAI,CAAC,YAAY,OAAO;CAIxB,MAAM,iBAAiB,WAAW,QAAQ;CAE1C,OACE,qBAAC,YAAD,aACE,oBAAC,aAAD;EAAa,eAAc;EAAiB;CAAO,IAClD,iBAAiB,oBAAC,gBAAD,CAAiB,KAAI,oBAAC,wBAAD,CAAyB,EACtD;AAEhB"}
@@ -0,0 +1,7 @@
1
+ import type { JSX } from 'react';
2
+ /**
3
+ * Framework default 404 page, shown when no route matches and the app provides
4
+ * no `/404` route. Shares the {@link DefaultScreen} shell with
5
+ * {@link DefaultError} so the two default screens look consistent.
6
+ */
7
+ export declare function NotFoundDefaultContent(): JSX.Element;
@@ -0,0 +1,29 @@
1
+ import { Link } from "./Link.js";
2
+ import { DefaultScreen } from "@ossido-labs/ossido-ui";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+
5
+ //#region src/components/NotFoundDefaultContent.tsx
6
+ /**
7
+ * Framework default 404 page, shown when no route matches and the app provides
8
+ * no `/404` route. Shares the {@link DefaultScreen} shell with
9
+ * {@link DefaultError} so the two default screens look consistent.
10
+ */
11
+ function NotFoundDefaultContent() {
12
+ return /* @__PURE__ */ jsxs(DefaultScreen, {
13
+ role: "status",
14
+ badge: "404",
15
+ title: "Page not found",
16
+ children: [/* @__PURE__ */ jsx("p", {
17
+ className: "ossido-screen-text",
18
+ children: "The page you’re looking for doesn’t exist or may have moved."
19
+ }), /* @__PURE__ */ jsx(Link, {
20
+ href: "/",
21
+ className: "ossido-screen-action",
22
+ children: "Return to homepage"
23
+ })]
24
+ });
25
+ }
26
+
27
+ //#endregion
28
+ export { NotFoundDefaultContent };
29
+ //# sourceMappingURL=NotFoundDefaultContent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NotFoundDefaultContent.js","names":[],"sources":["../../../src/components/NotFoundDefaultContent.tsx"],"sourcesContent":["import type { JSX } from 'react';\nimport { DefaultScreen } from '@ossido-labs/ossido-ui';\n\nimport { Link } from './Link';\n\n/**\n * Framework default 404 page, shown when no route matches and the app provides\n * no `/404` route. Shares the {@link DefaultScreen} shell with\n * {@link DefaultError} so the two default screens look consistent.\n */\nexport function NotFoundDefaultContent(): JSX.Element {\n return (\n <DefaultScreen role=\"status\" badge=\"404\" title=\"Page not found\">\n <p className=\"ossido-screen-text\">\n The page you’re looking for doesn’t exist or may have moved.\n </p>\n <Link href=\"/\" className=\"ossido-screen-action\">\n Return to homepage\n </Link>\n </DefaultScreen>\n );\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,yBAAsC;CACpD,OACE,qBAAC,eAAD;EAAe,MAAK;EAAS,OAAM;EAAM,OAAM;YAA/C,CACE,oBAAC,KAAD;GAAG,WAAU;aAAqB;EAE/B,IACH,oBAAC,MAAD;GAAM,MAAK;GAAI,WAAU;aAAuB;EAE1C,EACO;;AAEnB"}
@@ -0,0 +1,39 @@
1
+ import { Component } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import type { ErrorComponent } from '../types';
4
+ interface OssidoErrorBoundaryProps {
5
+ fallback: ErrorComponent;
6
+ /**
7
+ * Changes per navigation/retry (the route's data-resource key). When it
8
+ * changes the boundary clears any caught error and re-renders its children —
9
+ * a reset without remounting. Not remounting matters: the surrounding
10
+ * `<Suspense>` stays mounted, so a navigation (whose data + code are already
11
+ * prefetched) swaps straight to the new page instead of flashing the fallback.
12
+ */
13
+ resetKey: string;
14
+ /**
15
+ * Called by the fallback's `reset`. It bumps the navigation id (which changes
16
+ * `resetKey`), clearing the error and creating a fresh data resource.
17
+ */
18
+ onReset: () => void;
19
+ children: ReactNode;
20
+ }
21
+ interface OssidoErrorBoundaryState {
22
+ error: Error | null;
23
+ resetKey: string;
24
+ }
25
+ /**
26
+ * Renders the nearest `error.tsx` (or the framework default) when a descendant
27
+ * throws — including a rejected data resource surfaced through `use()`.
28
+ *
29
+ * Resets via {@link OssidoErrorBoundaryProps.resetKey} rather than a `key` from
30
+ * the caller, so its children re-render (not remount) on navigation.
31
+ */
32
+ export declare class OssidoErrorBoundary extends Component<OssidoErrorBoundaryProps, OssidoErrorBoundaryState> {
33
+ state: OssidoErrorBoundaryState;
34
+ static getDerivedStateFromError(error: Error): Partial<OssidoErrorBoundaryState>;
35
+ static getDerivedStateFromProps(props: OssidoErrorBoundaryProps, state: OssidoErrorBoundaryState): Partial<OssidoErrorBoundaryState> | null;
36
+ reset: () => void;
37
+ render(): ReactNode;
38
+ }
39
+ export {};
@@ -0,0 +1,48 @@
1
+ import { Component } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+
4
+ //#region src/components/OssidoErrorBoundary.tsx
5
+ /**
6
+ * Renders the nearest `error.tsx` (or the framework default) when a descendant
7
+ * throws — including a rejected data resource surfaced through `use()`.
8
+ *
9
+ * Resets via {@link OssidoErrorBoundaryProps.resetKey} rather than a `key` from
10
+ * the caller, so its children re-render (not remount) on navigation.
11
+ */
12
+ var OssidoErrorBoundary = class extends Component {
13
+ constructor(..._args) {
14
+ super(..._args);
15
+ this.state = {
16
+ error: null,
17
+ resetKey: this.props.resetKey
18
+ };
19
+ this.reset = () => {
20
+ this.props.onReset();
21
+ };
22
+ }
23
+ static getDerivedStateFromError(error) {
24
+ return { error };
25
+ }
26
+ static getDerivedStateFromProps(props, state) {
27
+ if (props.resetKey !== state.resetKey) return {
28
+ error: null,
29
+ resetKey: props.resetKey
30
+ };
31
+ return null;
32
+ }
33
+ render() {
34
+ const { error } = this.state;
35
+ if (error) {
36
+ const Fallback = this.props.fallback;
37
+ return /* @__PURE__ */ jsx(Fallback, {
38
+ error,
39
+ reset: this.reset
40
+ });
41
+ }
42
+ return this.props.children;
43
+ }
44
+ };
45
+
46
+ //#endregion
47
+ export { OssidoErrorBoundary };
48
+ //# sourceMappingURL=OssidoErrorBoundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OssidoErrorBoundary.js","names":[],"sources":["../../../src/components/OssidoErrorBoundary.tsx"],"sourcesContent":["import { Component } from 'react';\nimport type { ReactNode } from 'react';\n\nimport type { ErrorComponent } from '../types';\n\ninterface OssidoErrorBoundaryProps {\n fallback: ErrorComponent;\n /**\n * Changes per navigation/retry (the route's data-resource key). When it\n * changes the boundary clears any caught error and re-renders its children —\n * a reset without remounting. Not remounting matters: the surrounding\n * `<Suspense>` stays mounted, so a navigation (whose data + code are already\n * prefetched) swaps straight to the new page instead of flashing the fallback.\n */\n resetKey: string;\n /**\n * Called by the fallback's `reset`. It bumps the navigation id (which changes\n * `resetKey`), clearing the error and creating a fresh data resource.\n */\n onReset: () => void;\n children: ReactNode;\n}\n\ninterface OssidoErrorBoundaryState {\n error: Error | null;\n resetKey: string;\n}\n\n/**\n * Renders the nearest `error.tsx` (or the framework default) when a descendant\n * throws — including a rejected data resource surfaced through `use()`.\n *\n * Resets via {@link OssidoErrorBoundaryProps.resetKey} rather than a `key` from\n * the caller, so its children re-render (not remount) on navigation.\n */\nexport class OssidoErrorBoundary extends Component<\n OssidoErrorBoundaryProps,\n OssidoErrorBoundaryState\n> {\n state: OssidoErrorBoundaryState = {\n error: null,\n resetKey: this.props.resetKey,\n };\n\n static getDerivedStateFromError(\n error: Error,\n ): Partial<OssidoErrorBoundaryState> {\n return { error };\n }\n\n static getDerivedStateFromProps(\n props: OssidoErrorBoundaryProps,\n state: OssidoErrorBoundaryState,\n ): Partial<OssidoErrorBoundaryState> | null {\n // A new navigation/retry: drop the caught error and try the children again.\n if (props.resetKey !== state.resetKey) {\n return { error: null, resetKey: props.resetKey };\n }\n return null;\n }\n\n reset = (): void => {\n // Bumps the navigation id → `resetKey` changes → the error is cleared and a\n // fresh data resource is created (a refetch).\n this.props.onReset();\n };\n\n render(): ReactNode {\n const { error } = this.state;\n if (error) {\n const Fallback = this.props.fallback;\n return <Fallback error={error} reset={this.reset} />;\n }\n return this.props.children;\n }\n}\n"],"mappings":";;;;;;;;;;;AAmCA,IAAa,sBAAb,cAAyC,UAGvC;;;eACkC;GAChC,OAAO;GACP,UAAU,KAAK,MAAM;EACvB;qBAmBoB;GAGlB,KAAK,MAAM,QAAQ;EACrB;;CArBA,OAAO,yBACL,OACmC;EACnC,OAAO,EAAE,MAAM;CACjB;CAEA,OAAO,yBACL,OACA,OAC0C;EAE1C,IAAI,MAAM,aAAa,MAAM,UAC3B,OAAO;GAAE,OAAO;GAAM,UAAU,MAAM;EAAS;EAEjD,OAAO;CACT;CAQA,SAAoB;EAClB,MAAM,EAAE,UAAU,KAAK;EACvB,IAAI,OAAO;GACT,MAAM,WAAW,KAAK,MAAM;GAC5B,OAAO,oBAAC,UAAD;IAAiB;IAAO,OAAO,KAAK;GAAQ;EACrD;EACA,OAAO,KAAK,MAAM;CACpB;AACF"}
@@ -0,0 +1,11 @@
1
+ interface RedirectProps {
2
+ to: string;
3
+ }
4
+ /**
5
+ * Performs a client-side redirect (returned from a route's data load) in an
6
+ * effect — never during render, since navigation is a side effect and the app
7
+ * renders under `StrictMode`. Uses `replace` so the redirecting URL does not
8
+ * become a back-button trap.
9
+ */
10
+ export declare function Redirect({ to }: RedirectProps): null;
11
+ export {};
@@ -0,0 +1,21 @@
1
+ import { useRouter } from "../hooks/useRouter.js";
2
+ import { useEffect } from "react";
3
+
4
+ //#region src/components/Redirect.tsx
5
+ /**
6
+ * Performs a client-side redirect (returned from a route's data load) in an
7
+ * effect — never during render, since navigation is a side effect and the app
8
+ * renders under `StrictMode`. Uses `replace` so the redirecting URL does not
9
+ * become a back-button trap.
10
+ */
11
+ function Redirect({ to }) {
12
+ const { replace } = useRouter();
13
+ useEffect(() => {
14
+ replace(to);
15
+ }, [replace, to]);
16
+ return null;
17
+ }
18
+
19
+ //#endregion
20
+ export { Redirect };
21
+ //# sourceMappingURL=Redirect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Redirect.js","names":[],"sources":["../../../src/components/Redirect.tsx"],"sourcesContent":["import { useEffect } from 'react';\n\nimport { useRouter } from '../hooks/useRouter';\n\ninterface RedirectProps {\n to: string;\n}\n\n/**\n * Performs a client-side redirect (returned from a route's data load) in an\n * effect — never during render, since navigation is a side effect and the app\n * renders under `StrictMode`. Uses `replace` so the redirecting URL does not\n * become a back-button trap.\n */\nexport function Redirect({ to }: RedirectProps): null {\n const { replace } = useRouter();\n\n useEffect(() => {\n replace(to);\n }, [replace, to]);\n\n return null;\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAgB,SAAS,EAAE,MAA2B;CACpD,MAAM,EAAE,YAAY,UAAU;CAE9B,gBAAgB;EACd,QAAQ,EAAE;CACZ,GAAG,CAAC,SAAS,EAAE,CAAC;CAEhB,OAAO;AACT"}
@@ -0,0 +1,21 @@
1
+ import type { JSX } from 'react';
2
+ import type { Route } from '../route';
3
+ import type { ParsedLocation } from './RouterContext';
4
+ interface RouteDataLoaderProps {
5
+ route: Route;
6
+ resourceKey: string;
7
+ location: ParsedLocation;
8
+ }
9
+ /**
10
+ * Reads the route's server data resource via `use()`. On the initial render the
11
+ * resource is pre-seeded (SSR data) so this resolves synchronously; on client
12
+ * navigation it suspends until the fetch settles, showing the `<Suspense>`
13
+ * fallback (`loading.tsx`). A rejected resource is re-thrown by `use()` and
14
+ * caught by the surrounding error boundary (`error.tsx`).
15
+ *
16
+ * The route's critical CSS is intentionally rendered by `RouteMatch` OUTSIDE
17
+ * the `<Suspense>` boundary (see the note there) — a `precedence` stylesheet
18
+ * inside the boundary would defer its streamed reveal.
19
+ */
20
+ export declare function RouteDataLoader({ route, resourceKey, location, }: RouteDataLoaderProps): JSX.Element;
21
+ export {};
@@ -0,0 +1,27 @@
1
+ import { getOrCreateResource } from "../data/resourceCache.js";
2
+ import { Redirect } from "./Redirect.js";
3
+ import { use } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+
6
+ //#region src/components/RouteDataLoader.tsx
7
+ /**
8
+ * Reads the route's server data resource via `use()`. On the initial render the
9
+ * resource is pre-seeded (SSR data) so this resolves synchronously; on client
10
+ * navigation it suspends until the fetch settles, showing the `<Suspense>`
11
+ * fallback (`loading.tsx`). A rejected resource is re-thrown by `use()` and
12
+ * caught by the surrounding error boundary (`error.tsx`).
13
+ *
14
+ * The route's critical CSS is intentionally rendered by `RouteMatch` OUTSIDE
15
+ * the `<Suspense>` boundary (see the note there) — a `precedence` stylesheet
16
+ * inside the boundary would defer its streamed reveal.
17
+ */
18
+ function RouteDataLoader({ route, resourceKey, location }) {
19
+ const result = use(getOrCreateResource(resourceKey, route, location));
20
+ if (result.kind === "redirect") return /* @__PURE__ */ jsx(Redirect, { to: result.destination });
21
+ const Component = route.component;
22
+ return /* @__PURE__ */ jsx(Component, { ...result.props });
23
+ }
24
+
25
+ //#endregion
26
+ export { RouteDataLoader };
27
+ //# sourceMappingURL=RouteDataLoader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouteDataLoader.js","names":[],"sources":["../../../src/components/RouteDataLoader.tsx"],"sourcesContent":["import { use } from 'react';\nimport type { JSX } from 'react';\n\nimport type { Route } from '../route';\nimport { getOrCreateResource } from '../data/resourceCache';\n\nimport type { ParsedLocation } from './RouterContext';\nimport { Redirect } from './Redirect';\n\ninterface RouteDataLoaderProps {\n route: Route;\n resourceKey: string;\n location: ParsedLocation;\n}\n\n/**\n * Reads the route's server data resource via `use()`. On the initial render the\n * resource is pre-seeded (SSR data) so this resolves synchronously; on client\n * navigation it suspends until the fetch settles, showing the `<Suspense>`\n * fallback (`loading.tsx`). A rejected resource is re-thrown by `use()` and\n * caught by the surrounding error boundary (`error.tsx`).\n *\n * The route's critical CSS is intentionally rendered by `RouteMatch` OUTSIDE\n * the `<Suspense>` boundary (see the note there) — a `precedence` stylesheet\n * inside the boundary would defer its streamed reveal.\n */\nexport function RouteDataLoader({\n route,\n resourceKey,\n location,\n}: RouteDataLoaderProps): JSX.Element {\n const result = use(getOrCreateResource(resourceKey, route, location));\n\n if (result.kind === 'redirect') {\n return <Redirect to={result.destination} />;\n }\n\n const Component = route.component;\n\n // Server data is spread directly as the page component's props.\n return <Component {...result.props} />;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA0BA,SAAgB,gBAAgB,EAC9B,OACA,aACA,YACoC;CACpC,MAAM,SAAS,IAAI,oBAAoB,aAAa,OAAO,QAAQ,CAAC;CAEpE,IAAI,OAAO,SAAS,YAClB,OAAO,oBAAC,UAAD,EAAU,IAAI,OAAO,YAAc;CAG5C,MAAM,YAAY,MAAM;CAGxB,OAAO,oBAAC,WAAD,EAAW,GAAI,OAAO,MAAQ;AACvC"}
@@ -0,0 +1,15 @@
1
+ import type { JSX } from 'react';
2
+ import type { Mode } from '../types';
3
+ import type { Route } from '../route';
4
+ interface RouteMatchProps {
5
+ route: Route;
6
+ mode?: Mode;
7
+ }
8
+ /**
9
+ * Renders the matched route: its parent layouts wrap an error boundary +
10
+ * `<Suspense>` boundary around the data-reading leaf. The boundary is keyed by
11
+ * the data-resource key so a navigation (including same-route param changes)
12
+ * remounts it and shows the loading fallback, while the layouts persist.
13
+ */
14
+ export declare const RouteMatch: ({ route, mode }: RouteMatchProps) => JSX.Element;
15
+ export {};
@@ -0,0 +1,81 @@
1
+ import { buildResourceKey, readLayoutData } from "../data/resourceCache.js";
2
+ import { useRouterContext } from "./RouterContext.js";
3
+ import { CriticalCss } from "./CriticalCss.js";
4
+ import { RouteDataLoader } from "./RouteDataLoader.js";
5
+ import { OssidoErrorBoundary } from "./OssidoErrorBoundary.js";
6
+ import { Suspense, memo, useMemo } from "react";
7
+ import { DefaultError, DefaultLoading, DevErrorReporter } from "@ossido-labs/ossido-ui";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+
10
+ //#region src/components/RouteMatch.tsx
11
+ /**
12
+ * Renders the matched route: its parent layouts wrap an error boundary +
13
+ * `<Suspense>` boundary around the data-reading leaf. The boundary is keyed by
14
+ * the data-resource key so a navigation (including same-route param changes)
15
+ * remounts it and shows the loading fallback, while the layouts persist.
16
+ */
17
+ const RouteMatch = ({ route, mode }) => {
18
+ const { location, navigationId, retry } = useRouterContext();
19
+ const routes = useMemo(() => loadParentComponents(route), [route.id]);
20
+ const resourceKey = buildResourceKey(navigationId, location);
21
+ const LoadingComponent = route.options.loadingComponent ?? DefaultLoading;
22
+ const ErrorComponent = route.options.errorComponent ?? (mode === "Dev" ? DevErrorReporter : DefaultError);
23
+ return /* @__PURE__ */ jsxs(TraverseRootComponents, {
24
+ routes,
25
+ mode,
26
+ children: [/* @__PURE__ */ jsx(CriticalCss, {
27
+ routeFilePath: route.filePath,
28
+ mode
29
+ }), /* @__PURE__ */ jsx(Suspense, {
30
+ fallback: /* @__PURE__ */ jsx(LoadingComponent, {}),
31
+ children: /* @__PURE__ */ jsx(OssidoErrorBoundary, {
32
+ resetKey: resourceKey,
33
+ fallback: ErrorComponent,
34
+ onReset: retry,
35
+ children: /* @__PURE__ */ jsx(RouteDataLoader, {
36
+ route,
37
+ resourceKey,
38
+ location
39
+ })
40
+ })
41
+ })]
42
+ });
43
+ };
44
+ /**
45
+ * Renders the layout (`layout`) components that wrap the selected route.
46
+ * Layouts receive only `children` now — they live OUTSIDE the keyed Suspense
47
+ * boundary, so they persist across navigation while only the page area shows
48
+ * the loading fallback. Memoized so navigation does not re-render layouts.
49
+ */
50
+ const TraverseRootComponents = memo(({ routes, index = 0, mode, children }) => {
51
+ if (routes.length > index) {
52
+ const route = routes[index];
53
+ const Parent = route.component;
54
+ const routeFilePath = route.filePath || route.id;
55
+ const layoutProps = route.options.hasHandler && route.options.dataKey ? readLayoutData(route.options.dataKey) : void 0;
56
+ return /* @__PURE__ */ jsxs(Parent, {
57
+ ...layoutProps,
58
+ children: [/* @__PURE__ */ jsx(CriticalCss, {
59
+ routeFilePath,
60
+ mode
61
+ }), /* @__PURE__ */ jsx(TraverseRootComponents, {
62
+ routes,
63
+ index: index + 1,
64
+ mode,
65
+ children
66
+ })]
67
+ });
68
+ }
69
+ return /* @__PURE__ */ jsx(Fragment, { children });
70
+ });
71
+ TraverseRootComponents.displayName = "TraverseRootComponents";
72
+ const loadParentComponents = (route, loader = []) => {
73
+ const parentComponent = route.options.getParentRoute?.();
74
+ loader.push(parentComponent);
75
+ if (!parentComponent.isRoot) return loadParentComponents(parentComponent, loader);
76
+ return loader.reverse();
77
+ };
78
+
79
+ //#endregion
80
+ export { RouteMatch };
81
+ //# sourceMappingURL=RouteMatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouteMatch.js","names":[],"sources":["../../../src/components/RouteMatch.tsx"],"sourcesContent":["import type { JSX } from 'react';\nimport { memo, Suspense, useMemo } from 'react';\n\nimport { DefaultLoading, DefaultError, DevErrorReporter } from '@ossido-labs/ossido-ui';\n\nimport type { Mode } from '../types';\nimport type { Route } from '../route';\nimport { buildResourceKey, readLayoutData } from '../data/resourceCache';\n\nimport { useRouterContext } from './RouterContext';\nimport { CriticalCss } from './CriticalCss';\nimport { RouteDataLoader } from './RouteDataLoader';\nimport { OssidoErrorBoundary } from './OssidoErrorBoundary';\n\ninterface RouteMatchProps {\n route: Route;\n mode?: Mode;\n}\n\n/**\n * Renders the matched route: its parent layouts wrap an error boundary +\n * `<Suspense>` boundary around the data-reading leaf. The boundary is keyed by\n * the data-resource key so a navigation (including same-route param changes)\n * remounts it and shows the loading fallback, while the layouts persist.\n */\nexport const RouteMatch = ({ route, mode }: RouteMatchProps): JSX.Element => {\n const { location, navigationId, retry } = useRouterContext();\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const routes = useMemo(() => loadParentComponents(route), [route.id]);\n\n const resourceKey = buildResourceKey(navigationId, location);\n\n const LoadingComponent = route.options.loadingComponent ?? DefaultLoading;\n // In dev the boundary reports the error to the shared store (rendered by the\n // floating DevErrorOverlayHost); production falls back to a detail-free page\n // so source/stack are never leaked to end users.\n const ErrorComponent =\n route.options.errorComponent ??\n (mode === 'Dev' ? DevErrorReporter : DefaultError);\n\n return (\n <TraverseRootComponents routes={routes} mode={mode}>\n {/* The leaf route's critical CSS must stay OUTSIDE the <Suspense> below:\n React 19 ties a boundary's reveal to any `precedence` stylesheet\n rendered inside it, which would push the streamed initial content into\n an out-of-order late chunk (empty shell paints first — a flash on\n every cold load). Rendered here it still hoists into the shell's\n <head> as render-blocking, but the boundary streams inline. */}\n <CriticalCss routeFilePath={route.filePath} mode={mode} />\n {/* The <Suspense> is not keyed by the resource key, so navigation\n re-renders (rather than remounts) the subtree. With the destination's\n data + code prefetched by `updateLocation`, that lets the new page\n appear in place with no fallback flash. The error boundary resets via\n `resetKey` instead of a key change. */}\n <Suspense fallback={<LoadingComponent />}>\n <OssidoErrorBoundary\n resetKey={resourceKey}\n fallback={ErrorComponent}\n onReset={retry}\n >\n <RouteDataLoader\n route={route}\n resourceKey={resourceKey}\n location={location}\n />\n </OssidoErrorBoundary>\n </Suspense>\n </TraverseRootComponents>\n );\n};\n\ninterface TraverseRootComponentsProps {\n routes: Array<Route>;\n children?: React.ReactNode;\n index?: number;\n mode?: Mode;\n}\n\n/**\n * Renders the layout (`layout`) components that wrap the selected route.\n * Layouts receive only `children` now — they live OUTSIDE the keyed Suspense\n * boundary, so they persist across navigation while only the page area shows\n * the loading fallback. Memoized so navigation does not re-render layouts.\n */\nconst TraverseRootComponents = memo(\n ({\n routes,\n index = 0,\n mode,\n children,\n }: TraverseRootComponentsProps): React.JSX.Element => {\n if (routes.length > index) {\n const route = routes[index] as Route;\n const Parent = route.component;\n\n // Fallback to the route id if the filePath is not defined\n // as is the case for the root route\n const routeFilePath = route.filePath || route.id;\n\n // A `layout.rs` handler's data is seeded (by SSR or the page's data fetch)\n // and read synchronously here, then spread as the layout's props alongside\n // `children`. Layouts without a data handler receive only `children`.\n const layoutProps =\n route.options.hasHandler && route.options.dataKey\n ? readLayoutData(route.options.dataKey)\n : undefined;\n\n return (\n <Parent {...layoutProps}>\n <CriticalCss routeFilePath={routeFilePath} mode={mode} />\n <TraverseRootComponents routes={routes} index={index + 1} mode={mode}>\n {children}\n </TraverseRootComponents>\n </Parent>\n );\n }\n\n return <>{children}</>;\n },\n);\nTraverseRootComponents.displayName = 'TraverseRootComponents';\n\nconst loadParentComponents = (\n route: Route,\n loader: Array<Route> = [],\n): Array<Route> => {\n const parentComponent = route.options.getParentRoute?.() as Route;\n\n loader.push(parentComponent);\n\n if (!parentComponent.isRoot) {\n return loadParentComponents(parentComponent, loader);\n }\n\n return loader.reverse();\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,MAAa,cAAc,EAAE,OAAO,WAAyC;CAC3E,MAAM,EAAE,UAAU,cAAc,UAAU,iBAAiB;CAG3D,MAAM,SAAS,cAAc,qBAAqB,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC;CAEpE,MAAM,cAAc,iBAAiB,cAAc,QAAQ;CAE3D,MAAM,mBAAmB,MAAM,QAAQ,oBAAoB;CAI3D,MAAM,iBACJ,MAAM,QAAQ,mBACb,SAAS,QAAQ,mBAAmB;CAEvC,OACE,qBAAC,wBAAD;EAAgC;EAAc;YAA9C,CAOE,oBAAC,aAAD;GAAa,eAAe,MAAM;GAAgB;EAAO,IAMzD,oBAAC,UAAD;GAAU,UAAU,oBAAC,kBAAD,CAAmB;aACrC,oBAAC,qBAAD;IACE,UAAU;IACV,UAAU;IACV,SAAS;cAET,oBAAC,iBAAD;KACS;KACM;KACH;IACX;GACkB;EACb,EACY;;AAE5B;;;;;;;AAeA,MAAM,yBAAyB,MAC5B,EACC,QACA,QAAQ,GACR,MACA,eACoD;CACpD,IAAI,OAAO,SAAS,OAAO;EACzB,MAAM,QAAQ,OAAO;EACrB,MAAM,SAAS,MAAM;EAIrB,MAAM,gBAAgB,MAAM,YAAY,MAAM;EAK9C,MAAM,cACJ,MAAM,QAAQ,cAAc,MAAM,QAAQ,UACtC,eAAe,MAAM,QAAQ,OAAO,IACpC;EAEN,OACE,qBAAC,QAAD;GAAQ,GAAI;aAAZ,CACE,oBAAC,aAAD;IAA4B;IAAqB;GAAO,IACxD,oBAAC,wBAAD;IAAgC;IAAQ,OAAO,QAAQ;IAAS;IAC7D;GACqB,EAClB;;CAEZ;CAEA,OAAO,gCAAG,SAAW;AACvB,CACF;AACA,uBAAuB,cAAc;AAErC,MAAM,wBACJ,OACA,SAAuB,CAAC,MACP;CACjB,MAAM,kBAAkB,MAAM,QAAQ,iBAAiB;CAEvD,OAAO,KAAK,eAAe;CAE3B,IAAI,CAAC,gBAAgB,QACnB,OAAO,qBAAqB,iBAAiB,MAAM;CAGrD,OAAO,OAAO,QAAQ;AACxB"}