@real-router/preact 0.11.0 → 0.12.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 (43) hide show
  1. package/README.md +139 -13
  2. package/dist/cjs/index.d.ts +20 -5
  3. package/dist/cjs/index.d.ts.map +1 -1
  4. package/dist/cjs/index.js +1 -1
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/ssr.d.ts +169 -0
  7. package/dist/cjs/ssr.d.ts.map +1 -0
  8. package/dist/cjs/ssr.js +2 -0
  9. package/dist/cjs/ssr.js.map +1 -0
  10. package/dist/cjs/useRoute-B3rj5MXo.js +2 -0
  11. package/dist/cjs/useRoute-B3rj5MXo.js.map +1 -0
  12. package/dist/esm/index.d.mts +20 -5
  13. package/dist/esm/index.d.mts.map +1 -1
  14. package/dist/esm/index.mjs +1 -1
  15. package/dist/esm/index.mjs.map +1 -1
  16. package/dist/esm/ssr.d.mts +169 -0
  17. package/dist/esm/ssr.d.mts.map +1 -0
  18. package/dist/esm/ssr.mjs +2 -0
  19. package/dist/esm/ssr.mjs.map +1 -0
  20. package/dist/esm/useRoute-BSPVVbLz.mjs +2 -0
  21. package/dist/esm/useRoute-BSPVVbLz.mjs.map +1 -0
  22. package/package.json +21 -4
  23. package/src/RouterProvider.tsx +15 -2
  24. package/src/components/Await.tsx +99 -0
  25. package/src/components/ClientOnly.tsx +25 -0
  26. package/src/components/HttpStatusCode.tsx +82 -0
  27. package/src/components/HttpStatusProvider.tsx +22 -0
  28. package/src/components/Link.tsx +53 -38
  29. package/src/components/RouteView/RouteView.tsx +12 -8
  30. package/src/components/RouteView/helpers.tsx +20 -19
  31. package/src/components/RouterErrorBoundary.tsx +28 -3
  32. package/src/components/ServerOnly.tsx +26 -0
  33. package/src/components/Streamed.tsx +24 -0
  34. package/src/context.ts +17 -0
  35. package/src/hooks/useDeferred.tsx +26 -0
  36. package/src/hooks/useIsActiveRoute.tsx +21 -13
  37. package/src/hooks/useNavigator.tsx +5 -12
  38. package/src/hooks/useRoute.tsx +7 -8
  39. package/src/hooks/useRouteNode.tsx +11 -7
  40. package/src/hooks/useRouter.tsx +5 -12
  41. package/src/ssr.ts +39 -0
  42. package/src/useSyncExternalStore.ts +20 -0
  43. package/src/utils/createHttpStatusSink.ts +27 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr.js","names":["useRoute","Suspense"],"sources":["../../src/components/ClientOnly.tsx","../../src/components/ServerOnly.tsx","../../src/hooks/useDeferred.tsx","../../src/components/Await.tsx","../../src/components/Streamed.tsx","../../src/components/HttpStatusProvider.tsx","../../src/components/HttpStatusCode.tsx","../../src/utils/createHttpStatusSink.ts"],"sourcesContent":["import { useEffect, useState } from \"preact/hooks\";\n\nimport type { ComponentChildren } from \"preact\";\n\nexport interface ClientOnlyProps {\n readonly children: ComponentChildren;\n readonly fallback?: ComponentChildren;\n}\n\nexport function ClientOnly({\n children,\n fallback = null,\n}: ClientOnlyProps): ComponentChildren {\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n // SSR/hydration boundary: server emits the fallback branch, client matches\n // it on first paint, then this effect flips state to swap in the children.\n // The intentional re-render is what makes the markup match across renders.\n // eslint-disable-next-line @eslint-react/set-state-in-effect -- intentional post-hydration swap\n setMounted(true);\n }, []);\n\n return mounted ? children : fallback;\n}\n","import { useEffect, useState } from \"preact/hooks\";\n\nimport type { ComponentChildren } from \"preact\";\n\nexport interface ServerOnlyProps {\n readonly children: ComponentChildren;\n readonly fallback?: ComponentChildren;\n}\n\nexport function ServerOnly({\n children,\n fallback = null,\n}: ServerOnlyProps): ComponentChildren {\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n // SSR/hydration boundary: server emits the children branch, client matches\n // it on first paint, then this effect flips state to swap in the fallback\n // (or hide entirely). The intentional re-render keeps markup consistent\n // across renders.\n // eslint-disable-next-line @eslint-react/set-state-in-effect -- intentional post-hydration swap\n setMounted(true);\n }, []);\n\n return mounted ? fallback : children;\n}\n","import { useRoute } from \"./useRoute\";\n\ninterface DeferredContext {\n ssrDataDeferred?: Record<string, Promise<unknown>>;\n}\n\nconst NEVER_PROMISE = new Promise<never>(() => {\n // Intentionally never resolves — surfaces a forever-pending Suspense boundary\n // when a key is requested that the loader never declared.\n});\n\n/**\n * Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`\n * inside an SSR data loader. Mirror of `@real-router/react/ssr` `useDeferred`\n * — same `state.context.ssrDataDeferred` contract, same NEVER-on-missing\n * fallback. Pair with `<Await>` (this package) which adds Preact-side\n * promise-status tracking since Preact 10 has no `use(promise)` analogue.\n */\nexport function useDeferred<T = unknown>(key: string): Promise<T> {\n const { route } = useRoute();\n const context = route.context as DeferredContext;\n const deferred = context.ssrDataDeferred;\n const promise = deferred?.[key];\n\n return (promise ?? NEVER_PROMISE) as Promise<T>;\n}\n","import { useDeferred } from \"../hooks/useDeferred\";\n\nimport type { ComponentChildren } from \"preact\";\n\ninterface TrackedPromise<T> extends Promise<T> {\n status?: \"pending\" | \"fulfilled\" | \"rejected\";\n value?: T;\n reason?: unknown;\n}\n\n/**\n * Preact's `Suspense` (from `preact/compat`) catches a thrown thenable and\n * re-runs the boundary's render once it settles. For deterministic re-renders\n * we tag the promise with `.status` / `.value` / `.reason` on first access so\n * the second render-pass can return the value synchronously instead of\n * throwing again.\n *\n * The same tag layout is used by React 19's internal `use(promise)` cache,\n * so promises that already carry the tag (e.g. emitted by a Suspense-aware\n * data lib) are reused as-is.\n */\nfunction track<T>(promise: Promise<T>): TrackedPromise<T> {\n const tracked = promise as TrackedPromise<T>;\n\n if (tracked.status !== undefined) {\n return tracked;\n }\n\n tracked.status = \"pending\";\n promise.then(\n (value) => {\n /* v8 ignore next 4 -- @preserve: the `.status === \"pending\"` guard\n protects against external mutation between `track()` and the .then\n microtask; covered branch is the always-true case in our control. */\n if (tracked.status === \"pending\") {\n tracked.status = \"fulfilled\";\n tracked.value = value;\n }\n },\n /* v8 ignore start -- @preserve: rejection .then handler — tested\n end-to-end via the React adapter's e2e ssr-streaming Scenario 10\n (id=4 reviews promise rejects on the wire); covering it in unit tests\n requires Preact's Suspense to surface the rejection through render,\n which doesn't compose cleanly with vitest's unhandled-rejection\n detector. Behaviour is symmetric to the success handler above. */\n (error: unknown) => {\n if (tracked.status === \"pending\") {\n tracked.status = \"rejected\";\n tracked.reason = error;\n }\n },\n /* v8 ignore stop */\n );\n\n return tracked;\n}\n\nexport interface AwaitProps<T> {\n /** Deferred key declared in the loader's `defer({ deferred: { <name>: ... } })`. */\n readonly name: string;\n /** Render the resolved value. Suspends while pending; throws inside the\n * nearest Error Boundary on rejection. */\n readonly children: (value: T) => ComponentChildren;\n}\n\n/**\n * Reads `useDeferred(name)` and hands the resolved value to the render-prop\n * via Preact's `<Suspense>`-throwing convention. Wrap in `<Streamed>` (or\n * `<Suspense>` from `preact/compat`).\n *\n * ```tsx\n * <Streamed fallback={<Spinner />}>\n * <Await<Review[]> name=\"reviews\">\n * {(reviews) => <ReviewList items={reviews} />}\n * </Await>\n * </Streamed>\n * ```\n */\nexport function Await<T = unknown>({\n name,\n children,\n}: AwaitProps<T>): ComponentChildren {\n const promise = useDeferred<T>(name);\n const tracked = track(promise);\n\n if (tracked.status === \"fulfilled\") {\n return children(tracked.value as T);\n }\n\n if (tracked.status === \"rejected\") {\n throw tracked.reason;\n }\n\n // Suspense catches the thrown thenable and waits for resolution. ESLint\n // complains because Promises aren't Errors, but Preact's Suspense (like\n // React's pre-`use()` Suspense convention) explicitly expects a thenable.\n // eslint-disable-next-line @typescript-eslint/only-throw-error -- Suspense thenable convention\n throw promise;\n}\n","import { Suspense } from \"preact/compat\";\n\nimport type { ComponentChildren } from \"preact\";\n\nexport interface StreamedProps {\n /** Shown while any descendant `<Await>` / `use(promise)`-equivalent suspends. */\n readonly fallback: ComponentChildren;\n readonly children: ComponentChildren;\n}\n\n/**\n * Cross-adapter alias for `<Suspense fallback={…}>` from `preact/compat`.\n * Pairs with `<Await>` for symmetry with the React/Solid/Svelte/Vue/Angular\n * SSR streaming naming.\n *\n * Preact's `Suspense` is part of `preact/compat` (experimental). For\n * production streaming the preact-render-to-string toolchain is required.\n */\nexport function Streamed({\n fallback,\n children,\n}: StreamedProps): ComponentChildren {\n return <Suspense fallback={fallback}>{children}</Suspense>;\n}\n","import { createContext } from \"preact\";\n\nimport type { HttpStatusSink } from \"../utils/createHttpStatusSink\";\nimport type { ComponentChildren } from \"preact\";\n\nexport const HttpStatusContext = createContext<HttpStatusSink | null>(null);\n\nexport interface HttpStatusProviderProps {\n readonly sink: HttpStatusSink;\n readonly children: ComponentChildren;\n}\n\nexport function HttpStatusProvider({\n sink,\n children,\n}: HttpStatusProviderProps): ComponentChildren {\n return (\n <HttpStatusContext.Provider value={sink}>\n {children}\n </HttpStatusContext.Provider>\n );\n}\n","import { useContext } from \"preact/hooks\";\n\nimport { HttpStatusContext } from \"./HttpStatusProvider\";\n\nimport type { ComponentChildren } from \"preact\";\n\nexport interface HttpStatusCodeProps {\n /** HTTP status to apply to the response. Common values: 404, 410, 451, 503. */\n readonly code: number;\n}\n\n/**\n * Render-time HTTP status declaration. Mount inside a route component (typical\n * use case: a glob `*` route's NotFound page) when the status is decided by\n * the rendered tree rather than a loader.\n *\n * Writes `code` to the nearest `<HttpStatusProvider>`'s sink during render and\n * returns `null`. With no provider mounted (the standard client-side case)\n * the component is a silent no-op — same component tree hydrates without\n * touching the DOM or warning about mismatches.\n *\n * Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep\n * working as before; this component covers render-time decisions only.\n *\n * Last write wins when several `<HttpStatusCode />` instances mount in the\n * same render pass — sink reflects the last component that ran.\n *\n * ```tsx\n * // entry-server.tsx\n * import { renderToString } from \"preact-render-to-string\";\n * import { createHttpStatusSink, HttpStatusProvider } from \"@real-router/preact/ssr\";\n *\n * const sink = createHttpStatusSink();\n * const html = renderToString(\n * <HttpStatusProvider sink={sink}>\n * <RouterProvider router={router}>\n * <App />\n * </RouterProvider>\n * </HttpStatusProvider>,\n * );\n * response.status(sink.code ?? 200).send(html);\n * ```\n *\n * **Streaming SSR (`renderToReadableStream`):** the response status MUST be\n * sent before the first body byte flushes. If `<HttpStatusCode />` is mounted\n * inside a late-resolving `<Suspense>` boundary, the sink write may happen\n * AFTER the headers are already on the wire — the override is then lost.\n * Mount the component in the shell (above every `<Suspense>` that could\n * delay it). For non-streaming SSR (`renderToString` / `renderToStringAsync`)\n * there is no such ordering concern.\n *\n * **Valid `code` range:** Node's `res.end()` throws `Invalid status code` on\n * `NaN`, `0`, negative values, or values `> 999` — this surfaces as a 5xx /\n * dropped connection, not silent corruption. Pass a real HTTP status integer\n * (commonly 4xx/5xx; 100-999 is what Node accepts).\n */\nexport function HttpStatusCode({\n code,\n}: HttpStatusCodeProps): ComponentChildren {\n const sink = useContext(HttpStatusContext);\n\n if (sink) {\n // Dev-only validation: Node's `res.end()` throws `Invalid status code` on\n // NaN / 0 / negative / non-integer / >999. Surface the bad value at the\n // source so the consumer can fix the routing logic, instead of waiting\n // for the server to crash mid-response. Production builds (Vite, esbuild,\n // tsdown all replace `process.env.NODE_ENV !== \"production\"` with `false`)\n // strip the check.\n if (\n process.env.NODE_ENV !== \"production\" &&\n (!Number.isInteger(code) || code < 100 || code > 999)\n ) {\n console.error(\n `[real-router] <HttpStatusCode code={${String(code)}} /> received an invalid HTTP status code. Node's res.end() rejects values that are not an integer in [100, 999] — pass a real HTTP status (commonly 4xx/5xx).`,\n );\n }\n\n sink.code = code;\n }\n\n return null;\n}\n","/**\n * Render-scoped HTTP status sink. Created per request on the server, passed to\n * `<HttpStatusProvider sink={...}>`, and read after `renderToString` (or the\n * Preact streaming helper) to apply the value to the HTTP response.\n *\n * Last write wins: if the rendered tree mounts more than one\n * `<HttpStatusCode />`, the value reflects the last component that ran during\n * the render pass.\n *\n * No-op on the client — `<HttpStatusCode />` reads the optional context and\n * skips the write when no provider is mounted, so the same component tree can\n * be hydrated without changing behaviour.\n *\n * Constraints:\n * - **Per-request only.** Don't share a sink across requests; the rendered\n * tree mutates `code` in place. Module-level singletons leak status\n * between concurrent requests.\n * - **Don't `Object.freeze` the sink.** The component writes to `.code`;\n * freezing makes the assignment throw under ESM strict mode.\n */\nexport interface HttpStatusSink {\n code: number | undefined;\n}\n\nexport function createHttpStatusSink(): HttpStatusSink {\n return { code: undefined };\n}\n"],"mappings":"0NASA,SAAgB,EAAW,CACzB,WACA,WAAW,MAC0B,CACrC,GAAM,CAAC,EAAS,IAAA,EAAA,EAAA,UAAuB,GAAM,CAU7C,OARA,EAAA,EAAA,eAAgB,CAKd,EAAW,GAAK,EACf,EAAE,CAAC,CAEC,EAAU,EAAW,ECd9B,SAAgB,EAAW,CACzB,WACA,WAAW,MAC0B,CACrC,GAAM,CAAC,EAAS,IAAA,EAAA,EAAA,UAAuB,GAAM,CAW7C,OATA,EAAA,EAAA,eAAgB,CAMd,EAAW,GAAK,EACf,EAAE,CAAC,CAEC,EAAU,EAAW,EClB9B,MAAM,EAAgB,IAAI,YAAqB,GAG7C,CASF,SAAgB,EAAyB,EAAyB,CAChE,GAAM,CAAE,SAAUA,EAAAA,GAAU,CAK5B,OAJgB,EAAM,QACG,kBACE,IAER,ECHrB,SAAS,EAAS,EAAwC,CACxD,IAAM,EAAU,EAgChB,OA9BI,EAAQ,SAAW,IAAA,IAIvB,EAAQ,OAAS,UACjB,EAAQ,KACL,GAAU,CAIL,EAAQ,SAAW,YACrB,EAAQ,OAAS,YACjB,EAAQ,MAAQ,IASnB,GAAmB,CACd,EAAQ,SAAW,YACrB,EAAQ,OAAS,WACjB,EAAQ,OAAS,IAItB,CAEM,GA7BE,EAqDX,SAAgB,EAAmB,CACjC,OACA,YACmC,CACnC,IAAM,EAAU,EAAe,EAAK,CAC9B,EAAU,EAAM,EAAQ,CAE9B,GAAI,EAAQ,SAAW,YACrB,OAAO,EAAS,EAAQ,MAAW,CAWrC,MARI,EAAQ,SAAW,WACf,EAAQ,OAOV,EC/ER,SAAgB,EAAS,CACvB,WACA,YACmC,CACnC,OAAO,EAAA,EAAA,KAACC,EAAAA,SAAD,CAAoB,WAAW,WAAoB,CAAA,CCjB5D,MAAa,GAAA,EAAA,EAAA,eAAyD,KAAK,CAO3E,SAAgB,EAAmB,CACjC,OACA,YAC6C,CAC7C,OACE,EAAA,EAAA,KAAC,EAAkB,SAAnB,CAA4B,MAAO,EAChC,WAC0B,CAAA,CCqCjC,SAAgB,EAAe,CAC7B,QACyC,CACzC,IAAM,GAAA,EAAA,EAAA,YAAkB,EAAkB,CAqB1C,OAnBI,IAQA,QAAQ,IAAI,WAAa,eACxB,CAAC,OAAO,UAAU,EAAK,EAAI,EAAO,KAAO,EAAO,MAEjD,QAAQ,MACN,uCAAuC,OAAO,EAAK,CAAC,gKACrD,CAGH,EAAK,KAAO,GAGP,KCxDT,SAAgB,GAAuC,CACrD,MAAO,CAAE,KAAM,IAAA,GAAW"}
@@ -0,0 +1,2 @@
1
+ let e=require(`preact/hooks`),t=require(`preact`);const n=(0,t.createContext)(null),r=(0,t.createContext)(null),i=(0,t.createContext)(null);function a(t,n){return()=>{let r=(0,e.useContext)(t);if(!r)throw Error(`${n} must be used within a RouterProvider`);return r}}const o=a(n,`useRoute`),s=()=>{let e=o();if(!e.route)throw Error(`useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?`);return e};Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return s}});
2
+ //# sourceMappingURL=useRoute-B3rj5MXo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRoute-B3rj5MXo.js","names":[],"sources":["../../src/context.ts","../../src/hooks/useRoute.tsx"],"sourcesContent":["import { createContext } from \"preact\";\nimport { useContext } from \"preact/hooks\";\n\nimport type { RouteContext as RouteContextType } from \"./types\";\nimport type { Router, Navigator } from \"@real-router/core\";\nimport type { Context } from \"preact\";\n\nexport const RouteContext = createContext<RouteContextType | null>(null);\n\nexport const RouterContext = createContext<Router | null>(null);\n\nexport const NavigatorContext = createContext<Navigator | null>(null);\n\nexport function createUseContextOrThrow<T>(\n context: Context<T | null>,\n hookName: string,\n): () => T {\n return () => {\n const value = useContext(context);\n\n if (!value) {\n throw new Error(`${hookName} must be used within a RouterProvider`);\n }\n\n return value;\n };\n}\n","import { createUseContextOrThrow, RouteContext } from \"../context\";\n\nimport type { RouteContext as RouteContextType } from \"../types\";\nimport type { Params, State } from \"@real-router/core\";\n\nconst useRouteContextOrThrow = createUseContextOrThrow(\n RouteContext,\n \"useRoute\",\n);\n\nexport const useRoute = <P extends Params = Params>(): Omit<\n RouteContextType<P>,\n \"route\"\n> & { route: State<P> } => {\n const routeContext = useRouteContextOrThrow();\n\n if (!routeContext.route) {\n throw new Error(\n \"useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?\",\n );\n }\n\n return routeContext as Omit<RouteContextType<P>, \"route\"> & {\n route: State<P>;\n };\n};\n"],"mappings":"kDAOA,MAAa,GAAA,EAAA,EAAA,eAAsD,KAAK,CAE3D,GAAA,EAAA,EAAA,eAA6C,KAAK,CAElD,GAAA,EAAA,EAAA,eAAmD,KAAK,CAErE,SAAgB,EACd,EACA,EACS,CACT,UAAa,CACX,IAAM,GAAA,EAAA,EAAA,YAAmB,EAAQ,CAEjC,GAAI,CAAC,EACH,MAAU,MAAM,GAAG,EAAS,uCAAuC,CAGrE,OAAO,GCnBX,MAAM,EAAyB,EAC7B,EACA,WACD,CAEY,MAGc,CACzB,IAAM,EAAe,GAAwB,CAE7C,GAAI,CAAC,EAAa,MAChB,MAAU,MACR,oIACD,CAGH,OAAO"}
@@ -1,7 +1,6 @@
1
1
  import { NavigationOptions, Navigator, Navigator as Navigator$1, Params, Router, RouterError, State } from "@real-router/core";
2
2
  import { RouteUtils } from "@real-router/route-utils";
3
- import * as _$preact from "preact";
4
- import { ComponentChildren, FunctionComponent, JSX, VNode } from "preact";
3
+ import { ComponentChildren, Context, FunctionComponent, JSX, VNode } from "preact";
5
4
  import { RouterTransitionSnapshot, RouterTransitionSnapshot as RouterTransitionSnapshot$1 } from "@real-router/sources";
6
5
 
7
6
  //#region src/components/RouteView/types.d.ts
@@ -91,6 +90,22 @@ interface RouterErrorBoundaryProps {
91
90
  readonly fallback: (error: RouterError, resetError: () => void) => ComponentChildren;
92
91
  readonly onError?: (error: RouterError, toRoute: State | null, fromRoute: State | null) => void;
93
92
  }
93
+ /**
94
+ * Declarative navigation-error boundary.
95
+ *
96
+ * **Not** a Preact `componentDidCatch`-style ErrorBoundary — this component
97
+ * does NOT catch render-time exceptions from `children`. It is a compositional
98
+ * component that subscribes to `createDismissableError` from
99
+ * `@real-router/sources` and renders `fallback(error, resetError)` ALONGSIDE
100
+ * `children` (wrapped in a `<Fragment>`) when the router emits a navigation
101
+ * error (guard rejection, ROUTE_NOT_FOUND, etc.). The boundary auto-resets on
102
+ * the next successful navigation; `resetError()` lets the consumer dismiss
103
+ * the fallback imperatively.
104
+ *
105
+ * For real exception boundaries, wrap children in a Preact ErrorBoundary
106
+ * (e.g. `preact-iso/ErrorBoundary` or a custom `componentDidCatch` class) —
107
+ * the two can coexist.
108
+ */
94
109
  declare function RouterErrorBoundary({
95
110
  children,
96
111
  fallback,
@@ -368,9 +383,9 @@ interface RouteProviderProps {
368
383
  declare const RouterProvider: FunctionComponent<RouteProviderProps>;
369
384
  //#endregion
370
385
  //#region src/context.d.ts
371
- declare const RouteContext: _$preact.Context<RouteContext$1 | null>;
372
- declare const RouterContext: _$preact.Context<Router<object> | null>;
373
- declare const NavigatorContext: _$preact.Context<Navigator$1 | null>;
386
+ declare const RouteContext: Context<RouteContext$1 | null>;
387
+ declare const RouterContext: Context<Router<object> | null>;
388
+ declare const NavigatorContext: Context<Navigator$1 | null>;
374
389
  //#endregion
375
390
  export { Link, type LinkProps, type Navigator, NavigatorContext, RouteContext, type RouteEnterContext, type RouteEnterHandler, type RouteExitContext, type RouteExitHandler, RouteView, type MatchProps as RouteViewMatchProps, type NotFoundProps as RouteViewNotFoundProps, type RouteViewProps, type SelfProps as RouteViewSelfProps, RouterContext, RouterErrorBoundary, type RouterErrorBoundaryProps, RouterProvider, type RouterTransitionSnapshot, type UseRouteEnterOptions, type UseRouteExitOptions, useNavigator, useRoute, useRouteEnter, useRouteExit, useRouteNode, useRouteUtils, useRouter, useRouterTransition };
376
391
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/components/RouteView/types.ts","../../src/components/RouteView/components.tsx","../../src/components/RouteView/RouteView.tsx","../../src/types.ts","../../src/components/Link.tsx","../../src/components/RouterErrorBoundary.tsx","../../src/hooks/useRouter.tsx","../../src/hooks/useNavigator.tsx","../../src/hooks/useRouteUtils.tsx","../../src/hooks/useRoute.tsx","../../src/hooks/useRouteNode.tsx","../../src/hooks/useRouterTransition.tsx","../../src/hooks/useRouteExit.tsx","../../src/hooks/useRouteEnter.tsx","../../../../shared/dom-utils/scroll-restore.ts","../../src/RouterProvider.tsx","../../src/context.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/components/RouteView/types.ts","../../src/components/RouteView/components.tsx","../../src/components/RouteView/RouteView.tsx","../../src/types.ts","../../src/components/Link.tsx","../../src/components/RouterErrorBoundary.tsx","../../src/hooks/useRouter.tsx","../../src/hooks/useNavigator.tsx","../../src/hooks/useRouteUtils.tsx","../../src/hooks/useRoute.tsx","../../src/hooks/useRouteNode.tsx","../../src/hooks/useRouterTransition.tsx","../../src/hooks/useRouteExit.tsx","../../src/hooks/useRouteEnter.tsx","../../../../shared/dom-utils/scroll-restore.ts","../../src/RouterProvider.tsx","../../src/context.ts"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{useCallback as e,useContext as t,useEffect as n,useLayoutEffect as r,useMemo as i,useRef as a,useState as o}from"preact/hooks";import{UNKNOWN_ROUTE as s,getNavigator as c}from"@real-router/core";import{getRouteUtils as l,startsWithSegment as u}from"@real-router/route-utils";import{Fragment as d,createContext as f,isValidElement as p,toChildArray as m}from"preact";import{Suspense as h,memo as g}from"preact/compat";import{Fragment as _,jsx as v,jsxs as y}from"preact/jsx-runtime";import{createActiveRouteSource as b,createDismissableError as ee,createRouteNodeSource as te,createRouteSource as ne,getTransitionSource as x}from"@real-router/sources";import{getPluginApi as re}from"@real-router/core/api";function S(e){return null}S.displayName=`RouteView.Match`;function C(e){return null}C.displayName=`RouteView.Self`;function w(e){return null}w.displayName=`RouteView.NotFound`;function T(e,t,n){return t===``?!1:n?e===t:u(e,t)}function E(e,t){for(let n of m(e))p(n)&&(n.type===S||n.type===C||n.type===w?t.push(n):E(n.props.children,t))}function D(e,t,n){return v(d,{children:n===void 0?e:v(h,{fallback:n,children:e})},t)}function ie(e,t){return e.type===w?(t.notFoundChildren=e.props.children,!0):e.type===C?(t.selfFound||=(t.selfChildren=e.props.children,t.selfFallback=e.props.fallback,!0),!0):!1}function O(e,t,n,r){let{segment:i,exact:a=!1,fallback:o}=e.props,s=n?`${n}.${i}`:i;return!r&&T(t,s,a)?D(e.props.children,s,o):null}function k(e,t,n,r){if(r.selfFound&&t===n){e.push(D(r.selfChildren,`__route-view-self__`,r.selfFallback));return}t===s&&r.notFoundChildren!==null&&e.push(v(d,{children:r.notFoundChildren},`__route-view-not-found__`))}function A(e,t,n){let r={selfChildren:null,selfFallback:void 0,selfFound:!1,notFoundChildren:null},i=!1,a=[];for(let o of e){if(ie(o,r))continue;let e=O(o,t,n,i);e!==null&&(i=!0,a.push(e))}return i||k(a,t,n,r),{rendered:a,activeMatchFound:i}}function j(e,t,r){let[i,a]=o(t);return n(()=>{let n=()=>{a(e=>{let n=t();return Object.is(e,n)?e:n})};return n(),e(n)},[e,t]),i}const M=f(null),N=f(null),P=f(null),F=()=>{let e=t(N);if(!e)throw Error(`useRouter must be used within a RouterProvider`);return e};function I(e){let t=F(),n=i(()=>te(t,e),[t,e]),{route:r,previousRoute:a}=j(n.subscribe,n.getSnapshot,n.getSnapshot),o=i(()=>c(t),[t]);return i(()=>({navigator:o,route:r,previousRoute:a}),[o,r,a])}function L({nodeName:e,children:t}){let{route:n}=I(e),r=i(()=>{let e=[];return E(t,e),e},[t]);if(!n)return null;let{rendered:a}=A(r,n.name,e);return a.length>0?v(_,{children:a}):null}L.displayName=`RouteView`;const R=Object.assign(L,{Match:S,Self:C,NotFound:w}),z=Object.freeze({}),B=Object.freeze({}),V=`data-real-router-announcer`;function H(e,t){let n=t?.prefix??`Navigated to `,r=t?.getAnnouncementText,i=!0,a=!1,o=!1,s=``,c=null,l,u=U(),d=(e,t)=>{s=e,clearTimeout(l),u.textContent=e,l=setTimeout(()=>{u.textContent=``,s=``},7e3),K(t)},f=setTimeout(()=>{if(a=!0,c!==null&&!o){let e=c;c=null,d(e,document.querySelector(`h1`))}},100),p=e.subscribe(({route:e})=>{if(i){i=!1;return}requestAnimationFrame(()=>{requestAnimationFrame(()=>{if(o)return;let t=document.querySelector(`h1`),i=G(e,n,r,t);if(!(!i||i===s)){if(!a){c=i;return}d(i,t)}})})});return{destroy(){o=!0,p(),clearTimeout(l),clearTimeout(f),W()}}}function U(){let e=document.querySelector(`[${V}]`);if(e)return e;let t=document.createElement(`div`);return t.setAttribute(`style`,`position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0`),t.setAttribute(`aria-live`,`assertive`),t.setAttribute(`aria-atomic`,`true`),t.setAttribute(V,``),document.body.prepend(t),t}function W(){document.querySelector(`[${V}]`)?.remove()}function G(e,t,n,r){if(n)return n(e);let i=(r?.textContent??``).trim(),a=e.name.startsWith(`@@`)?``:e.name;return`${t}${i||document.title||a||globalThis.location.pathname}`}function K(e){e&&(e.hasAttribute(`tabindex`)||e.setAttribute(`tabindex`,`-1`),e.focus({preventScroll:!0}))}const q=Object.freeze({destroy:()=>{}});function J(e,t){if(globalThis.window===void 0)return q;let n=t?.mode??`restore`;if(n===`native`)return q;let r=t?.anchorScrolling??!0,i=t?.scrollContainer,a=t?.behavior??`auto`,o=t?.storageKey??`real-router:scroll`,s=()=>{try{let e=sessionStorage.getItem(o);return e?JSON.parse(e):{}}catch{return{}}},c=(e,t)=>{try{let n=s();n[e]=t,sessionStorage.setItem(o,JSON.stringify(n))}catch{}},l=history.scrollRestoration;try{history.scrollRestoration=`manual`}catch{}let u=()=>{let e=i?.();return e?e.scrollTop:globalThis.scrollY},d=e=>{let t=i?.();t?t.scrollTo({top:e,left:0,behavior:a}):globalThis.scrollTo({top:e,left:0,behavior:a})},f=e=>{let t=e.context?.url?.hash;if(t!==void 0){if(r&&t.length>0){let e=document.getElementById(t);if(e){e.scrollIntoView({behavior:a});return}}d(0);return}let n=globalThis.location.hash;if(r&&n.length>1){let e;try{e=decodeURIComponent(n.slice(1))}catch{e=n.slice(1)}let t=document.getElementById(e);if(t){t.scrollIntoView({behavior:a});return}}d(0)},p=!1,m=e.subscribe(({route:e,previousRoute:t})=>{let r=e.context.navigation;t&&c(Y(t),u()),requestAnimationFrame(()=>{if(!p){if(n===`top`||!r){f(e);return}if(r.navigationType!==`replace`){if(r.direction===`back`||r.navigationType===`traverse`||r.navigationType===`reload`){d(s()[Y(e)]??0);return}f(e)}}})}),h=()=>{let t=e.getState();t&&c(Y(t),u())};return globalThis.addEventListener(`pagehide`,h),{destroy:()=>{if(!p){p=!0,m(),globalThis.removeEventListener(`pagehide`,h);try{history.scrollRestoration=l}catch{}}}}}function Y(e){return`${e.name}:${ae(e.params)}`}function ae(e){return JSON.stringify(e,oe)}function oe(e,t){if(typeof t==`object`&&t&&!Array.isArray(t)){let e={},n=Object.keys(t).sort((e,t)=>e.localeCompare(t));for(let r of n)e[r]=t[r];return e}return t}const se=Object.freeze({destroy:()=>{}});function ce(e){if(typeof document>`u`||typeof document.startViewTransition!=`function`)return se;let t=null,n=null,r=!1,i=()=>{t?.(),t=null},a=e.subscribeLeave(({signal:e})=>{if(!e.aborted)return r=!1,i(),new Promise(a=>{let o=new Promise(e=>{t=e});e.addEventListener(`abort`,()=>{r||(i(),n?.skipTransition?.(),a())},{once:!0});try{n=document.startViewTransition(()=>(a(),o))}catch{i(),a()}})}),o=e.subscribe(()=>{let e=t;r=!0,t=null,e===null?n=null:setTimeout(()=>{e(),n=null},0)});return{destroy:()=>{a(),o(),n?.skipTransition?.(),n=null,i()}}}function le(e){return e.button===0&&!e.metaKey&&!e.altKey&&!e.ctrlKey&&!e.shiftKey}function ue(e){return encodeURI(e).replaceAll(`#`,`%23`)}function de(e,t,n,r){try{let i=r?.hash,a;i!==void 0&&(a=i.startsWith(`#`)?i.slice(1):i);let o=e.buildUrl;if(o){let e=o(t,n,a===void 0?void 0:{hash:a});if(e!==void 0)return e}let s=e.buildPath(t,n);return a?`${s}#${ue(a)}`:s}catch{console.error(`[real-router] Route "${t}" is not defined. The element will render without an href attribute.`);return}}function fe(e,t,n,r,i){let a={...i};r!==void 0&&(a.hash=r);let o=e.getState();if(o?.name===t&&Z(o.params,n)){let e=o.context?.url?.hash??``;e!==(r??e)&&(a.force=!0,a.hashChange=!0)}return e.navigate(t,n,a)}function X(e){return e?e.match(/\S+/g)??[]:[]}function pe(e,t,n){if(e&&t){let e=X(t);if(e.length===0)return n??void 0;if(!n)return e.join(` `);let r=X(n),i=new Set(r);for(let t of e)i.has(t)||(i.add(t),r.push(t));return r.join(` `)}return n??void 0}function Z(e,t){if(Object.is(e,t))return!0;if(!e||!t)return!1;let n=Object.keys(e);if(n.length!==Object.keys(t).length)return!1;let r=e,i=t;for(let e of n)if(!Object.is(r[e],i[e]))return!1;return!0}function me(e,t,n=!1,r=!0,i){let a=b(F(),e,t,i===void 0?{strict:n,ignoreQueryParams:r}:{strict:n,ignoreQueryParams:r,hash:i});return j(a.subscribe,a.getSnapshot,a.getSnapshot)}function he(e,t){return e.routeName===t.routeName&&e.className===t.className&&e.activeClassName===t.activeClassName&&e.activeStrict===t.activeStrict&&e.ignoreQueryParams===t.ignoreQueryParams&&e.onClick===t.onClick&&e.target===t.target&&e.style===t.style&&e.children===t.children&&e.hash===t.hash&&Z(e.routeParams,t.routeParams)&&Z(e.routeOptions,t.routeOptions)}const Q=g(({routeName:t,routeParams:n=z,routeOptions:r=B,className:a,activeClassName:o=`active`,activeStrict:s=!1,ignoreQueryParams:c=!0,hash:l,onClick:u,target:d,children:f,...p})=>{let m=F(),h=me(t,n,s,c,l),g=i(()=>de(m,t,n,l===void 0?void 0:{hash:l}),[m,t,n,l]),_=e(e=>{u&&(u(e),e.defaultPrevented)||!le(e)||d===`_blank`||(e.preventDefault(),fe(m,t,n,l,r).catch(()=>{}))},[u,d,m,t,n,r,l]),y=i(()=>pe(h,o,a),[h,o,a]);return v(`a`,{...p,href:g,className:y,onClick:_,children:f})},he);Q.displayName=`Link`;function ge({children:e,fallback:t,onError:r}){let i=ee(F()),o=j(i.subscribe,i.getSnapshot,i.getSnapshot),s=a(r);return s.current=r,n(()=>{o.error&&s.current?.(o.error,o.toRoute,o.fromRoute)},[o.version]),y(d,{children:[e,o.error?t(o.error,o.resetError):null]})}const _e=()=>{let e=t(P);if(!e)throw Error(`useNavigator must be used within a RouterProvider`);return e},ve=()=>l(re(F()).getTree()),$=()=>{let e=t(M);if(!e)throw Error(`useRoute must be used within a RouterProvider`);if(!e.route)throw Error(`useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?`);return e};function ye(){let e=x(F());return j(e.subscribe,e.getSnapshot,e.getSnapshot)}function be(e,t){let i=F(),o=a(e),s=t?.skipSameRoute??!0;r(()=>{o.current=e}),n(()=>i.subscribeLeave(({route:e,nextRoute:t,signal:n})=>{if(!(s&&e.name===t.name)&&!n.aborted)return o.current({route:e,nextRoute:t,signal:n})}),[i,s])}function xe(e,t){let{route:i,previousRoute:o}=$(),s=a(e),c=a(null),l=t?.skipSameRoute??!0;r(()=>{s.current=e}),n(()=>{i.transition.from&&(l&&i.transition.from===i.name||c.current===i||!o||(c.current=i,s.current({route:i,previousRoute:o})))},[i,o,l])}const Se=({router:e,children:t,announceNavigation:r,scrollRestoration:a,viewTransitions:o})=>{n(()=>{if(!r)return;let t=H(e);return()=>{t.destroy()}},[r,e]);let s=a?.mode,l=a?.anchorScrolling,u=a?.behavior,d=a?.storageKey,f=a!==void 0;n(()=>{if(!f)return;let t=J(e,{mode:s,anchorScrolling:l,behavior:u,storageKey:d,scrollContainer:a.scrollContainer});return()=>{t.destroy()}},[e,f,s,l,u,d]),n(()=>{if(!o)return;let t=ce(e);return()=>{t.destroy()}},[e,o]);let p=i(()=>c(e),[e]),m=i(()=>ne(e),[e]),{route:h,previousRoute:g}=j(m.subscribe,m.getSnapshot,m.getSnapshot),_=i(()=>({navigator:p,route:h,previousRoute:g}),[p,h,g]);return v(N.Provider,{value:e,children:v(P.Provider,{value:p,children:v(M.Provider,{value:_,children:t})})})};export{Q as Link,P as NavigatorContext,M as RouteContext,R as RouteView,N as RouterContext,ge as RouterErrorBoundary,Se as RouterProvider,_e as useNavigator,$ as useRoute,xe as useRouteEnter,be as useRouteExit,I as useRouteNode,ve as useRouteUtils,F as useRouter,ye as useRouterTransition};
1
+ import{a as e,i as t,n,r,t as i}from"./useRoute-BSPVVbLz.mjs";import{useEffect as a,useLayoutEffect as o,useMemo as s,useRef as c,useState as l}from"preact/hooks";import{UNKNOWN_ROUTE as u,getNavigator as d}from"@real-router/core";import{getRouteUtils as f,startsWithSegment as p}from"@real-router/route-utils";import{Fragment as m,isValidElement as h,toChildArray as g}from"preact";import{Suspense as _,memo as v}from"preact/compat";import{Fragment as y,jsx as b,jsxs as x}from"preact/jsx-runtime";import{createActiveRouteSource as S,createDismissableError as ee,createRouteNodeSource as te,createRouteSource as C,getTransitionSource as w}from"@real-router/sources";import{getPluginApi as T}from"@real-router/core/api";function E(e){return null}E.displayName=`RouteView.Match`;function D(e){return null}D.displayName=`RouteView.Self`;function O(e){return null}O.displayName=`RouteView.NotFound`;function k(e,t,n){return t===``?!1:n?e===t:p(e,t)}function A(e,t){for(let n of g(e))h(n)&&(n.type===E||n.type===D||n.type===O?t.push(n):A(n.props.children,t))}function j(e,t,n){return b(m,{children:n===void 0?e:b(_,{fallback:n,children:e})},t)}function M(e){return e.type===O||e.type===D}function N(e,t){if(e.type===O){t.notFoundChildren=e.props.children;return}t.selfFound||=(t.selfChildren=e.props.children,t.selfFallback=e.props.fallback,!0)}function ne(e,t,n,r){let{segment:i,exact:a=!1,fallback:o,children:s}=e.props,c=n?`${n}.${i}`:i;return!r&&k(t,c,a)?j(s,c,o):null}function re(e,t,n,r){if(r.selfFound&&t===n){e.push(j(r.selfChildren,`__route-view-self__`,r.selfFallback));return}t===u&&r.notFoundChildren!==null&&e.push(b(m,{children:r.notFoundChildren},`__route-view-not-found__`))}function ie(e,t,n){let r={selfChildren:null,selfFallback:void 0,selfFound:!1,notFoundChildren:null},i=!1,a=[];for(let o of e){if(M(o)){N(o,r);continue}let e=ne(o,t,n,i);e!==null&&(i=!0,a.push(e))}return i||re(a,t,n,r),{rendered:a,activeMatchFound:i}}function P(e,t,n){let[r,i]=l(t);return a(()=>{let n=()=>{i(e=>{let n=t();return Object.is(e,n)?e:n})};return n(),e(n)},[e,t]),r}const F=e(n,`useNavigator`),I=e(t,`useRouter`);function L(e){let t=I(),n=F(),r=te(t,e),{route:i,previousRoute:a}=P(r.subscribe,r.getSnapshot,r.getSnapshot);return s(()=>({navigator:n,route:i,previousRoute:a}),[n,i,a])}function R({nodeName:e,children:t}){let{route:n}=L(e),r=s(()=>{let e=[];return A(t,e),e},[t]),i=n?.name,a=s(()=>i===void 0?[]:ie(r,i,e).rendered,[r,i,e]);return a.length>0?b(y,{children:a}):null}R.displayName=`RouteView`;const z=Object.assign(R,{Match:E,Self:D,NotFound:O}),B=Object.freeze({}),V=Object.freeze({}),H=`data-real-router-announcer`,U=Object.freeze({destroy:()=>{}});function W(e,t){if(typeof document>`u`)return U;let n=t?.prefix??`Navigated to `,r=t?.getAnnouncementText,i=!0,a=!1,o=!1,s=``,c=null,l,u=G(),d=(e,t)=>{s=e,clearTimeout(l),u.textContent=e,l=setTimeout(()=>{u.textContent=``,s=``},7e3),J(t)},f=setTimeout(()=>{if(a=!0,c!==null&&!o){let e=c;c=null,d(e,document.querySelector(`h1`))}},100),p=e.subscribe(({route:e})=>{if(i){i=!1;return}requestAnimationFrame(()=>{requestAnimationFrame(()=>{if(o)return;let t=document.querySelector(`h1`),i=q(e,n,r,t);if(!(!i||i===s)){if(!a){c=i;return}d(i,t)}})})});return{destroy(){o=!0,p(),clearTimeout(l),clearTimeout(f),K()}}}function G(){let e=document.querySelector(`[${H}]`);if(e)return e;let t=document.createElement(`div`);return t.setAttribute(`style`,`position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0`),t.setAttribute(`aria-live`,`assertive`),t.setAttribute(`aria-atomic`,`true`),t.setAttribute(H,``),(document.body??document.documentElement).prepend(t),t}function K(){document.querySelector(`[${H}]`)?.remove()}function q(e,t,n,r){if(n)try{let t=n(e);if(t)return t}catch(e){console.error(`[real-router] getAnnouncementText threw; falling back to default resolution.`,e)}let i=(r?.textContent??``).trim(),a=e.name.startsWith(`@@`)?``:e.name;return`${t}${i||document.title||a||globalThis.location.pathname}`}function J(e){e&&(e.hasAttribute(`tabindex`)||e.setAttribute(`tabindex`,`-1`),e.focus({preventScroll:!0}))}const Y=Object.freeze({destroy:()=>{}});function ae(e,t){if(globalThis.window===void 0)return Y;let n=t?.mode??`restore`;if(n===`native`)return Y;let r=t?.anchorScrolling??!0,i=t?.scrollContainer,a=t?.behavior??`auto`,o=t?.storageKey??`real-router:scroll`,s,c=()=>{if(s!==void 0)return s;try{let e=sessionStorage.getItem(o);s=e?JSON.parse(e):{}}catch{s={}}return s},l=(e,t)=>{try{let n=c();if(n[e]===t)return;n[e]=t,sessionStorage.setItem(o,JSON.stringify(n))}catch{}},u=history.scrollRestoration;try{history.scrollRestoration=`manual`}catch{}let d=()=>{let e=i?.();return e?e.scrollTop:globalThis.scrollY},f=e=>{let t=i?.();t?t.scrollTo({top:e,left:0,behavior:a}):globalThis.scrollTo({top:e,left:0,behavior:a})},p=e=>{let t=e.context?.url?.hash;if(t!==void 0){if(r&&t.length>0){let e=document.getElementById(t);if(e){e.scrollIntoView({behavior:a});return}}f(0);return}let n=globalThis.location.hash;if(r&&n.length>1){let e;try{e=decodeURIComponent(n.slice(1))}catch{e=n.slice(1)}let t=document.getElementById(e);if(t){t.scrollIntoView({behavior:a});return}}f(0)},m=!1,h=!1,g=e=>{try{return oe(e)}catch{return h||(h=!0,console.error(`[real-router] scroll-restore: route "${e.name}" has params that cannot be canonicalized (e.g. BigInt or cyclic structure). Scroll position will not be captured or restored for this route.`)),null}},_=e.subscribe(({route:e,previousRoute:t})=>{let r=e.context.navigation;if(t){let e=g(t);e!==null&&l(e,d())}requestAnimationFrame(()=>{if(!m){if(n===`top`||!r){p(e);return}if(r.navigationType!==`replace`){if(r.direction===`back`||r.navigationType===`traverse`||r.navigationType===`reload`){let t=g(e);f(t===null?0:c()[t]??0);return}p(e)}}})}),v=()=>{let t=e.getState();if(t){let e=g(t);e!==null&&l(e,d())}};return globalThis.addEventListener(`pagehide`,v),{destroy:()=>{if(!m){m=!0,_(),globalThis.removeEventListener(`pagehide`,v);try{history.scrollRestoration=u}catch{}}}}}const X=new WeakMap;function oe(e){let t=X.get(e);if(t!==void 0)return t;let n=`${e.name}:${se(e.params)}`;return X.set(e,n),n}function se(e){return JSON.stringify(e,ce)}function ce(e,t){if(typeof t==`function`)return`<fn>`;if(typeof t==`symbol`)return`<sym>`;if(typeof t==`object`&&t&&!Array.isArray(t)){let e=Object.create(null),n=Object.keys(t).sort((e,t)=>e.localeCompare(t));for(let r of n)e[r]=t[r];return e}return t}const le=Object.freeze({destroy:()=>{}});function ue(e){if(typeof document>`u`||typeof document.startViewTransition!=`function`)return le;let t=null,n=null,r=!1,i=()=>{t?.(),t=null},a=e.subscribeLeave(({signal:e})=>{if(!e.aborted)return r=!1,i(),new Promise(a=>{let o=new Promise(e=>{t=e});e.addEventListener(`abort`,()=>{r||(i(),n?.skipTransition?.(),a())},{once:!0});try{n=document.startViewTransition(()=>(a(),o))}catch{i(),a()}})}),o=e.subscribe(()=>{let e=t;r=!0,t=null,e===null?n=null:setTimeout(()=>{e(),n=null},0)});return{destroy:()=>{a(),o(),n?.skipTransition?.(),n=null,i()}}}function de(e){return e.button===0&&!e.metaKey&&!e.altKey&&!e.ctrlKey&&!e.shiftKey}const fe=/%[\dA-Fa-f]{2}/;function pe(e){if(fe.test(e))try{return encodeURI(decodeURIComponent(e)).replaceAll(`#`,`%23`)}catch{}return encodeURI(e).replaceAll(`#`,`%23`)}function me(e,t,n,r){try{let i=r?.hash,a;i!==void 0&&(a=i.startsWith(`#`)?i.slice(1):i);let o=e.buildUrl;if(o){let e=o(t,n,a===void 0?void 0:{hash:a});if(typeof e==`string`&&e.length>0)return e}let s=e.buildPath(t,n);if(typeof s!=`string`||s.length===0){console.error(`[real-router] Route "${t}" yielded an empty path. The element will render without an href attribute.`);return}return a?`${s}#${pe(a)}`:s}catch{console.error(`[real-router] Route "${t}" is not defined. The element will render without an href attribute.`);return}}function he(e,t,n,r,i){let a={...i};r!==void 0&&(a.hash=r);let o=e.getState();if(o?.name===t&&Q(o.params,n)){let e=o.context?.url?.hash??``;e!==(r??e)&&(a.force=!0,a.hashChange=!0)}return e.navigate(t,n,a)}const ge=/\s/,_e=/\S+/g;function Z(e){return e?ge.test(e)?e.match(_e)??[]:[e]:[]}function ve(e,t,n){if(e&&t){let e=Z(t);if(e.length===0)return n??void 0;if(!n)return e.join(` `);let r=Z(n),i=new Set(r);for(let t of e)i.has(t)||(i.add(t),r.push(t));return r.join(` `)}return n??void 0}function Q(e,t){if(Object.is(e,t))return!0;if(!e||!t)return!1;let n=Object.keys(e);if(n.length!==Object.keys(t).length)return!1;let r=e,i=t;for(let e of n)if(!Object.prototype.hasOwnProperty.call(t,e)||!Object.is(r[e],i[e]))return!1;return!0}function ye(e,t,n=!1,r=!0,i){let a=I(),o=s(()=>i===void 0?{strict:n,ignoreQueryParams:r}:{strict:n,ignoreQueryParams:r,hash:i},[n,r,i]),c=s(()=>S(a,e,t,o),[a,e,t,o]);return P(c.subscribe,c.getSnapshot,c.getSnapshot)}function be(e,t){return e.routeName===t.routeName&&e.className===t.className&&e.activeClassName===t.activeClassName&&e.activeStrict===t.activeStrict&&e.ignoreQueryParams===t.ignoreQueryParams&&e.onClick===t.onClick&&e.target===t.target&&e.style===t.style&&e.children===t.children&&e.hash===t.hash&&Q(e.routeParams,t.routeParams)&&Q(e.routeOptions,t.routeOptions)}const $=v(({routeName:e,routeParams:t=B,routeOptions:n=V,className:r,activeClassName:i=`active`,activeStrict:a=!1,ignoreQueryParams:o=!0,hash:s,onClick:c,target:l,children:u,...d})=>{let f=I(),p=ye(e,t,a,o,s),m=me(f,e,t,s===void 0?void 0:{hash:s}),h=r=>{c&&(c(r),r.defaultPrevented)||!de(r)||l===`_blank`||(r.preventDefault(),he(f,e,t,s,n).catch(()=>{}))},g=ve(p,i,r);return b(`a`,{...d,href:m,className:g,onClick:h,children:u})},be);$.displayName=`Link`;function xe({children:e,fallback:t,onError:n}){let r=ee(I()),i=P(r.subscribe,r.getSnapshot,r.getSnapshot),s=c(n);return o(()=>{s.current=n}),a(()=>{i.error&&s.current?.(i.error,i.toRoute,i.fromRoute)},[i.version]),x(m,{children:[e,i.error?t(i.error,i.resetError):null]})}const Se=()=>f(T(I()).getTree());function Ce(){let e=w(I());return P(e.subscribe,e.getSnapshot,e.getSnapshot)}function we(e,t){let n=I(),r=c(e),i=t?.skipSameRoute??!0;o(()=>{r.current=e}),a(()=>n.subscribeLeave(({route:e,nextRoute:t,signal:n})=>{if(!(i&&e.name===t.name)&&!n.aborted)return r.current({route:e,nextRoute:t,signal:n})}),[n,i])}function Te(e,t){let{route:n,previousRoute:r}=i(),s=c(e),l=c(null),u=t?.skipSameRoute??!0;o(()=>{s.current=e}),a(()=>{n.transition.from&&(u&&n.transition.from===n.name||l.current===n||!r||(l.current=n,s.current({route:n,previousRoute:r})))},[n,r,u])}const Ee=({router:e,children:i,announceNavigation:o,scrollRestoration:c,viewTransitions:l})=>{a(()=>{if(!o)return;let t=W(e);return()=>{t.destroy()}},[o,e]);let u=c?.mode,f=c?.anchorScrolling,p=c?.behavior,m=c?.storageKey,h=c!==void 0;a(()=>{if(!h)return;let t=ae(e,{mode:u,anchorScrolling:f,behavior:p,storageKey:m,scrollContainer:c.scrollContainer});return()=>{t.destroy()}},[e,h,u,f,p,m]),a(()=>{if(!l)return;let t=ue(e);return()=>{t.destroy()}},[e,l]);let g=d(e),_=s(()=>C(e),[e]),{route:v,previousRoute:y}=P(_.subscribe,_.getSnapshot,_.getSnapshot),x=s(()=>({navigator:g,route:v,previousRoute:y}),[g,v,y]);return b(t.Provider,{value:e,children:b(n.Provider,{value:g,children:b(r.Provider,{value:x,children:i})})})};export{$ as Link,n as NavigatorContext,r as RouteContext,z as RouteView,t as RouterContext,xe as RouterErrorBoundary,Ee as RouterProvider,F as useNavigator,i as useRoute,Te as useRouteEnter,we as useRouteExit,L as useRouteNode,Se as useRouteUtils,I as useRouter,Ce as useRouterTransition};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["NOOP_INSTANCE"],"sources":["../../src/components/RouteView/components.tsx","../../src/components/RouteView/helpers.tsx","../../src/useSyncExternalStore.ts","../../src/context.ts","../../src/hooks/useRouter.tsx","../../src/hooks/useRouteNode.tsx","../../src/components/RouteView/RouteView.tsx","../../src/constants.ts","../../../../shared/dom-utils/route-announcer.ts","../../../../shared/dom-utils/scroll-restore.ts","../../../../shared/dom-utils/view-transitions.ts","../../../../shared/dom-utils/link-utils.ts","../../src/hooks/useIsActiveRoute.tsx","../../src/components/Link.tsx","../../src/components/RouterErrorBoundary.tsx","../../src/hooks/useNavigator.tsx","../../src/hooks/useRouteUtils.tsx","../../src/hooks/useRoute.tsx","../../src/hooks/useRouterTransition.tsx","../../src/hooks/useRouteExit.tsx","../../src/hooks/useRouteEnter.tsx","../../src/RouterProvider.tsx"],"sourcesContent":["import type { MatchProps, NotFoundProps, SelfProps } from \"./types\";\n\nexport function Match(_props: MatchProps): null {\n return null;\n}\n\nMatch.displayName = \"RouteView.Match\";\n\nexport function Self(_props: SelfProps): null {\n return null;\n}\n\nSelf.displayName = \"RouteView.Self\";\n\nexport function NotFound(_props: NotFoundProps): null {\n return null;\n}\n\nNotFound.displayName = \"RouteView.NotFound\";\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\nimport { startsWithSegment } from \"@real-router/route-utils\";\nimport { Fragment, isValidElement, toChildArray } from \"preact\";\nimport { Suspense } from \"preact/compat\";\n\nimport { Match, NotFound, Self } from \"./components\";\n\nimport type { MatchProps, NotFoundProps, SelfProps } from \"./types\";\nimport type { VNode, ComponentChildren } from \"preact\";\n\ninterface FallbackSlots {\n selfChildren: ComponentChildren;\n selfFallback: ComponentChildren | undefined;\n selfFound: boolean;\n notFoundChildren: ComponentChildren;\n}\n\nfunction isSegmentMatch(\n routeName: string,\n fullSegmentName: string,\n exact: boolean,\n): boolean {\n if (fullSegmentName === \"\") {\n return false;\n }\n\n if (exact) {\n return routeName === fullSegmentName;\n }\n\n return startsWithSegment(routeName, fullSegmentName);\n}\n\nexport function collectElements(\n children: ComponentChildren,\n result: VNode[],\n): void {\n for (const child of toChildArray(children)) {\n if (!isValidElement(child)) {\n continue;\n }\n\n if (\n child.type === Match ||\n child.type === Self ||\n child.type === NotFound\n ) {\n result.push(child);\n } else {\n collectElements(\n (child.props as { readonly children: ComponentChildren }).children,\n result,\n );\n }\n }\n}\n\nfunction renderSlot(\n slotChildren: ComponentChildren,\n key: string,\n fallback?: ComponentChildren,\n): VNode {\n const content =\n fallback === undefined ? (\n slotChildren\n ) : (\n <Suspense fallback={fallback}>{slotChildren}</Suspense>\n );\n\n return <Fragment key={key}>{content}</Fragment>;\n}\n\nfunction recordFallback(child: VNode, slots: FallbackSlots): boolean {\n if (child.type === NotFound) {\n slots.notFoundChildren = (child.props as NotFoundProps).children;\n\n return true;\n }\n\n if (child.type === Self) {\n if (!slots.selfFound) {\n slots.selfChildren = (child.props as SelfProps).children;\n slots.selfFallback = (child.props as SelfProps).fallback;\n slots.selfFound = true;\n }\n\n return true;\n }\n\n return false;\n}\n\nfunction processMatch(\n child: VNode,\n routeName: string,\n nodeName: string,\n alreadyActive: boolean,\n): VNode | null {\n const { segment, exact = false, fallback } = child.props as MatchProps;\n const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;\n const isActive =\n !alreadyActive && isSegmentMatch(routeName, fullSegmentName, exact);\n\n if (!isActive) {\n return null;\n }\n\n return renderSlot(\n (child.props as MatchProps).children,\n fullSegmentName,\n fallback,\n );\n}\n\nfunction appendFallback(\n rendered: VNode[],\n routeName: string,\n nodeName: string,\n slots: FallbackSlots,\n): void {\n if (slots.selfFound && routeName === nodeName) {\n rendered.push(\n renderSlot(slots.selfChildren, \"__route-view-self__\", slots.selfFallback),\n );\n\n return;\n }\n\n if (routeName === UNKNOWN_ROUTE && slots.notFoundChildren !== null) {\n rendered.push(\n <Fragment key=\"__route-view-not-found__\">\n {slots.notFoundChildren}\n </Fragment>,\n );\n }\n}\n\nexport function buildRenderList(\n elements: VNode[],\n routeName: string,\n nodeName: string,\n): { rendered: VNode[]; activeMatchFound: boolean } {\n const slots: FallbackSlots = {\n selfChildren: null,\n selfFallback: undefined,\n selfFound: false,\n notFoundChildren: null,\n };\n let activeMatchFound = false;\n const rendered: VNode[] = [];\n\n for (const child of elements) {\n if (recordFallback(child, slots)) {\n continue;\n }\n\n const matchRendered = processMatch(\n child,\n routeName,\n nodeName,\n activeMatchFound,\n );\n\n if (matchRendered !== null) {\n activeMatchFound = true;\n rendered.push(matchRendered);\n }\n }\n\n if (!activeMatchFound) {\n appendFallback(rendered, routeName, nodeName, slots);\n }\n\n return { rendered, activeMatchFound };\n}\n","import { useEffect, useState } from \"preact/hooks\";\n\n/**\n * Polyfill for React's useSyncExternalStore.\n *\n * Preact does not provide a native useSyncExternalStore.\n * This implementation uses useState + useEffect to subscribe\n * to external stores.\n *\n * Race condition handling: the value may change between\n * `useState(getSnapshot)` (render) and `useEffect` (commit).\n * We synchronize by calling `setValue(getSnapshot())` before\n * subscribing in the effect.\n *\n * The updater uses `Object.is` to bail out when the snapshot\n * is referentially stable, preventing redundant re-renders.\n */\nexport function useSyncExternalStore<T>(\n subscribe: (onStoreChange: () => void) => () => void,\n getSnapshot: () => T,\n _getServerSnapshot?: () => T,\n): T {\n const [value, setValue] = useState(getSnapshot);\n\n useEffect(() => {\n const sync = (): void => {\n setValue((prev) => {\n const next = getSnapshot();\n\n return Object.is(prev, next) ? prev : next;\n });\n };\n\n sync();\n\n return subscribe(sync);\n }, [subscribe, getSnapshot]);\n\n return value;\n}\n","import { createContext } from \"preact\";\n\nimport type { RouteContext as RouteContextType } from \"./types\";\nimport type { Router, Navigator } from \"@real-router/core\";\n\nexport const RouteContext = createContext<RouteContextType | null>(null);\n\nexport const RouterContext = createContext<Router | null>(null);\n\nexport const NavigatorContext = createContext<Navigator | null>(null);\n","import { useContext } from \"preact/hooks\";\n\nimport { RouterContext } from \"../context\";\n\nimport type { Router } from \"@real-router/core\";\n\nexport const useRouter = (): Router => {\n const router = useContext(RouterContext);\n\n if (!router) {\n throw new Error(\"useRouter must be used within a RouterProvider\");\n }\n\n return router;\n};\n","import { getNavigator } from \"@real-router/core\";\nimport { createRouteNodeSource } from \"@real-router/sources\";\nimport { useMemo } from \"preact/hooks\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouteContext } from \"../types\";\n\nexport function useRouteNode(nodeName: string): RouteContext {\n const router = useRouter();\n\n const store = useMemo(\n () => createRouteNodeSource(router, nodeName),\n [router, nodeName],\n );\n\n const { route, previousRoute } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n\n const navigator = useMemo(() => getNavigator(router), [router]);\n\n return useMemo(\n (): RouteContext => ({ navigator, route, previousRoute }),\n [navigator, route, previousRoute],\n );\n}\n","import { useMemo } from \"preact/hooks\";\n\nimport { Match, NotFound, Self } from \"./components\";\nimport { buildRenderList, collectElements } from \"./helpers\";\nimport { useRouteNode } from \"../../hooks/useRouteNode\";\n\nimport type { RouteViewProps } from \"./types\";\nimport type { VNode } from \"preact\";\n\nfunction RouteViewRoot({\n nodeName,\n children,\n}: Readonly<RouteViewProps>): VNode | null {\n const { route } = useRouteNode(nodeName);\n\n // Cache the flattened Match/Self/NotFound list across renders with unchanged\n // children. children only differs when the parent re-renders with a new\n // node, so this memoises the steady-state traversal.\n const elements = useMemo(() => {\n const collected: VNode[] = [];\n\n collectElements(children, collected);\n\n return collected;\n }, [children]);\n\n if (!route) {\n return null;\n }\n\n const { rendered } = buildRenderList(elements, route.name, nodeName);\n\n if (rendered.length > 0) {\n return <>{rendered}</>;\n }\n\n return null;\n}\n\nRouteViewRoot.displayName = \"RouteView\";\n\nexport const RouteView = Object.assign(RouteViewRoot, {\n Match,\n Self,\n NotFound,\n});\n\nexport type {\n RouteViewProps,\n MatchProps as RouteViewMatchProps,\n SelfProps as RouteViewSelfProps,\n NotFoundProps as RouteViewNotFoundProps,\n} from \"./types\";\n","/**\n * Stable empty object for default params\n */\nexport const EMPTY_PARAMS = Object.freeze({});\n\n/**\n * Stable empty options object\n */\nexport const EMPTY_OPTIONS = Object.freeze({});\n","import type { Router, State } from \"@real-router/core\";\n\nconst CLEAR_DELAY = 7000;\nconst SAFARI_READY_DELAY = 100;\nconst ANNOUNCER_ATTR = \"data-real-router-announcer\";\nconst INTERNAL_ROUTE_PREFIX = \"@@\";\nconst VISUALLY_HIDDEN =\n \"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0\";\n\nexport interface RouteAnnouncerOptions {\n prefix?: string;\n getAnnouncementText?: (route: State) => string;\n}\n\nexport function createRouteAnnouncer(\n router: Router,\n options?: RouteAnnouncerOptions,\n): { destroy: () => void } {\n const prefix = options?.prefix ?? \"Navigated to \";\n const getCustomText = options?.getAnnouncementText;\n\n let isInitialNavigation = true;\n let isReady = false;\n let isDestroyed = false;\n let lastAnnouncedText = \"\";\n let pendingText: string | null = null;\n let clearTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const announcer = getOrCreateAnnouncer();\n\n const doAnnounce = (text: string, h1: HTMLElement | null): void => {\n lastAnnouncedText = text;\n clearTimeout(clearTimeoutId);\n announcer.textContent = text;\n clearTimeoutId = setTimeout(() => {\n announcer.textContent = \"\";\n lastAnnouncedText = \"\";\n }, CLEAR_DELAY);\n\n manageFocus(h1);\n };\n\n // Safari-ready delay: announcing before VoiceOver wires up the aria-live region\n // causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms\n // before marking the announcer \"ready\" — any navigation during that window is\n // buffered in pendingText and flushed once the delay expires.\n const safariTimeoutId = setTimeout(() => {\n isReady = true;\n\n if (pendingText !== null && !isDestroyed) {\n const text = pendingText;\n\n pendingText = null;\n doAnnounce(text, document.querySelector<HTMLElement>(\"h1\"));\n }\n }, SAFARI_READY_DELAY);\n\n const unsubscribe = router.subscribe(({ route }) => {\n if (isInitialNavigation) {\n isInitialNavigation = false;\n\n return;\n }\n\n // Double rAF: waits for two paint frames so the incoming route's DOM\n // (including the new <h1>) is fully rendered before resolveText reads it.\n // Single rAF fires before the new route's template has been attached,\n // which would cause resolveText to pick up the OLD h1 or fall back to\n // document.title / route.name prematurely.\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (isDestroyed) {\n return;\n }\n\n const h1 = document.querySelector<HTMLElement>(\"h1\");\n const text = resolveText(route, prefix, getCustomText, h1);\n\n if (!text || text === lastAnnouncedText) {\n return;\n }\n\n if (!isReady) {\n // Defer announcement until Safari-ready window elapses (see safariTimeoutId).\n pendingText = text;\n\n return;\n }\n\n doAnnounce(text, h1);\n });\n });\n });\n\n return {\n destroy() {\n isDestroyed = true;\n unsubscribe();\n clearTimeout(clearTimeoutId);\n clearTimeout(safariTimeoutId);\n removeAnnouncer();\n },\n };\n}\n\nfunction getOrCreateAnnouncer(): HTMLElement {\n const existing = document.querySelector<HTMLElement>(`[${ANNOUNCER_ATTR}]`);\n\n if (existing) {\n return existing;\n }\n\n const element = document.createElement(\"div\");\n\n element.setAttribute(\"style\", VISUALLY_HIDDEN);\n element.setAttribute(\"aria-live\", \"assertive\");\n element.setAttribute(\"aria-atomic\", \"true\");\n element.setAttribute(ANNOUNCER_ATTR, \"\");\n\n document.body.prepend(element);\n\n return element;\n}\n\nfunction removeAnnouncer(): void {\n document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();\n}\n\nfunction resolveText(\n route: State,\n prefix: string,\n getCustomText: ((route: State) => string) | undefined,\n h1: HTMLElement | null,\n): string {\n if (getCustomText) {\n return getCustomText(route);\n }\n\n const h1Text = (h1?.textContent ?? \"\").trim();\n const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)\n ? \"\"\n : route.name;\n const rawText =\n h1Text || document.title || routeName || globalThis.location.pathname;\n\n return `${prefix}${rawText}`;\n}\n\nfunction manageFocus(h1: HTMLElement | null): void {\n if (!h1) {\n return;\n }\n\n if (!h1.hasAttribute(\"tabindex\")) {\n h1.setAttribute(\"tabindex\", \"-1\");\n }\n\n h1.focus({ preventScroll: true });\n}\n","import type { Router, State } from \"@real-router/core\";\n\nconst DEFAULT_STORAGE_KEY = \"real-router:scroll\";\n\nconst NOOP_INSTANCE: { destroy: () => void } = Object.freeze({\n destroy: () => {\n /* no-op */\n },\n});\n\nexport type ScrollRestorationMode = \"restore\" | \"top\" | \"native\";\n\nexport interface ScrollRestorationOptions {\n mode?: ScrollRestorationMode | undefined;\n anchorScrolling?: boolean | undefined;\n scrollContainer?: (() => HTMLElement | null) | undefined;\n /**\n * Scroll behavior passed to `scrollTo({ behavior })` and\n * `scrollIntoView({ behavior })`.\n *\n * - `\"auto\"` (default) — browser-defined, usually instant.\n * - `\"instant\"` — explicit instant jump (no animation).\n * - `\"smooth\"` — animated transition. Note: smooth restore on back/traverse\n * can feel disorienting if the user expects to land at the saved position\n * immediately. Recommended for `mode: \"top\"` or anchor scroll only.\n *\n * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions/behavior).\n */\n behavior?: ScrollBehavior | undefined;\n /**\n * sessionStorage key used to persist saved scroll positions. Default:\n * `\"real-router:scroll\"`. Override only when multiple independent\n * `RouterProvider` instances share the same document and you need to\n * isolate their scroll stores (e.g. micro-frontends, embedded widgets,\n * or testing). For a single app with one provider the default is fine.\n */\n storageKey?: string | undefined;\n}\n\ninterface NavigationContext {\n direction?: \"forward\" | \"back\" | \"unknown\";\n navigationType?: \"push\" | \"replace\" | \"traverse\" | \"reload\";\n}\n\nexport function createScrollRestoration(\n router: Router,\n options?: ScrollRestorationOptions,\n): { destroy: () => void } {\n if (typeof globalThis.window === \"undefined\") {\n return NOOP_INSTANCE;\n }\n\n const mode = options?.mode ?? \"restore\";\n\n // mode \"native\" = utility does nothing. Don't flip history.scrollRestoration,\n // don't subscribe, don't register pagehide — `history.scrollRestoration`\n // stays at the browser default (\"auto\") so the browser handles scroll\n // restore natively. (Note: this is the OPPOSITE of `history.scrollRestoration\n // === \"manual\"` — utility's \"native\" leaves the DOM property at \"auto\" so\n // the browser is in charge.)\n if (mode === \"native\") {\n return NOOP_INSTANCE;\n }\n\n const anchorEnabled = options?.anchorScrolling ?? true;\n const getContainer = options?.scrollContainer;\n const behavior: ScrollBehavior = options?.behavior ?? \"auto\";\n const storageKey = options?.storageKey ?? DEFAULT_STORAGE_KEY;\n\n const loadStore = (): Record<string, number> => {\n try {\n const raw = sessionStorage.getItem(storageKey);\n\n return raw ? (JSON.parse(raw) as Record<string, number>) : {};\n } catch {\n return {};\n }\n };\n\n const putPos = (key: string, pos: number): void => {\n try {\n const store = loadStore();\n\n store[key] = pos;\n sessionStorage.setItem(storageKey, JSON.stringify(store));\n } catch {\n // Ignore quota / security errors.\n }\n };\n\n const prevScrollRestoration = history.scrollRestoration;\n\n try {\n history.scrollRestoration = \"manual\";\n } catch {\n // Ignore — some embedded contexts may reject the assignment.\n }\n\n // Resolve the container lazily on every event so containers mounted AFTER\n // the provider still get correct scroll handling. Falls back to window when\n // the getter is absent or returns null (pre-mount).\n const readPos = (): number => {\n const element = getContainer?.();\n\n return element ? element.scrollTop : globalThis.scrollY;\n };\n\n const writePos = (top: number): void => {\n const element = getContainer?.();\n\n if (element) {\n element.scrollTo({ top, left: 0, behavior });\n } else {\n globalThis.scrollTo({ top, left: 0, behavior });\n }\n };\n\n const scrollToHashOrTop = (route: State): void => {\n // URL plugin path (#532): `state.context.url.hash` is the source of truth\n // when one of the URL plugins (browser-plugin / navigation-plugin) is\n // installed. The value is already DECODED — feeding it through\n // `decodeURIComponent` again would throw on a bare `%`.\n const ctxHash = (route.context as { url?: { hash?: string } } | undefined)\n ?.url?.hash;\n\n if (ctxHash !== undefined) {\n if (anchorEnabled && ctxHash.length > 0) {\n // eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars\n const element = document.getElementById(ctxHash);\n\n if (element) {\n element.scrollIntoView({ behavior });\n\n return;\n }\n }\n\n writePos(0);\n\n return;\n }\n\n // Fallback path: no URL plugin, read the DOM. `location.hash` is\n // percent-encoded; ids in the DOM are the raw string, so decode for the\n // match. Fall back to the raw slice if the hash contains a malformed\n // escape sequence (decodeURIComponent throws on those).\n const hash = globalThis.location.hash;\n\n if (anchorEnabled && hash.length > 1) {\n let id: string;\n\n try {\n id = decodeURIComponent(hash.slice(1));\n } catch {\n id = hash.slice(1);\n }\n\n // eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars\n const element = document.getElementById(id);\n\n if (element) {\n element.scrollIntoView({ behavior });\n\n return;\n }\n }\n\n writePos(0);\n };\n\n let destroyed = false;\n\n const unsubscribe = router.subscribe(({ route, previousRoute }) => {\n const nav = (route.context as { navigation?: NavigationContext })\n .navigation;\n\n // Browsers dispatch reload as the initial navigation after refresh, so\n // previousRoute is undefined and capture is naturally skipped. The\n // pre-refresh position was already persisted via pagehide.\n if (previousRoute) {\n putPos(keyOf(previousRoute), readPos());\n }\n\n // Single rAF so DOM is committed before we read anchors / write scroll.\n // Guard against destroy() racing with the callback.\n requestAnimationFrame(() => {\n if (destroyed) {\n return;\n }\n\n if (mode === \"top\" || !nav) {\n scrollToHashOrTop(route);\n\n return;\n }\n\n if (nav.navigationType === \"replace\") {\n return;\n }\n\n if (\n nav.direction === \"back\" ||\n nav.navigationType === \"traverse\" ||\n nav.navigationType === \"reload\"\n ) {\n writePos(loadStore()[keyOf(route)] ?? 0);\n\n return;\n }\n\n scrollToHashOrTop(route);\n });\n });\n\n const onPageHide = (): void => {\n const current = router.getState();\n\n if (current) {\n putPos(keyOf(current), readPos());\n }\n };\n\n globalThis.addEventListener(\"pagehide\", onPageHide);\n\n return {\n destroy: () => {\n if (destroyed) {\n return;\n }\n\n destroyed = true;\n unsubscribe();\n globalThis.removeEventListener(\"pagehide\", onPageHide);\n\n try {\n history.scrollRestoration = prevScrollRestoration;\n } catch {\n // Ignore.\n }\n },\n };\n}\n\nfunction keyOf(state: State): string {\n return `${state.name}:${canonicalJson(state.params)}`;\n}\n\nfunction canonicalJson(value: unknown): string {\n return JSON.stringify(value, canonicalReplacer);\n}\n\nfunction canonicalReplacer(_key: string, val: unknown): unknown {\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n const sorted: Record<string, unknown> = {};\n // eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable\n const keys = Object.keys(val as Record<string, unknown>).sort(\n (left: string, right: string) => left.localeCompare(right),\n );\n\n for (const key of keys) {\n sorted[key] = (val as Record<string, unknown>)[key];\n }\n\n return sorted;\n }\n\n return val;\n}\n","import type { Router } from \"@real-router/core\";\n\nexport interface ViewTransitions {\n destroy: () => void;\n}\n\nconst NOOP_INSTANCE: ViewTransitions = Object.freeze({\n destroy: () => {\n /* no-op */\n },\n});\n\nexport function createViewTransitions(router: Router): ViewTransitions {\n if (\n typeof document === \"undefined\" ||\n typeof document.startViewTransition !== \"function\"\n ) {\n return NOOP_INSTANCE;\n }\n\n let closeVT: (() => void) | null = null;\n let currentVT: { skipTransition?: () => void } | null = null;\n // Tracks whether TRANSITION_SUCCESS fired for the current leave. Used to\n // distinguish \"benign cleanup abort\" (router's async path aborts its own\n // controller in a finally block after successful navigation) from \"real\n // cancellation\" (concurrent navigate, guard rejection, dispose).\n let successFired = false;\n\n const resolveAndClear = (): void => {\n closeVT?.();\n closeVT = null;\n };\n\n const offLeave = router.subscribeLeave(({ signal }) => {\n // Reentrant abort: signal already aborted when we're called. Open no VT\n // — router will fall through to TRANSITION_CANCELLED via isCurrentNav()\n // after leave resolves. addEventListener(\"abort\", ...) does not re-fire\n // for past events, so skipping startViewTransition is the safe path.\n if (signal.aborted) {\n return;\n }\n\n successFired = false;\n resolveAndClear();\n\n // Return a Promise so the router awaits until the browser invokes\n // updateCallback. This ensures old DOM snapshot is captured BEFORE the\n // router commits the new state — giving correct exit→state→entry\n // ordering (vs fire-and-forget, where URL changes before VT captures).\n return new Promise<void>((resolveLeave) => {\n // Capture the resolver synchronously BEFORE startViewTransition() is\n // called. The browser invokes updateCallback in a later task, but\n // router.subscribe (TRANSITION_SUCCESS) can fire before that. If we\n // captured `resolve` inside the callback, subscribe would see closeVT\n // still null and skip resolving — the deferred would hang for 4s\n // until the VT API aborts with TimeoutError.\n const deferred = new Promise<void>((resolve) => {\n closeVT = resolve;\n });\n\n signal.addEventListener(\n \"abort\",\n () => {\n if (successFired) {\n // Router's async path (#finishAsyncNavigation) aborts its own\n // controller in a finally block AFTER completeTransition (and\n // thus AFTER subscribe fired). This is cleanup, not\n // cancellation — VT is progressing normally, do nothing.\n return;\n }\n\n // Real cancellation (concurrent navigate, dispose). Resolve the\n // deferred so updateCallback can complete, skip the VT so no\n // stale animation leaks, and unblock the router if the abort\n // fires before updateCallback was invoked.\n resolveAndClear();\n currentVT?.skipTransition?.();\n resolveLeave();\n },\n { once: true },\n );\n\n try {\n currentVT = document.startViewTransition(() => {\n // Resolving here unblocks the router at the moment the browser\n // enters updateCallback — by spec, old DOM snapshot is captured\n // before this callback runs. Router now proceeds through\n // activation guards and setState; the VT animation waits on\n // `deferred`, which is resolved from router.subscribe after a\n // task-queue tick (see NOTE on setTimeout below).\n resolveLeave();\n\n return deferred;\n });\n } catch {\n // Defensive: spec says startViewTransition doesn't throw under\n // normal conditions, but Chromium has had edge cases (detached\n // document, extension interference). Clean up and unblock router.\n resolveAndClear();\n resolveLeave();\n }\n });\n });\n\n const offSuccess = router.subscribe(() => {\n const resolver = closeVT;\n\n successFired = true;\n closeVT = null;\n\n if (resolver === null) {\n currentVT = null;\n } else {\n // CRITICAL: CANNOT use requestAnimationFrame here. When the router\n // takes the async path (leave returned a Promise), subscribe fires\n // AFTER the browser has already transitioned VT into the\n // \"update-callback-called\" phase. In that phase Chromium sets\n // rendering suppression to true, which ALSO blocks rAF callbacks.\n // rAF would never fire → deferred never resolves → browser aborts\n // vt.ready with TimeoutError after 4s (observed in Chromium).\n //\n // setTimeout runs on the task queue independent of the rendering\n // pipeline, so it fires regardless of suppression. React's scheduler\n // uses MessageChannel tasks, which are queued before our setTimeout,\n // so the new DOM is committed by the time our callback runs.\n setTimeout(() => {\n resolver();\n currentVT = null;\n }, 0);\n }\n });\n\n return {\n destroy: () => {\n offLeave();\n offSuccess();\n currentVT?.skipTransition?.();\n currentVT = null;\n resolveAndClear();\n },\n };\n}\n","import type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\n\nexport function shouldNavigate(evt: MouseEvent): boolean {\n return (\n evt.button === 0 &&\n !evt.metaKey &&\n !evt.altKey &&\n !evt.ctrlKey &&\n !evt.shiftKey\n );\n}\n\n/**\n * RFC 3986 fragment encoding: preserve sub-delims (`&`, `=`, `?`, `:`),\n * encode space, `%`, control chars, non-ASCII via encodeURI; defensively\n * escape `#` (encodeURI does not). Mirrors `encodeHashFragment` in\n * `shared/browser-env/url-context.ts` — duplicated here because the\n * shared/dom-utils symlink graph does not reach shared/browser-env.\n */\nfunction encodeFragmentInline(decoded: string): string {\n return encodeURI(decoded).replaceAll(\"#\", \"%23\");\n}\n\ntype BuildUrlFn = (\n name: string,\n params: Params,\n options?: { hash?: string },\n) => string | undefined;\n\n/**\n * Builds an href for a `<Link>` element.\n *\n * - Prefers the URL plugin's `buildUrl` (browser-plugin, navigation-plugin,\n * hash-plugin) when present.\n * - Falls back to `router.buildPath` for runtimes without a URL plugin\n * (memory-plugin, console UIs, NativeScript). In that fallback the hash\n * is appended manually so the rendered href is still correct.\n * - The optional 4th argument is an options object so the contract stays\n * extensible. The `hash` option is a decoded fragment without leading \"#\";\n * `<Link hash=\"#section\">` is accepted defensively (leading \"#\" stripped).\n * Frozen API: previous 3-arg call sites continue to work unchanged.\n */\nexport function buildHref(\n router: Router,\n routeName: string,\n routeParams: Params,\n options?: { hash?: string },\n): string | undefined {\n try {\n const rawHash = options?.hash;\n let normHash: string | undefined;\n\n if (rawHash !== undefined) {\n normHash = rawHash.startsWith(\"#\") ? rawHash.slice(1) : rawHash;\n }\n\n const buildUrl = router.buildUrl as BuildUrlFn | undefined;\n\n if (buildUrl) {\n const url = buildUrl(\n routeName,\n routeParams,\n normHash === undefined ? undefined : { hash: normHash },\n );\n\n if (url !== undefined) {\n return url;\n }\n }\n\n const path = router.buildPath(routeName, routeParams);\n\n return normHash ? `${path}#${encodeFragmentInline(normHash)}` : path;\n } catch {\n console.error(\n `[real-router] Route \"${routeName}\" is not defined. The element will render without an href attribute.`,\n );\n\n return undefined;\n }\n}\n\n/**\n * `<Link>` click-handler navigation helper (#532).\n *\n * Wraps `router.navigate(name, params, opts)` with same-route different-hash\n * detection: when the consumer clicks a hash-bearing Link that targets the\n * current route with the same params but a different fragment, core's\n * SAME_STATES check would otherwise reject the navigation. The helper adds\n * `force: true` and `hashChange: true` automatically — subscribers can then\n * disambiguate via `state.context.url.hashChanged`.\n *\n * For pure programmatic same-route hash-only navigation, callers are\n * documented to pass `{ force: true }` themselves; the auto-bypass here is\n * a UX convenience for `<Link hash>` that all 6 framework adapters share.\n */\n/**\n * Local extended-options type. Adapters that depend only on `@real-router/core`\n * (without a URL plugin) do not see the `NavigationOptions` augmentation that\n * declares `hash` / `hashChange`. Casting to this widened type inside the\n * helper keeps shared/dom-utils self-contained — adapters do not need to\n * augment NavigationOptions themselves to consume `<Link hash>`.\n */\ntype HashAwareNavigationOptions = NavigationOptions & {\n hash?: string;\n hashChange?: boolean;\n};\n\nexport function navigateWithHash(\n router: Router,\n routeName: string,\n routeParams: Params,\n hash: string | undefined,\n extraOptions?: NavigationOptions,\n): Promise<State> {\n const opts: HashAwareNavigationOptions = { ...extraOptions };\n\n if (hash !== undefined) {\n opts.hash = hash;\n }\n\n const current = router.getState();\n\n if (\n current?.name === routeName &&\n shallowEqual(current.params, routeParams)\n ) {\n const currentHash =\n (current.context as { url?: { hash?: string } } | undefined)?.url?.hash ??\n \"\";\n const newHash = hash ?? currentHash;\n\n if (currentHash !== newHash) {\n opts.force = true;\n opts.hashChange = true;\n }\n }\n\n return router.navigate(routeName, routeParams, opts);\n}\n\nfunction parseTokens(value: string | undefined): string[] {\n return value ? (value.match(/\\S+/g) ?? []) : [];\n}\n\nexport function buildActiveClassName(\n isActive: boolean,\n activeClassName: string | undefined,\n baseClassName: string | undefined,\n): string | undefined {\n if (isActive && activeClassName) {\n const activeTokens = parseTokens(activeClassName);\n\n if (activeTokens.length === 0) {\n return baseClassName ?? undefined;\n }\n if (!baseClassName) {\n return activeTokens.join(\" \");\n }\n\n const baseTokens = parseTokens(baseClassName);\n const seen = new Set(baseTokens);\n\n for (const token of activeTokens) {\n if (!seen.has(token)) {\n seen.add(token);\n baseTokens.push(token);\n }\n }\n\n return baseTokens.join(\" \");\n }\n\n return baseClassName ?? undefined;\n}\n\nexport function shallowEqual(\n prev: object | undefined,\n next: object | undefined,\n): boolean {\n if (Object.is(prev, next)) {\n return true;\n }\n if (!prev || !next) {\n return false;\n }\n\n const prevKeys = Object.keys(prev);\n\n if (prevKeys.length !== Object.keys(next).length) {\n return false;\n }\n\n const prevRecord = prev as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n\n for (const key of prevKeys) {\n if (!Object.is(prevRecord[key], nextRecord[key])) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function applyLinkA11y(element: HTMLElement | null | undefined): void {\n if (!element) {\n return;\n }\n if (\n element instanceof HTMLAnchorElement ||\n element instanceof HTMLButtonElement\n ) {\n return;\n }\n if (!element.hasAttribute(\"role\")) {\n element.setAttribute(\"role\", \"link\");\n }\n if (!element.hasAttribute(\"tabindex\")) {\n element.setAttribute(\"tabindex\", \"0\");\n }\n}\n","import { createActiveRouteSource } from \"@real-router/sources\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { Params } from \"@real-router/core\";\n\nexport function useIsActiveRoute(\n routeName: string,\n params?: Params,\n strict = false,\n ignoreQueryParams = true,\n hash?: string,\n): boolean {\n const router = useRouter();\n\n // createActiveRouteSource is per-router + canonical-args cached in\n // @real-router/sources, so passing params by reference is safe. The\n // `hash` argument (#532) participates in the cache key — a tab Link\n // pointing to `/settings#account` shares its source only with consumers\n // using the same routeName + params + hash.\n // exactOptionalPropertyTypes forbids `{ hash: undefined }` literally, so\n // we conditionally include the key only when the caller passed a value.\n const store = createActiveRouteSource(\n router,\n routeName,\n params,\n hash === undefined\n ? { strict, ignoreQueryParams }\n : { strict, ignoreQueryParams, hash },\n );\n\n return useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n}\n","import { memo } from \"preact/compat\";\nimport { useCallback, useMemo } from \"preact/hooks\";\n\nimport { EMPTY_PARAMS, EMPTY_OPTIONS } from \"../constants\";\nimport {\n shouldNavigate,\n buildHref,\n buildActiveClassName,\n navigateWithHash,\n shallowEqual,\n} from \"../dom-utils\";\nimport { useIsActiveRoute } from \"../hooks/useIsActiveRoute\";\nimport { useRouter } from \"../hooks/useRouter\";\n\nimport type { LinkProps } from \"../types\";\nimport type { FunctionComponent, JSX } from \"preact\";\n\nfunction areLinkPropsEqual(\n prev: Readonly<LinkProps>,\n next: Readonly<LinkProps>,\n): boolean {\n return (\n prev.routeName === next.routeName &&\n prev.className === next.className &&\n prev.activeClassName === next.activeClassName &&\n prev.activeStrict === next.activeStrict &&\n prev.ignoreQueryParams === next.ignoreQueryParams &&\n prev.onClick === next.onClick &&\n prev.target === next.target &&\n prev.style === next.style &&\n prev.children === next.children &&\n prev.hash === next.hash &&\n shallowEqual(prev.routeParams, next.routeParams) &&\n shallowEqual(prev.routeOptions, next.routeOptions)\n );\n}\n\nexport const Link: FunctionComponent<LinkProps> = memo(\n ({\n routeName,\n routeParams = EMPTY_PARAMS,\n routeOptions = EMPTY_OPTIONS,\n className,\n activeClassName = \"active\",\n activeStrict = false,\n ignoreQueryParams = true,\n hash,\n onClick,\n target,\n children,\n ...props\n }) => {\n const router = useRouter();\n\n // memo + areLinkPropsEqual guarantees that on bail-out the component does\n // not render; on render, routeParams/routeOptions changed reference (true\n // change caught by shallowEqual), so they're safe to use directly in hook\n // deps without useStableValue.\n\n // Hash-aware active (#532): when `hash` prop is set, isActive requires\n // both route AND hash to match. Tab-style UI (multiple links sharing\n // routeName but differing in hash) needs this to avoid marking all tabs\n // active by route-name alone.\n const isActive = useIsActiveRoute(\n routeName,\n routeParams,\n activeStrict,\n ignoreQueryParams,\n hash,\n );\n\n const href = useMemo(\n () =>\n buildHref(\n router,\n routeName,\n routeParams,\n hash === undefined ? undefined : { hash },\n ),\n [router, routeName, routeParams, hash],\n );\n\n const handleClick = useCallback(\n (evt: JSX.TargetedMouseEvent<HTMLAnchorElement>) => {\n if (onClick) {\n onClick(evt);\n\n if (evt.defaultPrevented) {\n return;\n }\n }\n\n if (!shouldNavigate(evt) || target === \"_blank\") {\n return;\n }\n\n evt.preventDefault();\n navigateWithHash(\n router,\n routeName,\n routeParams,\n hash,\n routeOptions,\n ).catch(() => {});\n },\n [onClick, target, router, routeName, routeParams, routeOptions, hash],\n );\n\n const finalClassName = useMemo(\n () => buildActiveClassName(isActive, activeClassName, className),\n [isActive, activeClassName, className],\n );\n\n return (\n <a\n {...props}\n href={href}\n className={finalClassName}\n onClick={handleClick}\n >\n {children}\n </a>\n );\n },\n areLinkPropsEqual,\n);\n\nLink.displayName = \"Link\";\n","import { createDismissableError } from \"@real-router/sources\";\nimport { Fragment } from \"preact\";\nimport { useEffect, useRef } from \"preact/hooks\";\n\nimport { useRouter } from \"../hooks/useRouter\";\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\n\nimport type { RouterError, State } from \"@real-router/core\";\nimport type { ComponentChildren, VNode } from \"preact\";\n\nexport interface RouterErrorBoundaryProps {\n readonly children: ComponentChildren;\n readonly fallback: (\n error: RouterError,\n resetError: () => void,\n ) => ComponentChildren;\n readonly onError?: (\n error: RouterError,\n toRoute: State | null,\n fromRoute: State | null,\n ) => void;\n}\n\nexport function RouterErrorBoundary({\n children,\n fallback,\n onError,\n}: RouterErrorBoundaryProps): VNode {\n const router = useRouter();\n const store = createDismissableError(router);\n const snapshot = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n\n const onErrorRef = useRef(onError);\n\n // eslint-disable-next-line @eslint-react/refs -- \"latest ref\" pattern: sync callback to ref to avoid effect re-runs\n onErrorRef.current = onError;\n\n useEffect(() => {\n if (snapshot.error) {\n onErrorRef.current?.(\n snapshot.error,\n snapshot.toRoute,\n snapshot.fromRoute,\n );\n }\n // eslint-disable-next-line @eslint-react/exhaustive-deps -- onError tracked via ref, snapshot fields accessed inside callback\n }, [snapshot.version]);\n\n return (\n <Fragment>\n {children}\n {snapshot.error ? fallback(snapshot.error, snapshot.resetError) : null}\n </Fragment>\n );\n}\n","import { useContext } from \"preact/hooks\";\n\nimport { NavigatorContext } from \"../context\";\n\nimport type { Navigator } from \"@real-router/core\";\n\nexport const useNavigator = (): Navigator => {\n const navigator = useContext(NavigatorContext);\n\n if (!navigator) {\n throw new Error(\"useNavigator must be used within a RouterProvider\");\n }\n\n return navigator;\n};\n","import { getPluginApi } from \"@real-router/core/api\";\nimport { getRouteUtils } from \"@real-router/route-utils\";\n\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouteUtils } from \"@real-router/route-utils\";\n\nexport const useRouteUtils = (): RouteUtils => {\n const router = useRouter();\n\n return getRouteUtils(getPluginApi(router).getTree());\n};\n","import { useContext } from \"preact/hooks\";\n\nimport { RouteContext } from \"../context\";\n\nimport type { RouteContext as RouteContextType } from \"../types\";\nimport type { Params, State } from \"@real-router/core\";\n\nexport const useRoute = <P extends Params = Params>(): Omit<\n RouteContextType<P>,\n \"route\"\n> & { route: State<P> } => {\n const routeContext = useContext(RouteContext);\n\n if (!routeContext) {\n throw new Error(\"useRoute must be used within a RouterProvider\");\n }\n\n if (!routeContext.route) {\n throw new Error(\n \"useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?\",\n );\n }\n\n return routeContext as Omit<RouteContextType<P>, \"route\"> & {\n route: State<P>;\n };\n};\n","import { getTransitionSource } from \"@real-router/sources\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouterTransitionSnapshot } from \"@real-router/sources\";\n\nexport function useRouterTransition(): RouterTransitionSnapshot {\n const router = useRouter();\n const store = getTransitionSource(router);\n\n return useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n}\n","import { useEffect, useLayoutEffect, useRef } from \"preact/hooks\";\n\nimport { useRouter } from \"./useRouter\";\n\nimport type { State } from \"@real-router/core\";\n\nexport interface RouteExitContext {\n /** The route being left. */\n route: State;\n /** The route being navigated to. */\n nextRoute: State;\n /**\n * AbortSignal that fires when this navigation is superseded by a later\n * one (rapid clicks). Already filtered: when the handler runs,\n * `signal.aborted` is guaranteed to be `false`. Use\n * `signal.addEventListener(\"abort\", cleanup, { once: true })` for\n * cleanup that must run on cancellation.\n */\n signal: AbortSignal;\n}\n\nexport interface UseRouteExitOptions {\n /**\n * Skip the handler when `route.name === nextRoute.name`\n * (sort/filter/query-only navigations on the same route). Default:\n * `true`.\n */\n skipSameRoute?: boolean;\n}\n\nexport type RouteExitHandler = (\n context: RouteExitContext,\n) => void | Promise<void>;\n\n/**\n * Subscribe to the router's leave-window with the universal guards baked\n * in. Wraps `router.subscribeLeave` so consumers don't repeat the same\n * boilerplate every time:\n *\n * - **Reentrant abort pre-check**: if `signal.aborted` is already `true`\n * when the handler would run (rapid navigation superseded a slower\n * one), the handler is skipped entirely. `signal.addEventListener(\n * \"abort\", ...)` does not fire retroactively, so without this guard\n * downstream cleanup would never trigger.\n * - **Same-route skip**: by default, `route.name === nextRoute.name`\n * short-circuits the handler — query-only navigations (sort, filter,\n * pagination) skip the work. Opt out with `skipSameRoute: false`.\n * - **Stable handler reference**: the handler can change identity on\n * every render without causing resubscription — internal ref keeps\n * the latest handler accessible to the long-lived subscription.\n *\n * Returns nothing — the subscription's lifecycle is bound to the\n * component's mount.\n *\n * If the handler returns a Promise, the router blocks on it. If the\n * Promise resolves, navigation proceeds. If it rejects, the router emits\n * `TRANSITION_CANCELLED` (existing core behavior, no change here).\n *\n * @example Animation\n * ```tsx\n * const ref = useRef<HTMLDivElement>(null);\n *\n * useRouteExit(async ({ signal }) => {\n * const el = ref.current;\n * if (!el) return;\n * el.classList.add(\"fade-out\");\n * const cleanup = () => el.classList.remove(\"fade-out\");\n * signal.addEventListener(\"abort\", cleanup, { once: true });\n * try {\n * el.getBoundingClientRect(); // style flush\n * await Promise.allSettled(el.getAnimations().map((a) => a.finished));\n * } finally {\n * cleanup();\n * }\n * });\n * ```\n *\n * @example Auto-save form draft\n * ```tsx\n * useRouteExit(async ({ signal }) => {\n * if (formState.dirty) await api.saveDraft(formState, { signal });\n * });\n * ```\n *\n * @example Cancel inflight requests\n * ```tsx\n * useRouteExit(() => {\n * inflightController.abort();\n * });\n * ```\n *\n * @example Library-coordinated exit (motion / framer-motion)\n * ```tsx\n * const exitResolverRef = useRef<(() => void) | null>(null);\n *\n * useRouteExit(({ signal }) => {\n * return new Promise<void>((resolve) => {\n * exitResolverRef.current = resolve;\n * signal.addEventListener(\"abort\", () => resolve(), { once: true });\n * });\n * });\n *\n * const onExitComplete = () => exitResolverRef.current?.();\n * // pass onExitComplete to <AnimatePresence>\n * ```\n *\n * @example Reading rich transition metadata via `nextRoute.transition`\n * ```tsx\n * useRouteExit(({ route, nextRoute }) => {\n * // nextRoute.transition: TransitionMeta — preview of the upcoming nav\n * if (nextRoute.transition.segments.deactivated.includes(\"products\")) {\n * // leaving the products subtree entirely — flush product-related caches\n * productCache.clear();\n * }\n * if (nextRoute.transition.redirected) {\n * // skip animation when navigation arrived via redirect\n * return;\n * }\n * });\n * ```\n */\nexport function useRouteExit(\n handler: RouteExitHandler,\n options?: UseRouteExitOptions,\n): void {\n const router = useRouter();\n const handlerRef = useRef(handler);\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n // Keep the latest handler accessible to the subscription without\n // resubscribing on every render — the subscription registers the\n // wrapper once and dispatches to whatever ref points to.\n // useLayoutEffect (synchronous, post-render, pre-paint) updates the\n // ref before the browser can dispatch any router events that could\n // observe a stale closure.\n useLayoutEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n return router.subscribeLeave(({ route, nextRoute, signal }) => {\n if (skipSameRoute && route.name === nextRoute.name) {\n return;\n }\n\n // Reentrant abort: signal is already aborted when listener fires\n // (e.g. a newer navigate superseded this one before subscribeLeave\n // even ran). addEventListener(\"abort\", ...) does not fire\n // retroactively, so we skip the handler entirely — any cleanup the\n // handler would have wired via abort listener must not run because\n // the corresponding `add` work never happened.\n if (signal.aborted) {\n return;\n }\n\n return handlerRef.current({ route, nextRoute, signal });\n });\n }, [router, skipSameRoute]);\n}\n","import { useEffect, useLayoutEffect, useRef } from \"preact/hooks\";\n\nimport { useRoute } from \"./useRoute\";\n\nimport type { State } from \"@real-router/core\";\n\nexport interface RouteEnterContext {\n /** The route that was just activated. */\n route: State;\n /** The route that was active immediately before this navigation. */\n previousRoute: State;\n}\n\nexport type RouteEnterHandler = (context: RouteEnterContext) => void;\n\nexport interface UseRouteEnterOptions {\n /**\n * Skip the handler when `route.name === previousRoute.name`\n * (sort/filter/query-only navigations on the same route). Default:\n * `true`. Symmetric with `useRouteExit`'s same-name option.\n */\n skipSameRoute?: boolean;\n}\n\n/**\n * Fire `handler` once when the component mounts as a result of a\n * navigation. Mirror of `useRouteExit` for the entry side.\n *\n * What this hook covers that ad-hoc `useEffect` + `useRoute()` doesn't:\n *\n * - **Skip-initial**: handler is skipped when there is no\n * `previousRoute` (i.e. first-load mount). Most consumers want to\n * fire side effects only on real navigations, not on hydration.\n * - **Same-route skip** (default): handler is skipped when\n * `route.name === previousRoute.name`. Sort/filter/query-only\n * navigations re-run the effect (because `route` reference changes\n * in `useRoute`'s snapshot), but they are not \"entries\" in the\n * animation / analytics sense — the component instance has stayed\n * mounted throughout. Opt out with `skipSameRoute: false` when\n * the handler legitimately needs to fire on every navigation\n * (e.g. analytics tracking each query-param flip).\n * - **Latest-handler ref**: the handler can change identity on every\n * render without re-running the effect — the registered wrapper\n * dispatches to whatever `handlerRef.current` points to.\n * - **Mount-time `route` / `previousRoute` snapshot**: the handler\n * receives the values that were live at the moment of mount, not\n * the latest ones (which may have moved on if the user navigated\n * again before the effect drained).\n *\n * Race-safety: `useRoute()` is wired through `useSyncExternalStore` from\n * `@real-router/sources` (Preact polyfill: useState + useEffect, same\n * post-commit semantics), so by the time the new component's effect\n * runs, the snapshot is the post-commit one. This is the reason we can\n * read mount-time context from `useRoute()` instead of subscribing to\n * `router.subscribe` directly (which fires before Preact schedules a\n * re-render — the well-known race in distributed components).\n *\n * Note: Preact does not expose a `StrictMode` equivalent, so the\n * `lastHandledRouteRef` guard exists primarily for defensive symmetry\n * with the React implementation. It is harmless in Preact.\n *\n * @example Direction-aware entry animation\n * ```tsx\n * useRouteEnter(({ route }) => {\n * const direction = route.context.browser?.direction;\n * ref.current?.classList.add(\n * direction === \"back\" ? \"slide-from-left\" : \"slide-from-right\",\n * );\n * });\n * ```\n *\n * @example Source-aware focus management\n * ```tsx\n * useRouteEnter(({ route }) => {\n * if (route.context.browser?.source === \"navigate\") {\n * headingRef.current?.focus();\n * }\n * });\n * ```\n *\n * @example Analytics page-enter event (skip-initial built-in)\n * ```tsx\n * useRouteEnter(({ route, previousRoute }) => {\n * analytics.track(\"page_enter\", {\n * route: route.name,\n * from: previousRoute.name,\n * });\n * });\n * ```\n *\n * @example Reading rich transition metadata via `route.transition`\n * ```tsx\n * useRouteEnter(({ route }) => {\n * // route.transition: TransitionMeta — populated by core for every state\n * if (route.transition.redirected) {\n * showToast(`Redirected from ${route.transition.from}`);\n * }\n * if (route.transition.segments.activated.includes(\"products\")) {\n * // products subtree just became active (could be products or\n * // products.detail). Useful for subtree-scoped side effects.\n * }\n * });\n * ```\n */\nexport function useRouteEnter(\n handler: RouteEnterHandler,\n options?: UseRouteEnterOptions,\n): void {\n const { route, previousRoute } = useRoute();\n const handlerRef = useRef(handler);\n const lastHandledRouteRef = useRef<State | null>(null);\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n // Keep the latest handler reference accessible without re-running\n // the effect. useLayoutEffect (synchronous, post-render, pre-paint)\n // updates the ref before the effect can read it.\n useLayoutEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n // Early-exit guards, top-down:\n //\n // - **Skip-initial**: `state.transition.from` is undefined only\n // for the very first state committed by `router.start()`.\n // - **Skip-same-route**: query-only navigations have\n // `transition.from === route.name`. Opt-out via\n // `skipSameRoute: false`.\n // - **Defensive dedupe**: same `route` ref between effect\n // cleanup + re-run. Preact has no StrictMode, but we keep the\n // guard for parity with React; v8-ignored.\n if (!route.transition.from) {\n return;\n }\n if (skipSameRoute && route.transition.from === route.name) {\n return;\n }\n /* v8 ignore start */\n if (lastHandledRouteRef.current === route || !previousRoute) {\n return;\n }\n /* v8 ignore stop */\n\n lastHandledRouteRef.current = route;\n handlerRef.current({ route, previousRoute });\n }, [route, previousRoute, skipSameRoute]);\n}\n","import { getNavigator } from \"@real-router/core\";\nimport { createRouteSource } from \"@real-router/sources\";\nimport { useEffect, useMemo } from \"preact/hooks\";\n\nimport { NavigatorContext, RouteContext, RouterContext } from \"./context\";\nimport {\n createRouteAnnouncer,\n createScrollRestoration,\n createViewTransitions,\n} from \"./dom-utils\";\nimport { useSyncExternalStore } from \"./useSyncExternalStore\";\n\nimport type { ScrollRestorationOptions } from \"./dom-utils\";\nimport type { Router } from \"@real-router/core\";\nimport type { FunctionComponent, ComponentChildren } from \"preact\";\n\nexport interface RouteProviderProps {\n router: Router;\n children: ComponentChildren;\n announceNavigation?: boolean;\n scrollRestoration?: ScrollRestorationOptions;\n viewTransitions?: boolean;\n}\n\nexport const RouterProvider: FunctionComponent<RouteProviderProps> = ({\n router,\n children,\n announceNavigation,\n scrollRestoration,\n viewTransitions,\n}) => {\n useEffect(() => {\n if (!announceNavigation) {\n return;\n }\n\n const announcer = createRouteAnnouncer(router);\n\n return () => {\n announcer.destroy();\n };\n }, [announceNavigation, router]);\n\n // Primitive deps so inline `{ mode: \"restore\" }` doesn't thrash on every\n // render. scrollContainer is a getter invoked lazily on every event inside\n // the utility — swapping its reference doesn't change the resolved element,\n // so we intentionally omit it from deps to keep inline getters stable.\n const srMode = scrollRestoration?.mode;\n const srAnchor = scrollRestoration?.anchorScrolling;\n const srBehavior = scrollRestoration?.behavior;\n const srStorageKey = scrollRestoration?.storageKey;\n const srEnabled = scrollRestoration !== undefined;\n\n useEffect(() => {\n if (!srEnabled) {\n return;\n }\n\n const sr = createScrollRestoration(router, {\n mode: srMode,\n anchorScrolling: srAnchor,\n behavior: srBehavior,\n storageKey: srStorageKey,\n // srEnabled check above guarantees scrollRestoration is defined.\n scrollContainer: scrollRestoration.scrollContainer,\n });\n\n return () => {\n sr.destroy();\n };\n // scrollRestoration (for scrollContainer) omitted — see comment above.\n // eslint-disable-next-line @eslint-react/exhaustive-deps\n }, [router, srEnabled, srMode, srAnchor, srBehavior, srStorageKey]);\n\n useEffect(() => {\n if (!viewTransitions) {\n return;\n }\n\n const vt = createViewTransitions(router);\n\n return () => {\n vt.destroy();\n };\n }, [router, viewTransitions]);\n\n const navigator = useMemo(() => getNavigator(router), [router]);\n\n // useSyncExternalStore manages the router subscription lifecycle:\n // subscribe connects to router on first listener, unsubscribes on last.\n const store = useMemo(() => createRouteSource(router), [router]);\n const { route, previousRoute } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot, // SSR: router returns same state on server and client\n );\n\n const routeContextValue = useMemo(\n () => ({ navigator, route, previousRoute }),\n [navigator, route, previousRoute],\n );\n\n return (\n <RouterContext.Provider value={router}>\n <NavigatorContext.Provider value={navigator}>\n <RouteContext.Provider value={routeContextValue}>\n {children}\n </RouteContext.Provider>\n </NavigatorContext.Provider>\n </RouterContext.Provider>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["NOOP_INSTANCE","NOOP_INSTANCE"],"sources":["../../src/components/RouteView/components.tsx","../../src/components/RouteView/helpers.tsx","../../src/useSyncExternalStore.ts","../../src/hooks/useNavigator.tsx","../../src/hooks/useRouter.tsx","../../src/hooks/useRouteNode.tsx","../../src/components/RouteView/RouteView.tsx","../../src/constants.ts","../../../../shared/dom-utils/route-announcer.ts","../../../../shared/dom-utils/scroll-restore.ts","../../../../shared/dom-utils/view-transitions.ts","../../../../shared/dom-utils/link-utils.ts","../../src/hooks/useIsActiveRoute.tsx","../../src/components/Link.tsx","../../src/components/RouterErrorBoundary.tsx","../../src/hooks/useRouteUtils.tsx","../../src/hooks/useRouterTransition.tsx","../../src/hooks/useRouteExit.tsx","../../src/hooks/useRouteEnter.tsx","../../src/RouterProvider.tsx"],"sourcesContent":["import type { MatchProps, NotFoundProps, SelfProps } from \"./types\";\n\nexport function Match(_props: MatchProps): null {\n return null;\n}\n\nMatch.displayName = \"RouteView.Match\";\n\nexport function Self(_props: SelfProps): null {\n return null;\n}\n\nSelf.displayName = \"RouteView.Self\";\n\nexport function NotFound(_props: NotFoundProps): null {\n return null;\n}\n\nNotFound.displayName = \"RouteView.NotFound\";\n","import { UNKNOWN_ROUTE } from \"@real-router/core\";\nimport { startsWithSegment } from \"@real-router/route-utils\";\nimport { Fragment, isValidElement, toChildArray } from \"preact\";\nimport { Suspense } from \"preact/compat\";\n\nimport { Match, NotFound, Self } from \"./components\";\n\nimport type { MatchProps, NotFoundProps, SelfProps } from \"./types\";\nimport type { VNode, ComponentChildren } from \"preact\";\n\ninterface FallbackSlots {\n selfChildren: ComponentChildren;\n selfFallback: ComponentChildren | undefined;\n selfFound: boolean;\n notFoundChildren: ComponentChildren;\n}\n\nfunction isSegmentMatch(\n routeName: string,\n fullSegmentName: string,\n exact: boolean,\n): boolean {\n if (fullSegmentName === \"\") {\n return false;\n }\n\n if (exact) {\n return routeName === fullSegmentName;\n }\n\n return startsWithSegment(routeName, fullSegmentName);\n}\n\nexport function collectElements(\n children: ComponentChildren,\n result: VNode[],\n): void {\n for (const child of toChildArray(children)) {\n if (!isValidElement(child)) {\n continue;\n }\n\n if (\n child.type === Match ||\n child.type === Self ||\n child.type === NotFound\n ) {\n result.push(child);\n } else {\n collectElements(\n (child.props as { readonly children: ComponentChildren }).children,\n result,\n );\n }\n }\n}\n\nfunction renderSlot(\n slotChildren: ComponentChildren,\n key: string,\n fallback?: ComponentChildren,\n): VNode {\n const content =\n fallback === undefined ? (\n slotChildren\n ) : (\n <Suspense fallback={fallback}>{slotChildren}</Suspense>\n );\n\n return <Fragment key={key}>{content}</Fragment>;\n}\n\nfunction isFallbackKind(child: VNode): boolean {\n return child.type === NotFound || child.type === Self;\n}\n\nfunction assignFallbackSlot(child: VNode, slots: FallbackSlots): void {\n if (child.type === NotFound) {\n slots.notFoundChildren = (child.props as NotFoundProps).children;\n\n return;\n }\n\n if (!slots.selfFound) {\n slots.selfChildren = (child.props as SelfProps).children;\n slots.selfFallback = (child.props as SelfProps).fallback;\n slots.selfFound = true;\n }\n}\n\nfunction processMatch(\n child: VNode,\n routeName: string,\n nodeName: string,\n alreadyActive: boolean,\n): VNode | null {\n const {\n segment,\n exact = false,\n fallback,\n children,\n } = child.props as MatchProps;\n const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;\n const isActive =\n !alreadyActive && isSegmentMatch(routeName, fullSegmentName, exact);\n\n if (!isActive) {\n return null;\n }\n\n return renderSlot(children, fullSegmentName, fallback);\n}\n\nfunction appendFallback(\n rendered: VNode[],\n routeName: string,\n nodeName: string,\n slots: FallbackSlots,\n): void {\n if (slots.selfFound && routeName === nodeName) {\n rendered.push(\n renderSlot(slots.selfChildren, \"__route-view-self__\", slots.selfFallback),\n );\n\n return;\n }\n\n if (routeName === UNKNOWN_ROUTE && slots.notFoundChildren !== null) {\n rendered.push(\n <Fragment key=\"__route-view-not-found__\">\n {slots.notFoundChildren}\n </Fragment>,\n );\n }\n}\n\nexport function buildRenderList(\n elements: VNode[],\n routeName: string,\n nodeName: string,\n): { rendered: VNode[]; activeMatchFound: boolean } {\n const slots: FallbackSlots = {\n selfChildren: null,\n selfFallback: undefined,\n selfFound: false,\n notFoundChildren: null,\n };\n let activeMatchFound = false;\n const rendered: VNode[] = [];\n\n for (const child of elements) {\n if (isFallbackKind(child)) {\n assignFallbackSlot(child, slots);\n\n continue;\n }\n\n const matchRendered = processMatch(\n child,\n routeName,\n nodeName,\n activeMatchFound,\n );\n\n if (matchRendered !== null) {\n activeMatchFound = true;\n rendered.push(matchRendered);\n }\n }\n\n if (!activeMatchFound) {\n appendFallback(rendered, routeName, nodeName, slots);\n }\n\n return { rendered, activeMatchFound };\n}\n","import { useEffect, useState } from \"preact/hooks\";\n\n/**\n * Polyfill for React's useSyncExternalStore.\n *\n * Preact does not provide a native useSyncExternalStore.\n * This implementation uses useState + useEffect to subscribe\n * to external stores.\n *\n * Race condition handling: the value may change between\n * `useState(getSnapshot)` (render) and `useEffect` (commit).\n * We synchronize by calling `setValue(getSnapshot())` before\n * subscribing in the effect.\n *\n * The updater uses `Object.is` to bail out when the snapshot\n * is referentially stable, preventing redundant re-renders.\n *\n * SSR semantics: `_getServerSnapshot` is intentionally ignored.\n * Preact's `preact-render-to-string` runs `useState(getSnapshot)`\n * on the server and does not commit effects, so the initial\n * render already uses `getSnapshot()`. Real-Router's `createRouteSource`\n * (and friends) return the same value on server and client given the\n * same `router` instance, so passing `getSnapshot` itself as the third\n * argument at every call site is the symmetric SSR contract; a separate\n * `getServerSnapshot` would diverge during hydration. Consumers that\n * truly need a different server value should branch in `getSnapshot`.\n *\n * Stable-reference contract: `subscribe` and `getSnapshot` are deps of\n * the subscription effect. If a consumer passes inline closures, every\n * render triggers `unsubscribe → subscribe` plus a fresh `sync()` pass\n * — a silent O(N) reconnect that the Preact polyfill cannot bail out\n * of (React's native impl uses an internal sub-store keyed by identity;\n * we cannot replicate that without losing the latest-snapshot guarantee).\n * All Real-Router hooks pass router-keyed cached factories from\n * `@real-router/sources`, which produce stable refs per `(router, args…)`\n * — keep that pattern for every external use.\n */\nexport function useSyncExternalStore<T>(\n subscribe: (onStoreChange: () => void) => () => void,\n getSnapshot: () => T,\n _getServerSnapshot?: () => T,\n): T {\n const [value, setValue] = useState(getSnapshot);\n\n useEffect(() => {\n const sync = (): void => {\n setValue((prev) => {\n const next = getSnapshot();\n\n return Object.is(prev, next) ? prev : next;\n });\n };\n\n sync();\n\n return subscribe(sync);\n }, [subscribe, getSnapshot]);\n\n return value;\n}\n","import { createUseContextOrThrow, NavigatorContext } from \"../context\";\n\nimport type { Navigator } from \"@real-router/core\";\n\nexport const useNavigator: () => Navigator = createUseContextOrThrow(\n NavigatorContext,\n \"useNavigator\",\n);\n","import { createUseContextOrThrow, RouterContext } from \"../context\";\n\nimport type { Router } from \"@real-router/core\";\n\nexport const useRouter: () => Router = createUseContextOrThrow(\n RouterContext,\n \"useRouter\",\n);\n","import { createRouteNodeSource } from \"@real-router/sources\";\nimport { useMemo } from \"preact/hooks\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useNavigator } from \"./useNavigator\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouteContext } from \"../types\";\n\nexport function useRouteNode(nodeName: string): RouteContext {\n const router = useRouter();\n const navigator = useNavigator();\n\n // `createRouteNodeSource` is the cached factory from `@real-router/sources`\n // keyed on (router, nodeName) — identical args return identical refs across\n // renders. No `useMemo` needed.\n const store = createRouteNodeSource(router, nodeName);\n\n const { route, previousRoute } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n\n // Public stable-ref contract (locked by `useRouteNode.test.tsx` \"should\n // return stable reference when nothing changes\"): consecutive renders with\n // identical (navigator, route, previousRoute) return the same RouteContext\n // ref. Drop this `useMemo` and downstream consumers re-render on every\n // parent re-render.\n return useMemo(\n (): RouteContext => ({ navigator, route, previousRoute }),\n [navigator, route, previousRoute],\n );\n}\n","import { useMemo } from \"preact/hooks\";\n\nimport { Match, NotFound, Self } from \"./components\";\nimport { buildRenderList, collectElements } from \"./helpers\";\nimport { useRouteNode } from \"../../hooks/useRouteNode\";\n\nimport type { RouteViewProps } from \"./types\";\nimport type { VNode } from \"preact\";\n\nfunction RouteViewRoot({\n nodeName,\n children,\n}: Readonly<RouteViewProps>): VNode | null {\n const { route } = useRouteNode(nodeName);\n\n // Cache the flattened Match/Self/NotFound list across renders with unchanged\n // children. children only differs when the parent re-renders with a new\n // node, so this memoises the steady-state traversal.\n const elements = useMemo(() => {\n const collected: VNode[] = [];\n\n collectElements(children, collected);\n\n return collected;\n }, [children]);\n\n const routeName = route?.name;\n\n // buildRenderList is O(N) over Match/Self/NotFound children. Memo on\n // (elements, routeName, nodeName) skips the re-walk on parent re-renders\n // that don't change the active route; navigations always invalidate via\n // routeName.\n const rendered = useMemo(() => {\n if (routeName === undefined) {\n return [];\n }\n\n return buildRenderList(elements, routeName, nodeName).rendered;\n }, [elements, routeName, nodeName]);\n\n return rendered.length > 0 ? <>{rendered}</> : null;\n}\n\nRouteViewRoot.displayName = \"RouteView\";\n\nexport const RouteView = Object.assign(RouteViewRoot, {\n Match,\n Self,\n NotFound,\n});\n\nexport type {\n RouteViewProps,\n MatchProps as RouteViewMatchProps,\n SelfProps as RouteViewSelfProps,\n NotFoundProps as RouteViewNotFoundProps,\n} from \"./types\";\n","/**\n * Stable empty object for default params\n */\nexport const EMPTY_PARAMS = Object.freeze({});\n\n/**\n * Stable empty options object\n */\nexport const EMPTY_OPTIONS = Object.freeze({});\n","import type { Router, State } from \"@real-router/core\";\n\nconst CLEAR_DELAY = 7000;\nconst SAFARI_READY_DELAY = 100;\nconst ANNOUNCER_ATTR = \"data-real-router-announcer\";\nconst INTERNAL_ROUTE_PREFIX = \"@@\";\nconst VISUALLY_HIDDEN =\n \"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0\";\n\nexport interface RouteAnnouncerOptions {\n prefix?: string;\n getAnnouncementText?: (route: State) => string;\n}\n\nconst NOOP_INSTANCE: { destroy: () => void } = Object.freeze({\n destroy: () => {\n /* no-op */\n },\n});\n\nexport function createRouteAnnouncer(\n router: Router,\n options?: RouteAnnouncerOptions,\n): { destroy: () => void } {\n // Defensive SSR / non-browser guard: in SSR (Node.js) or non-DOM\n // environments, `document` is undefined and the announcer cannot\n // attach its aria-live region. Return a frozen NOOP_INSTANCE — same\n // pattern as `createDirectionTracker`, `createScrollRestoration`, and\n // `createViewTransitions`. Without this guard, `NavigationAnnouncer`\n // component construction would throw `ReferenceError: document is not\n // defined` under `@angular/ssr` rendering, tearing down the whole SSR\n // bootstrap. Closes review-2026-05-10 §5.10 ⛔ \"NavigationAnnouncer\n // SSR mode\" MED.\n if (typeof document === \"undefined\") {\n return NOOP_INSTANCE;\n }\n\n const prefix = options?.prefix ?? \"Navigated to \";\n const getCustomText = options?.getAnnouncementText;\n\n let isInitialNavigation = true;\n let isReady = false;\n let isDestroyed = false;\n let lastAnnouncedText = \"\";\n let pendingText: string | null = null;\n let clearTimeoutId: ReturnType<typeof setTimeout> | undefined;\n\n const announcer = getOrCreateAnnouncer();\n\n const doAnnounce = (text: string, h1: HTMLElement | null): void => {\n lastAnnouncedText = text;\n clearTimeout(clearTimeoutId);\n announcer.textContent = text;\n clearTimeoutId = setTimeout(() => {\n announcer.textContent = \"\";\n lastAnnouncedText = \"\";\n }, CLEAR_DELAY);\n\n manageFocus(h1);\n };\n\n // Safari-ready delay: announcing before VoiceOver wires up the aria-live region\n // causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms\n // before marking the announcer \"ready\" — any navigation during that window is\n // buffered in pendingText and flushed once the delay expires.\n const safariTimeoutId = setTimeout(() => {\n isReady = true;\n\n if (pendingText !== null && !isDestroyed) {\n const text = pendingText;\n\n pendingText = null;\n doAnnounce(text, document.querySelector<HTMLElement>(\"h1\"));\n }\n }, SAFARI_READY_DELAY);\n\n const unsubscribe = router.subscribe(({ route }) => {\n if (isInitialNavigation) {\n isInitialNavigation = false;\n\n return;\n }\n\n // Double rAF: waits for two paint frames so the incoming route's DOM\n // (including the new <h1>) is fully rendered before resolveText reads it.\n // Single rAF fires before the new route's template has been attached,\n // which would cause resolveText to pick up the OLD h1 or fall back to\n // document.title / route.name prematurely.\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (isDestroyed) {\n return;\n }\n\n const h1 = document.querySelector<HTMLElement>(\"h1\");\n const text = resolveText(route, prefix, getCustomText, h1);\n\n if (!text || text === lastAnnouncedText) {\n return;\n }\n\n if (!isReady) {\n // Defer announcement until Safari-ready window elapses (see safariTimeoutId).\n pendingText = text;\n\n return;\n }\n\n doAnnounce(text, h1);\n });\n });\n });\n\n return {\n destroy() {\n isDestroyed = true;\n unsubscribe();\n clearTimeout(clearTimeoutId);\n clearTimeout(safariTimeoutId);\n removeAnnouncer();\n },\n };\n}\n\nfunction getOrCreateAnnouncer(): HTMLElement {\n const existing = document.querySelector<HTMLElement>(`[${ANNOUNCER_ATTR}]`);\n\n if (existing) {\n return existing;\n }\n\n const element = document.createElement(\"div\");\n\n element.setAttribute(\"style\", VISUALLY_HIDDEN);\n element.setAttribute(\"aria-live\", \"assertive\");\n element.setAttribute(\"aria-atomic\", \"true\");\n element.setAttribute(ANNOUNCER_ATTR, \"\");\n\n // Defensive SSR / pre-`<body>` guard: in some environments (early\n // injection, deferred-body documents, certain SSR rehydration paths)\n // `document.body` can be null when the announcer is constructed.\n // `document.body.prepend(...)` would throw `TypeError: Cannot read\n // properties of null`, tearing down the consumer's RouterProvider /\n // NavigationAnnouncer mount. Fallback to `documentElement` keeps the\n // announcer working for SR users; visual-hidden styling means there is\n // no visible artifact regardless of mount point.\n //\n // TS dom lib types `document.body` as `HTMLElement` (non-null), but\n // runtime can return null per spec. The `as` cast narrows the type to\n // include null so the `??` short-circuit is type-safe.\n ((document.body as HTMLElement | null) ?? document.documentElement).prepend(\n element,\n );\n\n return element;\n}\n\nfunction removeAnnouncer(): void {\n document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();\n}\n\nfunction resolveText(\n route: State,\n prefix: string,\n getCustomText: ((route: State) => string) | undefined,\n h1: HTMLElement | null,\n): string {\n if (getCustomText) {\n try {\n const customText = getCustomText(route);\n\n // Mini-sprint E.4 (audit-5 §4.2 #4) — empty-string fallback.\n // A consumer pattern like\n // getAnnouncementText: (route) => myMap[route.name] ?? \"\"\n // returns `\"\"` for routes outside the map. The subscribe loop\n // then sees an empty text and silently no-announces — screen\n // readers stay quiet without any signal to the developer. Treat\n // a falsy custom result (`\"\"` / `null` / `undefined`) as\n // \"consumer doesn't have a name for this route\" and fall through\n // to the default resolution chain (h1 → title → route name).\n if (customText) {\n return customText;\n }\n } catch (error) {\n // A throwing consumer callback inside the router's subscribe loop\n // would tear down sibling listeners — log and fall through to the\n // built-in resolution chain so the announcer keeps working.\n console.error(\n \"[real-router] getAnnouncementText threw; falling back to default resolution.\",\n error,\n );\n }\n }\n\n const h1Text = (h1?.textContent ?? \"\").trim();\n const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)\n ? \"\"\n : route.name;\n const rawText =\n h1Text || document.title || routeName || globalThis.location.pathname;\n\n return `${prefix}${rawText}`;\n}\n\nfunction manageFocus(h1: HTMLElement | null): void {\n if (!h1) {\n return;\n }\n\n if (!h1.hasAttribute(\"tabindex\")) {\n h1.setAttribute(\"tabindex\", \"-1\");\n }\n\n h1.focus({ preventScroll: true });\n}\n","import type { Router, State } from \"@real-router/core\";\n\nconst DEFAULT_STORAGE_KEY = \"real-router:scroll\";\n\nconst NOOP_INSTANCE: { destroy: () => void } = Object.freeze({\n destroy: () => {\n /* no-op */\n },\n});\n\nexport type ScrollRestorationMode = \"restore\" | \"top\" | \"native\";\n\nexport interface ScrollRestorationOptions {\n mode?: ScrollRestorationMode | undefined;\n anchorScrolling?: boolean | undefined;\n scrollContainer?: (() => HTMLElement | null) | undefined;\n /**\n * Scroll behavior passed to `scrollTo({ behavior })` and\n * `scrollIntoView({ behavior })`.\n *\n * - `\"auto\"` (default) — browser-defined, usually instant.\n * - `\"instant\"` — explicit instant jump (no animation).\n * - `\"smooth\"` — animated transition. Note: smooth restore on back/traverse\n * can feel disorienting if the user expects to land at the saved position\n * immediately. Recommended for `mode: \"top\"` or anchor scroll only.\n *\n * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions/behavior).\n */\n behavior?: ScrollBehavior | undefined;\n /**\n * sessionStorage key used to persist saved scroll positions. Default:\n * `\"real-router:scroll\"`. Override only when multiple independent\n * `RouterProvider` instances share the same document and you need to\n * isolate their scroll stores (e.g. micro-frontends, embedded widgets,\n * or testing). For a single app with one provider the default is fine.\n */\n storageKey?: string | undefined;\n}\n\ninterface NavigationContext {\n direction?: \"forward\" | \"back\" | \"unknown\";\n navigationType?: \"push\" | \"replace\" | \"traverse\" | \"reload\";\n}\n\nexport function createScrollRestoration(\n router: Router,\n options?: ScrollRestorationOptions,\n): { destroy: () => void } {\n if (typeof globalThis.window === \"undefined\") {\n return NOOP_INSTANCE;\n }\n\n const mode = options?.mode ?? \"restore\";\n\n // mode \"native\" = utility does nothing. Don't flip history.scrollRestoration,\n // don't subscribe, don't register pagehide — `history.scrollRestoration`\n // stays at the browser default (\"auto\") so the browser handles scroll\n // restore natively. (Note: this is the OPPOSITE of `history.scrollRestoration\n // === \"manual\"` — utility's \"native\" leaves the DOM property at \"auto\" so\n // the browser is in charge.)\n if (mode === \"native\") {\n return NOOP_INSTANCE;\n }\n\n const anchorEnabled = options?.anchorScrolling ?? true;\n const getContainer = options?.scrollContainer;\n const behavior: ScrollBehavior = options?.behavior ?? \"auto\";\n const storageKey = options?.storageKey ?? DEFAULT_STORAGE_KEY;\n\n // Write-through in-memory cache: parse sessionStorage once per provider\n // mount, then mutate in-memory. Avoids a JSON.parse + JSON.stringify pair\n // on every subscribeLeave / pagehide event.\n let store: Record<string, number> | undefined;\n\n const loadStore = (): Record<string, number> => {\n if (store !== undefined) {\n return store;\n }\n\n try {\n const raw = sessionStorage.getItem(storageKey);\n\n store = raw ? (JSON.parse(raw) as Record<string, number>) : {};\n } catch {\n store = {};\n }\n\n return store;\n };\n\n const putPos = (key: string, pos: number): void => {\n try {\n const cached = loadStore();\n\n // Skip-same-value: when a route is left at the same scroll position it\n // already holds in the cache (e.g. tab-switching without scrolling),\n // both the in-memory write and the JSON.stringify + setItem pair are\n // no-ops. Eliminates redundant serialization on the navigation hot\n // path for the common \"click tabs without scrolling\" case.\n if (cached[key] === pos) {\n return;\n }\n\n cached[key] = pos;\n sessionStorage.setItem(storageKey, JSON.stringify(cached));\n } catch {\n // Ignore quota / security errors.\n }\n };\n\n const prevScrollRestoration = history.scrollRestoration;\n\n try {\n history.scrollRestoration = \"manual\";\n } catch {\n // Ignore — some embedded contexts may reject the assignment.\n }\n\n // Resolve the container lazily on every event so containers mounted AFTER\n // the provider still get correct scroll handling. Falls back to window when\n // the getter is absent or returns null (pre-mount).\n const readPos = (): number => {\n const element = getContainer?.();\n\n return element ? element.scrollTop : globalThis.scrollY;\n };\n\n const writePos = (top: number): void => {\n const element = getContainer?.();\n\n if (element) {\n element.scrollTo({ top, left: 0, behavior });\n } else {\n globalThis.scrollTo({ top, left: 0, behavior });\n }\n };\n\n const scrollToHashOrTop = (route: State): void => {\n // URL plugin path (#532): `state.context.url.hash` is the source of truth\n // when one of the URL plugins (browser-plugin / navigation-plugin) is\n // installed. The value is already DECODED — feeding it through\n // `decodeURIComponent` again would throw on a bare `%`.\n const ctxHash = (route.context as { url?: { hash?: string } } | undefined)\n ?.url?.hash;\n\n if (ctxHash !== undefined) {\n if (anchorEnabled && ctxHash.length > 0) {\n // eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars\n const element = document.getElementById(ctxHash);\n\n if (element) {\n element.scrollIntoView({ behavior });\n\n return;\n }\n }\n\n writePos(0);\n\n return;\n }\n\n // Fallback path: no URL plugin, read the DOM. `location.hash` is\n // percent-encoded; ids in the DOM are the raw string, so decode for the\n // match. Fall back to the raw slice if the hash contains a malformed\n // escape sequence (decodeURIComponent throws on those).\n const hash = globalThis.location.hash;\n\n if (anchorEnabled && hash.length > 1) {\n let id: string;\n\n try {\n id = decodeURIComponent(hash.slice(1));\n } catch {\n id = hash.slice(1);\n }\n\n // eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars\n const element = document.getElementById(id);\n\n if (element) {\n element.scrollIntoView({ behavior });\n\n return;\n }\n }\n\n writePos(0);\n };\n\n let destroyed = false;\n let unserializableWarned = false;\n\n // `keyOf` defers to `canonicalJson` which calls `JSON.stringify`. Two\n // realistic inputs blow up the serializer and would otherwise crash the\n // subscribe callback (taking scroll-restore offline for the whole session):\n // - `BigInt` params → `TypeError: Do not know how to serialize a BigInt`\n // - cyclic params (reactive proxies, DOM-ref back-pointers) → stack\n // overflow.\n // The defensive wrapper drops capture/restore for that specific navigation\n // and warns once per provider — the rest of the cache stays usable.\n const safeKeyOf = (state: State): string | null => {\n try {\n return keyOf(state);\n } catch {\n if (!unserializableWarned) {\n unserializableWarned = true;\n console.error(\n `[real-router] scroll-restore: route \"${state.name}\" has params that cannot be canonicalized (e.g. BigInt or cyclic structure). Scroll position will not be captured or restored for this route.`,\n );\n }\n\n return null;\n }\n };\n\n const unsubscribe = router.subscribe(({ route, previousRoute }) => {\n const nav = (route.context as { navigation?: NavigationContext })\n .navigation;\n\n // Browsers dispatch reload as the initial navigation after refresh, so\n // previousRoute is undefined and capture is naturally skipped. The\n // pre-refresh position was already persisted via pagehide.\n if (previousRoute) {\n const prevKey = safeKeyOf(previousRoute);\n\n if (prevKey !== null) {\n putPos(prevKey, readPos());\n }\n }\n\n // Single rAF so DOM is committed before we read anchors / write scroll.\n // Guard against destroy() racing with the callback.\n requestAnimationFrame(() => {\n if (destroyed) {\n return;\n }\n\n if (mode === \"top\" || !nav) {\n scrollToHashOrTop(route);\n\n return;\n }\n\n if (nav.navigationType === \"replace\") {\n return;\n }\n\n if (\n nav.direction === \"back\" ||\n nav.navigationType === \"traverse\" ||\n nav.navigationType === \"reload\"\n ) {\n const key = safeKeyOf(route);\n\n writePos(key === null ? 0 : (loadStore()[key] ?? 0));\n\n return;\n }\n\n scrollToHashOrTop(route);\n });\n });\n\n const onPageHide = (): void => {\n const current = router.getState();\n\n if (current) {\n const key = safeKeyOf(current);\n\n if (key !== null) {\n putPos(key, readPos());\n }\n }\n };\n\n globalThis.addEventListener(\"pagehide\", onPageHide);\n\n return {\n destroy: () => {\n if (destroyed) {\n return;\n }\n\n destroyed = true;\n unsubscribe();\n globalThis.removeEventListener(\"pagehide\", onPageHide);\n\n try {\n history.scrollRestoration = prevScrollRestoration;\n } catch {\n // Ignore.\n }\n },\n };\n}\n\n/**\n * Internal cache-key builder for scroll-position storage.\n *\n * **Exported for testing only — not part of the public API** (intentionally\n * excluded from `index.ts` barrel). Adapter property tests import it via\n * the direct path to lock the `(name, canonicalJson(params))` key shape\n * as a regression guard (§8b H20 / audit-2026-05-16 #S3). A change to\n * key format would silently lose scroll positions across an upgrade —\n * the test set is the contract.\n *\n * ## Identity-based memoization (audit-2026-05-17 §8b #2)\n *\n * `State` objects emitted by core are frozen per-navigation: their\n * `name` / `params` are immutable for the lifetime of the snapshot, and\n * any change produces a new `State` reference. A `WeakMap<State, string>`\n * therefore safely caches the canonicalised key by identity — repeat\n * `keyOf(state)` calls on the same snapshot (typical on\n * back/forward/traverse where the same prior `State` is re-emitted)\n * skip the recursive `canonicalJson` pass entirely.\n *\n * The cache key is the `State` reference, so entries auto-release when\n * the snapshot is GC'd — no eviction needed.\n */\nconst KEY_CACHE = new WeakMap<State, string>();\n\nexport function keyOf(state: State): string {\n const cached = KEY_CACHE.get(state);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const key = `${state.name}:${canonicalJson(state.params)}`;\n\n KEY_CACHE.set(state, key);\n\n return key;\n}\n\n/**\n * Stable JSON serializer with sorted object keys.\n *\n * **Exported for testing only — not part of the public API** (intentionally\n * excluded from `index.ts` barrel). Adapter property tests import it via\n * the direct path to lock the key-order-insensitive property\n * (`canonicalJson({a:1,b:2}) === canonicalJson({b:2,a:1})`).\n *\n * ## Divergence from `@real-router/sources/canonicalJson` — by design\n *\n * Two independent implementations live in the monorepo:\n *\n * - **`shared/dom-utils/scroll-restore.canonicalJson`** (this file) — scroll\n * cache key builder. Uses `localeCompare` and a plain-object accumulator;\n * tolerates `__proto__`-keyed inputs only insofar as `JSON.stringify`'s\n * replacer happens to sort them; relies on `JSON.stringify`'s native cycle\n * detector. Designed to be cheap on the navigation hot path. The\n * surrounding [[safeKeyOf]] wrapper catches the two crash inputs (`BigInt`,\n * cyclic) and skips the offending capture/restore.\n *\n * - **`@real-router/sources/canonicalJson`** — sources cache key builder.\n * Uses byte-order compare (`< / >`) for locale-independence, a\n * `Object.create(null)` accumulator to prevent prototype pollution, and a\n * bespoke path-based cycle detector (the native one cannot see the cloned\n * graph). Throws eagerly on `Map`/`Set`/`RegExp`/cycles — the caller falls\n * back to a non-cached source.\n *\n * **They are intentionally NOT interchangeable.** Aligning them would either\n * regress scroll-restore performance (byte-order + recursive clone is heavier\n * per call) or weaken the sources cache (locale dependence breaks\n * deterministic cache keys across machines). No cross-package equivalence\n * test exists or should be added; the relationship is \"different invariants,\n * different costs, different consumers.\" Audit-2 / audit-2026-05-17 §2\n * documents the choice.\n */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(value, canonicalReplacer);\n}\n\nfunction canonicalReplacer(_key: string, val: unknown): unknown {\n // audit-2026-05-17 §5 MEDIUM (Sprint A.3) — function/Symbol marker.\n // `JSON.stringify` silently drops function and symbol values from\n // object output. Two routes that differ ONLY in a function/Symbol\n // value would canonicalize to the same string → silent scroll-cache\n // key collision (positions clobber each other). Replacing the value\n // with a sentinel string breaks the collision while keeping the\n // canonical form deterministic. The sentinels are intentionally\n // ASCII-only and lexically distinct from valid JSON-stringified\n // values; consumers will see `\"<fn>\"` / `\"<sym>\"` if they ever\n // round-trip the cache key, signalling the substitution clearly.\n if (typeof val === \"function\") {\n return \"<fn>\";\n }\n if (typeof val === \"symbol\") {\n return \"<sym>\";\n }\n\n if (val !== null && typeof val === \"object\" && !Array.isArray(val)) {\n // Null-prototype accumulator: a plain `{}` would interpret\n // `sorted[\"__proto__\"] = x` as a prototype assignment (silently dropped\n // from JSON.stringify output AND a prototype-pollution vector). Mirrors\n // the same guard in `@real-router/sources/canonicalJson`. The two\n // implementations are still intentionally divergent (see the doc-block\n // on [[canonicalJson]] above), but prototype-safety is non-negotiable\n // on both. Lock-test: scrollRestoreKey.properties.ts Invariant 11.\n const sorted = Object.create(null) as Record<string, unknown>;\n // eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable\n const keys = Object.keys(val as Record<string, unknown>).sort(\n (left: string, right: string) => left.localeCompare(right),\n );\n\n for (const key of keys) {\n sorted[key] = (val as Record<string, unknown>)[key];\n }\n\n return sorted;\n }\n\n return val;\n}\n","import type { Router } from \"@real-router/core\";\n\nexport interface ViewTransitions {\n destroy: () => void;\n}\n\nconst NOOP_INSTANCE: ViewTransitions = Object.freeze({\n destroy: () => {\n /* no-op */\n },\n});\n\nexport function createViewTransitions(router: Router): ViewTransitions {\n if (\n typeof document === \"undefined\" ||\n typeof document.startViewTransition !== \"function\"\n ) {\n return NOOP_INSTANCE;\n }\n\n let closeVT: (() => void) | null = null;\n let currentVT: { skipTransition?: () => void } | null = null;\n // Tracks whether TRANSITION_SUCCESS fired for the current leave. Used to\n // distinguish \"benign cleanup abort\" (router's async path aborts its own\n // controller in a finally block after successful navigation) from \"real\n // cancellation\" (concurrent navigate, guard rejection, dispose).\n let successFired = false;\n\n const resolveAndClear = (): void => {\n closeVT?.();\n closeVT = null;\n };\n\n const offLeave = router.subscribeLeave(({ signal }) => {\n // Reentrant abort: signal already aborted when we're called. Open no VT\n // — router will fall through to TRANSITION_CANCELLED via isCurrentNav()\n // after leave resolves. addEventListener(\"abort\", ...) does not re-fire\n // for past events, so skipping startViewTransition is the safe path.\n if (signal.aborted) {\n return;\n }\n\n successFired = false;\n resolveAndClear();\n\n // Return a Promise so the router awaits until the browser invokes\n // updateCallback. This ensures old DOM snapshot is captured BEFORE the\n // router commits the new state — giving correct exit→state→entry\n // ordering (vs fire-and-forget, where URL changes before VT captures).\n return new Promise<void>((resolveLeave) => {\n // Capture the resolver synchronously BEFORE startViewTransition() is\n // called. The browser invokes updateCallback in a later task, but\n // router.subscribe (TRANSITION_SUCCESS) can fire before that. If we\n // captured `resolve` inside the callback, subscribe would see closeVT\n // still null and skip resolving — the deferred would hang for 4s\n // until the VT API aborts with TimeoutError.\n const deferred = new Promise<void>((resolve) => {\n closeVT = resolve;\n });\n\n signal.addEventListener(\n \"abort\",\n () => {\n if (successFired) {\n // Router's async path (#finishAsyncNavigation) aborts its own\n // controller in a finally block AFTER completeTransition (and\n // thus AFTER subscribe fired). This is cleanup, not\n // cancellation — VT is progressing normally, do nothing.\n return;\n }\n\n // Real cancellation (concurrent navigate, dispose). Resolve the\n // deferred so updateCallback can complete, skip the VT so no\n // stale animation leaks, and unblock the router if the abort\n // fires before updateCallback was invoked.\n resolveAndClear();\n currentVT?.skipTransition?.();\n resolveLeave();\n },\n { once: true },\n );\n\n try {\n currentVT = document.startViewTransition(() => {\n // Resolving here unblocks the router at the moment the browser\n // enters updateCallback — by spec, old DOM snapshot is captured\n // before this callback runs. Router now proceeds through\n // activation guards and setState; the VT animation waits on\n // `deferred`, which is resolved from router.subscribe after a\n // task-queue tick (see NOTE on setTimeout below).\n resolveLeave();\n\n return deferred;\n });\n } catch {\n // Defensive: spec says startViewTransition doesn't throw under\n // normal conditions, but Chromium has had edge cases (detached\n // document, extension interference). Clean up and unblock router.\n resolveAndClear();\n resolveLeave();\n }\n });\n });\n\n const offSuccess = router.subscribe(() => {\n const resolver = closeVT;\n\n successFired = true;\n closeVT = null;\n\n if (resolver === null) {\n currentVT = null;\n } else {\n // CRITICAL: CANNOT use requestAnimationFrame here. When the router\n // takes the async path (leave returned a Promise), subscribe fires\n // AFTER the browser has already transitioned VT into the\n // \"update-callback-called\" phase. In that phase Chromium sets\n // rendering suppression to true, which ALSO blocks rAF callbacks.\n // rAF would never fire → deferred never resolves → browser aborts\n // vt.ready with TimeoutError after 4s (observed in Chromium).\n //\n // setTimeout runs on the task queue independent of the rendering\n // pipeline, so it fires regardless of suppression. React's scheduler\n // uses MessageChannel tasks, which are queued before our setTimeout,\n // so the new DOM is committed by the time our callback runs.\n setTimeout(() => {\n resolver();\n currentVT = null;\n }, 0);\n }\n });\n\n return {\n destroy: () => {\n offLeave();\n offSuccess();\n currentVT?.skipTransition?.();\n currentVT = null;\n resolveAndClear();\n },\n };\n}\n","import type {\n NavigationOptions,\n Params,\n Router,\n State,\n} from \"@real-router/core\";\n\nexport function shouldNavigate(evt: MouseEvent): boolean {\n return (\n evt.button === 0 &&\n !evt.metaKey &&\n !evt.altKey &&\n !evt.ctrlKey &&\n !evt.shiftKey\n );\n}\n\n// Matches a single percent-escape triple (`%` + two hex digits). Used as\n// the \"already-encoded\" probe in `encodeFragmentInline` below — see the\n// idempotency rationale there.\nconst PERCENT_ESCAPE_PROBE = /%[\\dA-Fa-f]{2}/;\n\n/**\n * RFC 3986 fragment encoding: preserve sub-delims (`&`, `=`, `?`, `:`),\n * encode space, `%`, control chars, non-ASCII via encodeURI; defensively\n * escape `#` (encodeURI does not). Mirrors `encodeHashFragment` in\n * `shared/browser-env/url-context.ts` — duplicated here because the\n * shared/dom-utils symlink graph does not reach shared/browser-env.\n *\n * **Idempotency for pre-encoded input (audit-2026-05-17 §5 MEDIUM E.1).**\n * The doc-comment on `<Link hash>` says the value is a \"decoded fragment\n * without leading #\". But realistic consumers copy hashes out of\n * `location.hash` (which is percent-encoded) and pass them back, so the\n * naive `encodeURI(\"%20\")` would double-encode into `\"%2520\"` and break\n * anchor lookup. We detect a percent-escape triple in the input and, if\n * present, decode + re-encode for idempotency. Malformed `%XX` (e.g.\n * `\"%2\"` or `\"%ZZ\"`) makes `decodeURIComponent` throw — in that case we\n * fall through to plain `encodeURI`, which never throws.\n */\nfunction encodeFragmentInline(decoded: string): string {\n if (PERCENT_ESCAPE_PROBE.test(decoded)) {\n try {\n const roundtrip = decodeURIComponent(decoded);\n\n return encodeURI(roundtrip).replaceAll(\"#\", \"%23\");\n } catch {\n // Malformed `%XX` — fall through to the plain encoding path.\n // encodeURI does not throw on malformed escapes; it treats the\n // `%` as a literal and percent-encodes it (`%2` → `%252`).\n }\n }\n\n return encodeURI(decoded).replaceAll(\"#\", \"%23\");\n}\n\ntype BuildUrlFn = (\n name: string,\n params: Params,\n options?: { hash?: string },\n) => string | undefined;\n\n/**\n * Builds an href for a `<Link>` element.\n *\n * - Prefers the URL plugin's `buildUrl` (browser-plugin, navigation-plugin,\n * hash-plugin) when present.\n * - Falls back to `router.buildPath` for runtimes without a URL plugin\n * (memory-plugin, console UIs, NativeScript). In that fallback the hash\n * is appended manually so the rendered href is still correct.\n * - The optional 4th argument is an options object so the contract stays\n * extensible. The `hash` option is a decoded fragment without leading \"#\";\n * `<Link hash=\"#section\">` is accepted defensively (leading \"#\" stripped).\n * Frozen API: previous 3-arg call sites continue to work unchanged.\n */\nexport function buildHref(\n router: Router,\n routeName: string,\n routeParams: Params,\n options?: { hash?: string },\n): string | undefined {\n try {\n const rawHash = options?.hash;\n let normHash: string | undefined;\n\n if (rawHash !== undefined) {\n normHash = rawHash.startsWith(\"#\") ? rawHash.slice(1) : rawHash;\n }\n\n const buildUrl = router.buildUrl as BuildUrlFn | undefined;\n\n if (buildUrl) {\n const url = buildUrl(\n routeName,\n routeParams,\n normHash === undefined ? undefined : { hash: normHash },\n );\n\n // Accept only non-empty strings. The BuildUrlFn type contract is\n // `string | undefined`, but defensive against:\n // - `\"\"` (empty string) → would render `<a href=\"\">`, which resolves\n // to the current page URL → silent self-navigation on click.\n // - `null` (type-contract violation) → would render `<a href={null}>`,\n // stringified to `\"null\"` in some renderers.\n // Either case falls through to the `router.buildPath` fallback below.\n if (typeof url === \"string\" && url.length > 0) {\n return url;\n }\n }\n\n const path = router.buildPath(routeName, routeParams);\n\n // Symmetric to the buildUrl guard above (#S1 audit, Invariant 12).\n // `router.buildPath` is typed `string`, but defends against:\n // - `\"\"` (empty string) — would render `<a href=\"\">`, which resolves\n // to the current page URL → silent self-navigation on click.\n // - non-string type-contract violations from custom path-matchers.\n // Both yield `undefined` (renderer drops the attribute) with a warning.\n if (typeof path !== \"string\" || path.length === 0) {\n console.error(\n `[real-router] Route \"${routeName}\" yielded an empty path. The element will render without an href attribute.`,\n );\n\n return undefined;\n }\n\n return normHash ? `${path}#${encodeFragmentInline(normHash)}` : path;\n } catch {\n console.error(\n `[real-router] Route \"${routeName}\" is not defined. The element will render without an href attribute.`,\n );\n\n return undefined;\n }\n}\n\n/**\n * `<Link>` click-handler navigation helper (#532).\n *\n * Wraps `router.navigate(name, params, opts)` with same-route different-hash\n * detection: when the consumer clicks a hash-bearing Link that targets the\n * current route with the same params but a different fragment, core's\n * SAME_STATES check would otherwise reject the navigation. The helper adds\n * `force: true` and `hashChange: true` automatically — subscribers can then\n * disambiguate via `state.context.url.hashChanged`.\n *\n * For pure programmatic same-route hash-only navigation, callers are\n * documented to pass `{ force: true }` themselves; the auto-bypass here is\n * a UX convenience for `<Link hash>` that all 6 framework adapters share.\n */\n/**\n * Local extended-options type. Adapters that depend only on `@real-router/core`\n * (without a URL plugin) do not see the `NavigationOptions` augmentation that\n * declares `hash` / `hashChange`. Casting to this widened type inside the\n * helper keeps shared/dom-utils self-contained — adapters do not need to\n * augment NavigationOptions themselves to consume `<Link hash>`.\n */\ntype HashAwareNavigationOptions = NavigationOptions & {\n hash?: string;\n hashChange?: boolean;\n};\n\nexport function navigateWithHash(\n router: Router,\n routeName: string,\n routeParams: Params,\n hash: string | undefined,\n extraOptions?: NavigationOptions,\n): Promise<State> {\n const opts: HashAwareNavigationOptions = { ...extraOptions };\n\n if (hash !== undefined) {\n opts.hash = hash;\n }\n\n const current = router.getState();\n\n if (\n current?.name === routeName &&\n shallowEqual(current.params, routeParams)\n ) {\n const currentHash =\n (current.context as { url?: { hash?: string } } | undefined)?.url?.hash ??\n \"\";\n const newHash = hash ?? currentHash;\n\n if (currentHash !== newHash) {\n opts.force = true;\n opts.hashChange = true;\n }\n }\n\n return router.navigate(routeName, routeParams, opts);\n}\n\n// Match-any-whitespace regex shared across calls. RegExp literals at\n// call-site recompile in some engines; lifting it avoids that microcost\n// for the slow-path branch.\nconst WHITESPACE_PROBE = /\\s/;\nconst WHITESPACE_SPLIT = /\\S+/g;\n\nfunction parseTokens(value: string | undefined): string[] {\n if (!value) {\n return [];\n }\n\n // Hot-path fast-path (audit-2026-05-17 §8b #1): >99% of active-class\n // inputs at `<Link>` emit are single-token strings like `\"active\"` or\n // `\"is-current\"` — no whitespace, no leading/trailing pad. Skip the\n // regex match and Array result allocation: a literal `[value]` works\n // because the slow-path `match(/\\S+/g)` would return exactly `[value]`\n // for the same input. PBT lock: linkUtils.properties.ts Invariant 13.\n if (!WHITESPACE_PROBE.test(value)) {\n return [value];\n }\n\n return value.match(WHITESPACE_SPLIT) ?? [];\n}\n\nexport function buildActiveClassName(\n isActive: boolean,\n activeClassName: string | undefined,\n baseClassName: string | undefined,\n): string | undefined {\n if (isActive && activeClassName) {\n const activeTokens = parseTokens(activeClassName);\n\n if (activeTokens.length === 0) {\n return baseClassName ?? undefined;\n }\n if (!baseClassName) {\n return activeTokens.join(\" \");\n }\n\n const baseTokens = parseTokens(baseClassName);\n const seen = new Set(baseTokens);\n\n for (const token of activeTokens) {\n if (!seen.has(token)) {\n seen.add(token);\n baseTokens.push(token);\n }\n }\n\n return baseTokens.join(\" \");\n }\n\n return baseClassName ?? undefined;\n}\n\n/**\n * One-level structural equality using `Object.is` per key.\n *\n * **String-keyed properties only (Mini-sprint E.3 — audit-5 §4.2 #3).**\n * Implementation walks `Object.keys()` which by spec returns only\n * enumerable own STRING keys. Symbol-keyed properties — created via\n * `obj[Symbol(\"brand\")] = value` or `{ [Symbol(...)]: value }` — are\n * NOT compared. Two records that differ only in a Symbol-keyed value\n * will compare as equal.\n *\n * This is intentional: route params and Link options are documented as\n * string-keyed primitives (string | number | boolean) — Symbol-keyed\n * metadata (e.g. brand markers, private state) doesn't belong in a\n * cache-key comparison. Switching to `Reflect.ownKeys()` would extend\n * the contract to symbols at the cost of one extra allocation per call\n * (Reflect.ownKeys composes string-keys + symbol-keys arrays). If a\n * consumer relies on symbol-keyed metadata for navigation\n * disambiguation, they should encode it into a string key instead.\n *\n * Mirrors React's `shallowEqual` (packages/shared/shallowEqual.js) in\n * both the string-keys-only semantics and the `hasOwnProperty` guard\n * below.\n */\nexport function shallowEqual(\n prev: object | undefined,\n next: object | undefined,\n): boolean {\n if (Object.is(prev, next)) {\n return true;\n }\n if (!prev || !next) {\n return false;\n }\n\n const prevKeys = Object.keys(prev);\n\n if (prevKeys.length !== Object.keys(next).length) {\n return false;\n }\n\n const prevRecord = prev as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n\n for (const key of prevKeys) {\n // hasOwnProperty guard: without it, a key missing in `next` reads as\n // `undefined` and falsely matches `prev[key] === undefined`. Same shape\n // as React's shallowEqual (packages/shared/shallowEqual.js).\n if (\n !Object.prototype.hasOwnProperty.call(next, key) ||\n !Object.is(prevRecord[key], nextRecord[key])\n ) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function applyLinkA11y(element: HTMLElement | null | undefined): void {\n if (!element) {\n return;\n }\n\n // Cross-realm safety (audit-2026-05-17 §5 HIGH #4):\n // `instanceof HTMLAnchorElement` compares against the constructor from\n // the CURRENT realm. An element created in a different window (iframe\n // contentDocument, micro-frontend, embedded widget) fails the check\n // even when it IS a real anchor — the helper would then inject\n // role=\"link\" + tabindex=\"0\" on top of native anchor semantics,\n // breaking screen reader output (\"link link\") and focus order.\n //\n // tagName is realm-agnostic and is uppercase for HTML-namespaced\n // elements in any document. SVG `<a>` has lowercase tagName plus a\n // different prototype (SVGAElement) — skipping it here is wrong by\n // accident: SVG anchors don't have keyboard activation semantics the\n // helper would add. But they also don't reach this helper in\n // practice (router Link components emit HTML anchors). Lock the\n // uppercase compare to keep the contract narrow.\n const tag = element.tagName;\n\n if (tag === \"A\" || tag === \"BUTTON\") {\n return;\n }\n if (!element.hasAttribute(\"role\")) {\n element.setAttribute(\"role\", \"link\");\n }\n if (!element.hasAttribute(\"tabindex\")) {\n element.setAttribute(\"tabindex\", \"0\");\n }\n}\n","import { createActiveRouteSource } from \"@real-router/sources\";\nimport { useMemo } from \"preact/hooks\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { Params } from \"@real-router/core\";\nimport type { ActiveRouteSourceOptions } from \"@real-router/sources\";\n\nexport function useIsActiveRoute(\n routeName: string,\n params?: Params,\n strict = false,\n ignoreQueryParams = true,\n hash?: string,\n): boolean {\n const router = useRouter();\n\n // createActiveRouteSource is per-router + canonical-args cached in\n // @real-router/sources. Caching the opts object + memoising the source\n // lookup avoids (a) re-allocating the literal on every render and (b)\n // re-running canonicalJson(params) on the cache lookup path. The `hash`\n // argument (#532) participates in the cache key — a tab Link pointing to\n // `/settings#account` shares its source only with consumers using the\n // same routeName + params + hash. exactOptionalPropertyTypes forbids\n // `{ hash: undefined }` literally, so we conditionally include the key\n // only when the caller passed a value.\n const opts = useMemo<ActiveRouteSourceOptions>(\n () =>\n hash === undefined\n ? { strict, ignoreQueryParams }\n : { strict, ignoreQueryParams, hash },\n [strict, ignoreQueryParams, hash],\n );\n\n const store = useMemo(\n () => createActiveRouteSource(router, routeName, params, opts),\n [router, routeName, params, opts],\n );\n\n return useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n}\n","import { memo } from \"preact/compat\";\n\nimport { EMPTY_PARAMS, EMPTY_OPTIONS } from \"../constants\";\nimport {\n shouldNavigate,\n buildHref,\n buildActiveClassName,\n navigateWithHash,\n shallowEqual,\n} from \"../dom-utils\";\nimport { useIsActiveRoute } from \"../hooks/useIsActiveRoute\";\nimport { useRouter } from \"../hooks/useRouter\";\n\nimport type { LinkProps } from \"../types\";\nimport type { FunctionComponent, JSX } from \"preact\";\n\n/**\n * Custom comparator for `Link`'s `memo()` wrapper.\n *\n * **Maintenance contract:** every field in `LinkProps` MUST appear in either\n * the `===` chain (primitives + identity-checked references like `onClick` /\n * `children` / `style`) or in the `shallowEqual` arms (object-valued\n * `routeParams` / `routeOptions`). When adding a new prop to `LinkProps`,\n * extend this function in the same PR — `tests/functional/Link.test.tsx`\n * contains a regression-guard that fails the build if `LinkProps` gains a\n * field that is not compared here.\n *\n * **Intentional omissions:** `props` (the rest-spread of HTMLAnchorElement\n * attributes — `aria-label`, `data-*`, `target`-other-than-`_blank`-handling,\n * etc.) is NOT compared. A change to `aria-label` will NOT trigger a Link\n * re-render. This is by design: dynamic `aria-label` is rare; consumers who\n * truly need a reactive aria-label should call `<Link key={ariaLabel}>` to\n * force a remount.\n */\nfunction areLinkPropsEqual(\n prev: Readonly<LinkProps>,\n next: Readonly<LinkProps>,\n): boolean {\n return (\n prev.routeName === next.routeName &&\n prev.className === next.className &&\n prev.activeClassName === next.activeClassName &&\n prev.activeStrict === next.activeStrict &&\n prev.ignoreQueryParams === next.ignoreQueryParams &&\n prev.onClick === next.onClick &&\n prev.target === next.target &&\n prev.style === next.style &&\n prev.children === next.children &&\n prev.hash === next.hash &&\n shallowEqual(prev.routeParams, next.routeParams) &&\n shallowEqual(prev.routeOptions, next.routeOptions)\n );\n}\n\nexport const Link: FunctionComponent<LinkProps> = memo(\n ({\n routeName,\n routeParams = EMPTY_PARAMS,\n routeOptions = EMPTY_OPTIONS,\n className,\n activeClassName = \"active\",\n activeStrict = false,\n ignoreQueryParams = true,\n hash,\n onClick,\n target,\n children,\n ...props\n }) => {\n const router = useRouter();\n\n // memo + areLinkPropsEqual guarantees that on bail-out the component does\n // not render; on render, routeParams/routeOptions changed reference (true\n // change caught by shallowEqual), so they're safe to use directly in hook\n // deps without useStableValue.\n\n // Hash-aware active (#532) — see useIsActiveRoute for the contract.\n const isActive = useIsActiveRoute(\n routeName,\n routeParams,\n activeStrict,\n ignoreQueryParams,\n hash,\n );\n\n // `buildHref` is a cheap synchronous call (route-tree lookup + string\n // concat). Wrapping it in `useMemo` allocates a deps array on every\n // render that does not bail out — and on bail-out the function body\n // doesn't execute, so the cache never pays off. Same logic for\n // `buildActiveClassName` and `handleClick` below.\n const href = buildHref(\n router,\n routeName,\n routeParams,\n hash === undefined ? undefined : { hash },\n );\n\n const handleClick = (\n evt: JSX.TargetedMouseEvent<HTMLAnchorElement>,\n ): void => {\n if (onClick) {\n onClick(evt);\n\n if (evt.defaultPrevented) {\n return;\n }\n }\n\n if (!shouldNavigate(evt) || target === \"_blank\") {\n return;\n }\n\n evt.preventDefault();\n navigateWithHash(\n router,\n routeName,\n routeParams,\n hash,\n routeOptions,\n ).catch(() => {});\n };\n\n const finalClassName = buildActiveClassName(\n isActive,\n activeClassName,\n className,\n );\n\n return (\n <a\n {...props}\n href={href}\n className={finalClassName}\n onClick={handleClick}\n >\n {children}\n </a>\n );\n },\n areLinkPropsEqual,\n);\n\nLink.displayName = \"Link\";\n","import { createDismissableError } from \"@real-router/sources\";\nimport { Fragment } from \"preact\";\nimport { useEffect, useLayoutEffect, useRef } from \"preact/hooks\";\n\nimport { useRouter } from \"../hooks/useRouter\";\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\n\nimport type { RouterError, State } from \"@real-router/core\";\nimport type { ComponentChildren, VNode } from \"preact\";\n\nexport interface RouterErrorBoundaryProps {\n readonly children: ComponentChildren;\n readonly fallback: (\n error: RouterError,\n resetError: () => void,\n ) => ComponentChildren;\n readonly onError?: (\n error: RouterError,\n toRoute: State | null,\n fromRoute: State | null,\n ) => void;\n}\n\n/**\n * Declarative navigation-error boundary.\n *\n * **Not** a Preact `componentDidCatch`-style ErrorBoundary — this component\n * does NOT catch render-time exceptions from `children`. It is a compositional\n * component that subscribes to `createDismissableError` from\n * `@real-router/sources` and renders `fallback(error, resetError)` ALONGSIDE\n * `children` (wrapped in a `<Fragment>`) when the router emits a navigation\n * error (guard rejection, ROUTE_NOT_FOUND, etc.). The boundary auto-resets on\n * the next successful navigation; `resetError()` lets the consumer dismiss\n * the fallback imperatively.\n *\n * For real exception boundaries, wrap children in a Preact ErrorBoundary\n * (e.g. `preact-iso/ErrorBoundary` or a custom `componentDidCatch` class) —\n * the two can coexist.\n */\nexport function RouterErrorBoundary({\n children,\n fallback,\n onError,\n}: RouterErrorBoundaryProps): VNode {\n const router = useRouter();\n\n // `createDismissableError` is the cached factory from `@real-router/sources`\n // — keyed per-router, identity stable across renders. `useMemo` would wrap\n // a call that already memoizes downstream.\n const store = createDismissableError(router);\n const snapshot = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n\n const onErrorRef = useRef(onError);\n\n useLayoutEffect(() => {\n onErrorRef.current = onError;\n });\n\n // snapshot.version is the @real-router/sources dismissable-error invariant:\n // it is the only field that monotonically advances on each new error episode\n // (snapshot.error/toRoute/fromRoute are correlated reads within the same\n // version frame), so depending on it covers all error fields by construction.\n useEffect(() => {\n if (snapshot.error) {\n onErrorRef.current?.(\n snapshot.error,\n snapshot.toRoute,\n snapshot.fromRoute,\n );\n }\n // eslint-disable-next-line @eslint-react/exhaustive-deps -- onError tracked via ref, snapshot fields accessed inside callback\n }, [snapshot.version]);\n\n return (\n <Fragment>\n {children}\n {snapshot.error ? fallback(snapshot.error, snapshot.resetError) : null}\n </Fragment>\n );\n}\n","import { getPluginApi } from \"@real-router/core/api\";\nimport { getRouteUtils } from \"@real-router/route-utils\";\n\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouteUtils } from \"@real-router/route-utils\";\n\nexport const useRouteUtils = (): RouteUtils => {\n const router = useRouter();\n\n return getRouteUtils(getPluginApi(router).getTree());\n};\n","import { getTransitionSource } from \"@real-router/sources\";\n\nimport { useSyncExternalStore } from \"../useSyncExternalStore\";\nimport { useRouter } from \"./useRouter\";\n\nimport type { RouterTransitionSnapshot } from \"@real-router/sources\";\n\nexport function useRouterTransition(): RouterTransitionSnapshot {\n const router = useRouter();\n const store = getTransitionSource(router);\n\n return useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot,\n );\n}\n","import { useEffect, useLayoutEffect, useRef } from \"preact/hooks\";\n\nimport { useRouter } from \"./useRouter\";\n\nimport type { State } from \"@real-router/core\";\n\nexport interface RouteExitContext {\n /** The route being left. */\n route: State;\n /** The route being navigated to. */\n nextRoute: State;\n /**\n * AbortSignal that fires when this navigation is superseded by a later\n * one (rapid clicks). Already filtered: when the handler runs,\n * `signal.aborted` is guaranteed to be `false`. Use\n * `signal.addEventListener(\"abort\", cleanup, { once: true })` for\n * cleanup that must run on cancellation.\n */\n signal: AbortSignal;\n}\n\nexport interface UseRouteExitOptions {\n /**\n * Skip the handler when `route.name === nextRoute.name`\n * (sort/filter/query-only navigations on the same route). Default:\n * `true`.\n */\n skipSameRoute?: boolean;\n}\n\nexport type RouteExitHandler = (\n context: RouteExitContext,\n) => void | Promise<void>;\n\n/**\n * Subscribe to the router's leave-window with the universal guards baked\n * in. Wraps `router.subscribeLeave` so consumers don't repeat the same\n * boilerplate every time:\n *\n * - **Reentrant abort pre-check**: if `signal.aborted` is already `true`\n * when the handler would run (rapid navigation superseded a slower\n * one), the handler is skipped entirely. `signal.addEventListener(\n * \"abort\", ...)` does not fire retroactively, so without this guard\n * downstream cleanup would never trigger.\n * - **Same-route skip**: by default, `route.name === nextRoute.name`\n * short-circuits the handler — query-only navigations (sort, filter,\n * pagination) skip the work. Opt out with `skipSameRoute: false`.\n * - **Stable handler reference**: the handler can change identity on\n * every render without causing resubscription — internal ref keeps\n * the latest handler accessible to the long-lived subscription.\n *\n * Returns nothing — the subscription's lifecycle is bound to the\n * component's mount.\n *\n * If the handler returns a Promise, the router blocks on it. If the\n * Promise resolves, navigation proceeds. If it rejects, the router emits\n * `TRANSITION_CANCELLED` (existing core behavior, no change here).\n *\n * @example Animation\n * ```tsx\n * const ref = useRef<HTMLDivElement>(null);\n *\n * useRouteExit(async ({ signal }) => {\n * const el = ref.current;\n * if (!el) return;\n * el.classList.add(\"fade-out\");\n * const cleanup = () => el.classList.remove(\"fade-out\");\n * signal.addEventListener(\"abort\", cleanup, { once: true });\n * try {\n * el.getBoundingClientRect(); // style flush\n * await Promise.allSettled(el.getAnimations().map((a) => a.finished));\n * } finally {\n * cleanup();\n * }\n * });\n * ```\n *\n * @example Auto-save form draft\n * ```tsx\n * useRouteExit(async ({ signal }) => {\n * if (formState.dirty) await api.saveDraft(formState, { signal });\n * });\n * ```\n *\n * @example Cancel inflight requests\n * ```tsx\n * useRouteExit(() => {\n * inflightController.abort();\n * });\n * ```\n *\n * @example Library-coordinated exit (motion / framer-motion)\n * ```tsx\n * const exitResolverRef = useRef<(() => void) | null>(null);\n *\n * useRouteExit(({ signal }) => {\n * return new Promise<void>((resolve) => {\n * exitResolverRef.current = resolve;\n * signal.addEventListener(\"abort\", () => resolve(), { once: true });\n * });\n * });\n *\n * const onExitComplete = () => exitResolverRef.current?.();\n * // pass onExitComplete to <AnimatePresence>\n * ```\n *\n * @example Reading rich transition metadata via `nextRoute.transition`\n * ```tsx\n * useRouteExit(({ route, nextRoute }) => {\n * // nextRoute.transition: TransitionMeta — preview of the upcoming nav\n * if (nextRoute.transition.segments.deactivated.includes(\"products\")) {\n * // leaving the products subtree entirely — flush product-related caches\n * productCache.clear();\n * }\n * if (nextRoute.transition.redirected) {\n * // skip animation when navigation arrived via redirect\n * return;\n * }\n * });\n * ```\n */\nexport function useRouteExit(\n handler: RouteExitHandler,\n options?: UseRouteExitOptions,\n): void {\n const router = useRouter();\n const handlerRef = useRef(handler);\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n // Keep the latest handler accessible to the subscription without\n // resubscribing on every render — the subscription registers the\n // wrapper once and dispatches to whatever ref points to.\n // useLayoutEffect (synchronous, post-render, pre-paint) updates the\n // ref before the browser can dispatch any router events that could\n // observe a stale closure.\n useLayoutEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n return router.subscribeLeave(({ route, nextRoute, signal }) => {\n if (skipSameRoute && route.name === nextRoute.name) {\n return;\n }\n\n // Reentrant abort: signal is already aborted when listener fires\n // (e.g. a newer navigate superseded this one before subscribeLeave\n // even ran). addEventListener(\"abort\", ...) does not fire\n // retroactively, so we skip the handler entirely — any cleanup the\n // handler would have wired via abort listener must not run because\n // the corresponding `add` work never happened.\n if (signal.aborted) {\n return;\n }\n\n return handlerRef.current({ route, nextRoute, signal });\n });\n }, [router, skipSameRoute]);\n}\n","import { useEffect, useLayoutEffect, useRef } from \"preact/hooks\";\n\nimport { useRoute } from \"./useRoute\";\n\nimport type { State } from \"@real-router/core\";\n\nexport interface RouteEnterContext {\n /** The route that was just activated. */\n route: State;\n /** The route that was active immediately before this navigation. */\n previousRoute: State;\n}\n\nexport type RouteEnterHandler = (context: RouteEnterContext) => void;\n\nexport interface UseRouteEnterOptions {\n /**\n * Skip the handler when `route.name === previousRoute.name`\n * (sort/filter/query-only navigations on the same route). Default:\n * `true`. Symmetric with `useRouteExit`'s same-name option.\n */\n skipSameRoute?: boolean;\n}\n\n/**\n * Fire `handler` once when the component mounts as a result of a\n * navigation. Mirror of `useRouteExit` for the entry side.\n *\n * What this hook covers that ad-hoc `useEffect` + `useRoute()` doesn't:\n *\n * - **Skip-initial**: handler is skipped when there is no\n * `previousRoute` (i.e. first-load mount). Most consumers want to\n * fire side effects only on real navigations, not on hydration.\n * - **Same-route skip** (default): handler is skipped when\n * `route.name === previousRoute.name`. Sort/filter/query-only\n * navigations re-run the effect (because `route` reference changes\n * in `useRoute`'s snapshot), but they are not \"entries\" in the\n * animation / analytics sense — the component instance has stayed\n * mounted throughout. Opt out with `skipSameRoute: false` when\n * the handler legitimately needs to fire on every navigation\n * (e.g. analytics tracking each query-param flip).\n * - **Latest-handler ref**: the handler can change identity on every\n * render without re-running the effect — the registered wrapper\n * dispatches to whatever `handlerRef.current` points to.\n * - **Mount-time `route` / `previousRoute` snapshot**: the handler\n * receives the values that were live at the moment of mount, not\n * the latest ones (which may have moved on if the user navigated\n * again before the effect drained).\n *\n * Race-safety: `useRoute()` is wired through `useSyncExternalStore` from\n * `@real-router/sources` (Preact polyfill: useState + useEffect, same\n * post-commit semantics), so by the time the new component's effect\n * runs, the snapshot is the post-commit one. This is the reason we can\n * read mount-time context from `useRoute()` instead of subscribing to\n * `router.subscribe` directly (which fires before Preact schedules a\n * re-render — the well-known race in distributed components).\n *\n * Note: Preact does not expose a `StrictMode` equivalent, so the\n * `lastHandledRouteRef` guard exists primarily for defensive symmetry\n * with the React implementation. It is harmless in Preact.\n *\n * @example Direction-aware entry animation\n * ```tsx\n * useRouteEnter(({ route }) => {\n * const direction = route.context.browser?.direction;\n * ref.current?.classList.add(\n * direction === \"back\" ? \"slide-from-left\" : \"slide-from-right\",\n * );\n * });\n * ```\n *\n * @example Source-aware focus management\n * ```tsx\n * useRouteEnter(({ route }) => {\n * if (route.context.browser?.source === \"navigate\") {\n * headingRef.current?.focus();\n * }\n * });\n * ```\n *\n * @example Analytics page-enter event (skip-initial built-in)\n * ```tsx\n * useRouteEnter(({ route, previousRoute }) => {\n * analytics.track(\"page_enter\", {\n * route: route.name,\n * from: previousRoute.name,\n * });\n * });\n * ```\n *\n * @example Reading rich transition metadata via `route.transition`\n * ```tsx\n * useRouteEnter(({ route }) => {\n * // route.transition: TransitionMeta — populated by core for every state\n * if (route.transition.redirected) {\n * showToast(`Redirected from ${route.transition.from}`);\n * }\n * if (route.transition.segments.activated.includes(\"products\")) {\n * // products subtree just became active (could be products or\n * // products.detail). Useful for subtree-scoped side effects.\n * }\n * });\n * ```\n */\nexport function useRouteEnter(\n handler: RouteEnterHandler,\n options?: UseRouteEnterOptions,\n): void {\n const { route, previousRoute } = useRoute();\n const handlerRef = useRef(handler);\n const lastHandledRouteRef = useRef<State | null>(null);\n const skipSameRoute = options?.skipSameRoute ?? true;\n\n // Keep the latest handler reference accessible without re-running\n // the effect. useLayoutEffect (synchronous, post-render, pre-paint)\n // updates the ref before the effect can read it.\n useLayoutEffect(() => {\n handlerRef.current = handler;\n });\n\n useEffect(() => {\n // Early-exit guards, top-down:\n //\n // - **Skip-initial**: `state.transition.from` is undefined only\n // for the very first state committed by `router.start()`.\n // - **Skip-same-route**: query-only navigations have\n // `transition.from === route.name`. Opt-out via\n // `skipSameRoute: false`.\n // - **Defensive dedupe**: same `route` ref between effect\n // cleanup + re-run. Preact has no StrictMode, but we keep the\n // guard for parity with React; v8-ignored.\n if (!route.transition.from) {\n return;\n }\n if (skipSameRoute && route.transition.from === route.name) {\n return;\n }\n /* v8 ignore start */\n if (lastHandledRouteRef.current === route || !previousRoute) {\n return;\n }\n /* v8 ignore stop */\n\n lastHandledRouteRef.current = route;\n handlerRef.current({ route, previousRoute });\n }, [route, previousRoute, skipSameRoute]);\n}\n","import { getNavigator } from \"@real-router/core\";\nimport { createRouteSource } from \"@real-router/sources\";\nimport { useEffect, useMemo } from \"preact/hooks\";\n\nimport { NavigatorContext, RouteContext, RouterContext } from \"./context\";\nimport {\n createRouteAnnouncer,\n createScrollRestoration,\n createViewTransitions,\n} from \"./dom-utils\";\nimport { useSyncExternalStore } from \"./useSyncExternalStore\";\n\nimport type { ScrollRestorationOptions } from \"./dom-utils\";\nimport type { Router } from \"@real-router/core\";\nimport type { FunctionComponent, ComponentChildren } from \"preact\";\n\nexport interface RouteProviderProps {\n router: Router;\n children: ComponentChildren;\n announceNavigation?: boolean;\n scrollRestoration?: ScrollRestorationOptions;\n viewTransitions?: boolean;\n}\n\nexport const RouterProvider: FunctionComponent<RouteProviderProps> = ({\n router,\n children,\n announceNavigation,\n scrollRestoration,\n viewTransitions,\n}) => {\n useEffect(() => {\n if (!announceNavigation) {\n return;\n }\n\n const announcer = createRouteAnnouncer(router);\n\n return () => {\n announcer.destroy();\n };\n }, [announceNavigation, router]);\n\n // Primitive deps so inline `{ mode: \"restore\" }` doesn't thrash on every\n // render. scrollContainer is a getter invoked lazily on every event inside\n // the utility — swapping its reference doesn't change the resolved element,\n // so we intentionally omit it from deps to keep inline getters stable.\n const srMode = scrollRestoration?.mode;\n const srAnchor = scrollRestoration?.anchorScrolling;\n const srBehavior = scrollRestoration?.behavior;\n const srStorageKey = scrollRestoration?.storageKey;\n const srEnabled = scrollRestoration !== undefined;\n\n useEffect(() => {\n if (!srEnabled) {\n return;\n }\n\n const sr = createScrollRestoration(router, {\n mode: srMode,\n anchorScrolling: srAnchor,\n behavior: srBehavior,\n storageKey: srStorageKey,\n // srEnabled check above guarantees scrollRestoration is defined.\n scrollContainer: scrollRestoration.scrollContainer,\n });\n\n return () => {\n sr.destroy();\n };\n // scrollRestoration (for scrollContainer) omitted — see comment above.\n // eslint-disable-next-line @eslint-react/exhaustive-deps\n }, [router, srEnabled, srMode, srAnchor, srBehavior, srStorageKey]);\n\n useEffect(() => {\n if (!viewTransitions) {\n return;\n }\n\n const vt = createViewTransitions(router);\n\n return () => {\n vt.destroy();\n };\n }, [router, viewTransitions]);\n\n // `getNavigator` is cached per-router in `@real-router/core` (WeakMap) —\n // same router always returns the same Navigator ref. No `useMemo` needed.\n const navigator = getNavigator(router);\n\n // `createRouteSource` is NOT cached (per packages/sources/CLAUDE.md table).\n // It must be stable across renders so `useSyncExternalStore`'s deps don't\n // change identity and trigger an unsubscribe/resubscribe loop on every\n // render. `useMemo([router])` gives one source per router-instance lifetime.\n const store = useMemo(() => createRouteSource(router), [router]);\n\n // useSyncExternalStore manages the router subscription lifecycle:\n // subscribe connects to router on first listener, unsubscribes on last.\n const { route, previousRoute } = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n store.getSnapshot, // SSR: router returns same state on server and client\n );\n\n // Stable-ref against parent re-renders: when parent re-renders RouterProvider\n // without a route change (e.g. consumer re-renders the root), navigator /\n // route / previousRoute references stay identical (useSyncExternalStore +\n // Object.is bail-out). Without `useMemo` the object literal is fresh every\n // render, propagating spurious re-renders to every `useRoute()` consumer.\n // The memo bails out whenever the three deps are referentially equal.\n const routeContextValue = useMemo(\n () => ({ navigator, route, previousRoute }),\n [navigator, route, previousRoute],\n );\n\n return (\n <RouterContext.Provider value={router}>\n <NavigatorContext.Provider value={navigator}>\n <RouteContext.Provider value={routeContextValue}>\n {children}\n </RouteContext.Provider>\n </NavigatorContext.Provider>\n </RouterContext.Provider>\n );\n};\n"],"mappings":"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"}