{"_id":"@dmytromykhailiuk/preact-signal-router","_rev":"2-918c69b2ad17e608237cdbe35ef273b8","name":"@dmytromykhailiuk/preact-signal-router","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@dmytromykhailiuk/preact-signal-router","version":"1.0.0","keywords":["preact","preact-signals","signals","router","routing","spa-router","angular-router","ion-router","guards","resolvers","lazy-routes","preload","typed-routes","reactive","zero-rerender","typescript","typed"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/preact-signal-router@1.0.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/preact-signal-router#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-router/issues"},"dist":{"shasum":"ec065e7ad159a3f49fc5f07a82ac13aed62b31e6","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-router/-/preact-signal-router-1.0.0.tgz","fileCount":9,"integrity":"sha512-00nVUklQq51UQNHmchUJbYuMKj7NfWr8HbFN61ndAsm+/DnLI5tXwkk5O25DfEM6OHy/pWIkwqASI9B51RotAA==","signatures":[{"sig":"MEYCIQDGNq3kL/uNT5r4jeq1Iyic8gzoLd6K0xJw6drSPbTSNwIhALNgClx3W/AFR9Yt/IytmQ6VMP1wjXbsJ7vjrYxvbL3H","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":204316},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"152f44cd9673cbfded9933ae5e140475a352ae19","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/preact-signal-router.git","type":"git"},"_npmVersion":"11.6.2","description":"A fully-typed, Angular-style router for Preact built entirely on @preact/signals — typed navigation, guards, resolvers, lazy/preload routes, base-path deploy and opt-in ion-router transitions. Signal-first, zero re-render.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^5.4.11","jsdom":"^25.0.1","preact":"^10.25.4","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@preact/signals":"^2.0.1","@preact/preset-vite":"^2.10.1","@testing-library/preact":"^3.2.4"},"peerDependencies":{"preact":">=10.25.0","@preact/signals":"^2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/preact-signal-router_1.0.0_1784753261568_0.5936610432480809","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@dmytromykhailiuk/preact-signal-router","version":"1.0.1","description":"A fully-typed, Angular-style router for Preact built entirely on @preact/signals — typed navigation, guards, resolvers, lazy/preload routes, base-path deploy and opt-in ion-router transitions. Signal-first, zero re-render.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["preact","preact-signals","signals","router","routing","spa-router","angular-router","ion-router","guards","resolvers","lazy-routes","preload","typed-routes","reactive","zero-rerender","typescript","typed"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"vite --config vite.playground.config.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"peerDependencies":{"@preact/signals":"^2.0.0","preact":">=10.25.0"},"devDependencies":{"@biomejs/biome":"^1.9.4","@preact/preset-vite":"^2.10.1","@preact/signals":"^2.0.1","@testing-library/preact":"^3.2.4","@types/node":"^22.10.5","jsdom":"^25.0.1","preact":"^10.25.4","tsup":"^8.3.5","typescript":"^5.7.3","vite":"^5.4.11","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-router.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-router/issues"},"homepage":"https://dmytromykhailiuk.github.io/preact-signal-router/","gitHead":"f041f92966343a22a0a528d2cd8d63b3643dbb69","_id":"@dmytromykhailiuk/preact-signal-router@1.0.1","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-8hZQL/CJSC5QErDu6Dayx+rF0IXPHdo6Sjyu3CwxrkBIwzj/awmspSaNGbd+qoKSvIr6o+vPVXW9nGmt18ox0w==","shasum":"d20b916c8a398a7a42a8cf1e8db6ae3ea392408b","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-router/-/preact-signal-router-1.0.1.tgz","fileCount":9,"unpackedSize":204309,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIAmb9SGImG7zQatgpwOmA7ubqIKGyq3DyZBfa6mmcSbdAiAGTuuZkN5W4vx0LhH7sA4EqU6QglFW5YvkNbrbjHgO+w=="}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/preact-signal-router_1.0.1_1786639214439_0.8929477812307345"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-22T20:47:41.365Z","modified":"2026-08-13T16:40:14.777Z","1.0.0":"2026-07-22T20:47:41.723Z","1.0.1":"2026-08-13T16:40:14.589Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-router/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/preact-signal-router/","keywords":["preact","preact-signals","signals","router","routing","spa-router","angular-router","ion-router","guards","resolvers","lazy-routes","preload","typed-routes","reactive","zero-rerender","typescript","typed"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-router.git"},"description":"A fully-typed, Angular-style router for Preact built entirely on @preact/signals — typed navigation, guards, resolvers, lazy/preload routes, base-path deploy and opt-in ion-router transitions. Signal-first, zero re-render.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# @dmytromykhailiuk/preact-signal-router\n\nA fully-typed, Angular-style router for **Preact**, built entirely on\n[@preact/signals](https://github.com/preactjs/signals). Typed navigation, guards,\nresolvers, lazy/preload routes, base-path deploy and opt-in ion-router\ntransitions. **Signal-first, zero re-render** — only the outlet re-renders when\nthe active page changes; everything else you read is a signal you bind directly\nto the DOM.\n\n> **Full documentation:** open [Docs](https://dmytromykhailiuk.github.io/preact-signal-router/)\n> in a browser — every option, with examples, a table of contents and\n> cross-links. This README is the long-form summary.\n\n## Highlights\n\n- **Typed navigation.** `createRouterConfig` records every registered path at the\n  type level. `navigateForward` autocompletes your static paths; parameterised\n  routes go through `router.path(\"/user/:id\", { id })`, which type-checks the\n  params. Unknown paths and missing params are **compile errors**.\n- **Angular-style config as data.** Guards (`canActivate` / `canDeactivate`),\n  resolvers, redirects, nested children and composable layouts.\n- **Ion-router navigation.** `navigateForward` / `navigateBack` / `navigateRoot`\n  over an internal navigation stack. There is no public `navigate`.\n- **Lazy & preload routes.** `addLazyPage` loads on activation; `addPreloadPage`\n  starts importing immediately and memoizes.\n- **Opt-in ion-router transitions.** `animations: true` gives the iOS\n  slide for forward/back and an **instant** navigateRoot — identical to\n  ion-router. Fully configurable: duration, easing and per-direction classes.\n- **Base-path deploy.** `base: \"/web\"` for apps served from `company.com/web/`.\n- **A signal snapshot.** `snapshot$`, `component$`, `pending$`, `direction$` and\n  `navId$` are read-only signals you consume anywhere with zero re-render.\n\n## Install\n\n```sh\nnpm i @dmytromykhailiuk/preact-signal-router @preact/signals preact\n```\n\n> Requires `@preact/signals` **^2.0.0** (the `Show`/`For` utilities live in the\n> `/utils` subpath, a signals v2 feature) and `preact` **>=10.25.0**. Both are\n> peer dependencies.\n\n## Quick start\n\n```tsx\nimport { render } from \"preact\";\nimport {\n  createRouter,\n  createRouterConfig,\n  RouterOutlet,\n} from \"@dmytromykhailiuk/preact-signal-router\";\n\nconst Home = () => <h1>Home</h1>;\nconst About = () => <h1>About</h1>;\nconst User = () => <h1>User</h1>;\n\n// 1. Describe the route tree. RETURN the chained builder so the paths are typed.\nconst routes = createRouterConfig((r) =>\n  r\n    .addPage(\"/home\", Home)\n    .addPage(\"/about\", About)\n    .addChildren(\"/user\", (u) => u.addPage(\"/:id\", User))\n    .addRedirect(\"**\", \"/home\"),\n);\n\n// 2. Create the router — export it and navigate from anywhere.\nexport const router = createRouter(routes, { animations: true });\n\n// 3. Mount the outlet once. It wires history + renders the active page.\nfunction App() {\n  return (\n    <div>\n      <nav>\n        <button onClick={() => router.navigateForward(\"/home\")}>Home</button>\n        <button onClick={() => router.navigateForward(\"/about\")}>About</button>\n        <button onClick={() => router.navigateForward(router.path(\"/user/:id\", { id: \"42\" }))}>\n          User 42\n        </button>\n      </nav>\n      <RouterOutlet router={router} />\n    </div>\n  );\n}\n\nrender(<App />, document.getElementById(\"app\")!);\n```\n\n## The signal rules (zero re-render)\n\nThe router exposes **signals**, not values. The whole point is that a component\nmounts once and never re-renders on navigation. Follow three rules:\n\n1. **Never unwrap `.value` in the render path.** Reading `snapshot$.value` in a\n   component body subscribes the whole component and re-renders it on every\n   change.\n2. **Bind signals directly** to JSX text/attributes, or **derive** with\n   `useComputed` and bind the result.\n3. **Conditionals** use `<Show>`, **lists** use `<For>` (from\n   `@preact/signals/utils`).\n\n```tsx\nimport { useComputed } from \"@preact/signals\";\nimport { Show } from \"@preact/signals/utils\";\nimport { router } from \"./router\";\n\n// ❌ re-renders on every navigation\nfunction BadCrumb() {\n  return <span>{router.snapshot$.value?.path}</span>;\n}\n\n// ✅ derive + bind — never re-renders\nfunction Crumb() {\n  const label = useComputed(() => router.snapshot$.value?.data.title ?? router.snapshot$.value?.path ?? \"\");\n  return <span>{label}</span>;\n}\n\n// ✅ conditional through <Show>\nfunction LoadingBar() {\n  return (\n    <Show when={router.pending$}>\n      <div class=\"bar\" />\n    </Show>\n  );\n}\n```\n\n`<RouterOutlet>` is the **only** component that re-renders on navigation — it has\nto swap the page subtree.\n\n## The router instance\n\n`createRouter(config, options?)` returns a `RouterInstance`. Export it and use it\nanywhere — components, event handlers, services.\n\n### Signals\n\n| Signal        | Type                                             | Meaning                                                            |\n| ------------- | ------------------------------------------------ | ----------------------------------------------------------------- |\n| `snapshot$`   | `RouteSnapshot \\| null`                          | `{ path, params, query, data }` for the active route.             |\n| `component$`  | `PageComponent \\| null`                          | The component to render for the active route.                     |\n| `pending$`    | `boolean`                                        | `true` while a navigation resolves (guards/resolvers/lazy load).  |\n| `direction$`  | `\"forward\" \\| \"back\" \\| \"root\" \\| null`          | Direction of the most recent navigation — drives the animation.   |\n| `navId$`      | `number`                                         | Increments on every committed navigation.                         |\n\n```ts\nconst snap = router.snapshot$.peek(); // { path: \"/user/42\", params: { id: \"42\" }, query: {}, data: {…} }\n```\n\n### Options\n\n```ts\ncreateRouter(routes, {\n  base: \"/web\",        // deploy under a sub-path (default \"\")\n  animations: true,    // ion-style transitions (default off)\n});\n```\n\n## Defining routes\n\n`createRouterConfig((r) => …)` builds the route tree with a fluent builder. Each\nmethod is chainable and returns the builder.\n\n> **Typed paths:** the callback must **return** the chained builder\n> (`(r) => r.addPage(...).addChildren(...)`) so the union of paths is inferred.\n> If you write statements instead of returning, navigation still works but falls\n> back to plain strings.\n\n| Method                                          | Adds                                                            |\n| ----------------------------------------------- | -------------------------------------------------------------- |\n| `addPage(path, component, options?)`            | An eagerly-bundled page `() => JSX.Element \\| null`.           |\n| `addLazyPage(path, lazyComponent, options?)`    | A code-split page loaded when the route activates.             |\n| `addPreloadPage(path, preloadComponent, opts?)` | A code-split page whose import starts immediately + memoizes.  |\n| `addRedirect(path, redirectTo, options?)`       | A redirect (string / object / function).                       |\n| `addChildren(path, (child) => …, options?)`     | A nested group, optionally wrapped in a `layout`.              |\n\n### Path patterns\n\n- `/segment` — a literal segment.\n- `:param` — captured into `snapshot.params`.\n- `**` — a catch-all matching the rest of the path (**not** navigable; excluded\n  from typed paths — use it for fallbacks/redirects).\n\nChild paths are prefixed by their parent: `addChildren(\"/user\", (u) => u.addPage(\"/:id\", …))`\nregisters `/user/:id`.\n\n### RouteOptions\n\nEvery `add*` method takes the same options object (last argument):\n\n| Option          | Type                        | Meaning                                                    |\n| --------------- | --------------------------- | ---------------------------------------------------------- |\n| `canActivate`   | `Guard[]`                   | Guards run before entering. Cascade to children.           |\n| `canDeactivate` | `Guard[]`                   | Guards run before leaving the active route.                |\n| `resolve`       | `Record<string, Resolver>`  | Data loaders; results merged into `snapshot.data`.         |\n| `data`          | `Record<string, any>`       | Static data merged into `snapshot.data`.                   |\n| `pathMatch`     | `\"full\" \\| \"prefix\"`        | Match strategy (default `\"prefix\"`).                       |\n\nOn `addChildren`, options also accept `layout`. Guards, resolvers and data on a\nparent group cascade down and **merge** with the child's own.\n\n### Layouts\n\nA group's `layout` wraps every descendant page. Layouts compose — an ancestor\nlayout wraps a nested layout wraps the page.\n\n```tsx\nconst Shell = ({ children }: { children: JSX.Element | null }) => (\n  <div class=\"shell\">\n    <Sidebar />\n    <main>{children}</main>\n  </div>\n);\n\nconst routes = createRouterConfig((r) =>\n  r.addChildren(\n    \"/app\",\n    (app) => app.addPage(\"/home\", Home).addPage(\"/settings\", Settings),\n    { layout: Shell },\n  ),\n);\n```\n\n## Guards\n\nA `Guard` is `(ctx) => boolean | Redirect | Promise<…>`. Return `true` to allow,\n`false` to block, or a redirect to send the user elsewhere. `createGuard` just\ntypes the function.\n\n```tsx\nimport { createGuard } from \"@dmytromykhailiuk/preact-signal-router\";\n\nconst authGuard = createGuard(({ to }) => (isLoggedIn() ? true : \"/login\"));\n\nconst adminGuard = createGuard(async ({ to, signal }) => {\n  const ok = await hasRole(\"admin\", { signal });\n  return ok ? true : { redirectTo: \"/403\", data: { attempted: to.path } };\n});\n\nconst routes = createRouterConfig((r) =>\n  r\n    .addChildren(\"/admin\", (a) => a.addPage(\"/dashboard\", Dashboard), {\n      canActivate: [authGuard, adminGuard],\n    })\n    .addPage(\"/login\", Login),\n);\n```\n\n`canDeactivate` runs on the route you're leaving — useful for \"unsaved changes\"\nprompts:\n\n```tsx\nconst confirmLeave = createGuard(() => hasUnsavedChanges() ? confirm(\"Discard changes?\") : true);\nr.addPage(\"/edit\", Editor, { canDeactivate: [confirmLeave] });\n```\n\n### The context object\n\nEvery guard, resolver and redirect receives:\n\n| Field    | Type                      | Meaning                                                       |\n| -------- | ------------------------- | ------------------------------------------------------------ |\n| `from`   | `RouteSnapshot \\| null`   | The route being left (`null` on first navigation).           |\n| `to`     | `RouteSnapshot`           | The target route (params & query already filled in).         |\n| `signal` | `AbortSignal`             | Aborts when a newer navigation supersedes this one.          |\n\nRapid navigations abort in-flight guards/resolvers automatically; check\n`signal.aborted` (or pass `signal` to `fetch`) in async work.\n\n## Resolvers\n\nA `Resolver` is `(ctx) => value | Promise<value>`. Each entry in a route's\n`resolve` map runs (in parallel) before the page mounts; the result lands in\n`snapshot.data` under its key, so the page reads it synchronously — no\nin-component loading state.\n\n```tsx\nimport { createResolver } from \"@dmytromykhailiuk/preact-signal-router\";\n\nconst userResolver = createResolver(async ({ to, signal }) => {\n  const res = await fetch(`/api/users/${to.params.id}`, { signal });\n  return res.json();\n});\n\nconst routes = createRouterConfig((r) =>\n  r.addChildren(\"/user\", (u) =>\n    u.addPage(\"/:id\", UserPage, { resolve: { user: userResolver } }),\n  ),\n);\n\n// Inside UserPage — derive + bind, no .value in the render path.\nfunction UserPage() {\n  const name = useComputed(() => router.snapshot$.value!.data.user.name);\n  return <h1>{name}</h1>;\n}\n```\n\n## Redirects\n\n`addRedirect` accepts three forms:\n\n```tsx\nimport { createRedirect } from \"@dmytromykhailiuk/preact-signal-router\";\n\nr\n  // 1. a plain string\n  .addRedirect(\"/\", \"/home\")\n  // 2. a redirect object (replace history entry, attach data)\n  .addRedirect(\"/legacy\", { redirectTo: \"/home\", replace: true, data: { via: \"legacy\" } })\n  // 3. a function of ctx (async allowed)\n  .addRedirect(\"/enter\", createRedirect(({ to }) => (to.query.next === \"1\" ? \"/home\" : \"/login\")))\n  // a ** catch-all fallback\n  .addRedirect(\"**\", \"/home\");\n```\n\nGuards can redirect too (return a string or redirect object) — the same three\nshapes apply.\n\n## Lazy & preload\n\nBoth split code with a dynamic `import()`; they differ in **when** the import\nstarts.\n\n| Method             | Import starts…                          | Use for                                             |\n| ------------------ | --------------------------------------- | --------------------------------------------------- |\n| `addLazyPage`      | when the route activates                | rarely-visited or heavy pages                       |\n| `addPreloadPage`   | immediately at `createRouter(...)` time | likely-next pages you want warm (result memoized)   |\n\n```tsx\nr\n  .addLazyPage(\"/reports\", () => import(\"./pages/Reports\").then((m) => m.Reports))\n  .addPreloadPage(\"/checkout\", () => import(\"./pages/Checkout\").then((m) => m.Checkout));\n```\n\n## Typed navigation\n\nThere is no public `navigate`. Navigation is ion-router style, over an internal\nstack:\n\n| Method                          | Does                                                                            |\n| ------------------------------- | ------------------------------------------------------------------------------- |\n| `navigateForward(to, opts?)`    | Push a new route with a **forward** slide.                                       |\n| `navigateBack(to?, opts?)`      | **Back** slide. With `to` go there; without `to` pop to the previous entry.      |\n| `navigateRoot(to, opts?)`       | Reset the stack and go to `to` **instantly** (no animation, like ion-router).    |\n\n`navigateBack()` at the root of the stack is a no-op.\n\n**Same-URL navigation is ignored.** Navigating to the route that is already\nactive (same path **and** query) does nothing — `navId$` stays put, and there is\nno re-render or re-animation (like Angular's `onSameUrlNavigation: \"ignore\"`). A\ndifferent param on the same route (`/user/1` → `/user/2`) is a real navigation —\nit's a new view, so it animates.\n\n### The `path()` helper\n\nStatic (param-free, non-wildcard) paths may be passed as bare autocompleted\nstrings. Parameterised paths go through `router.path`, which requires — and\ntype-checks — the params:\n\n```tsx\nrouter.navigateForward(\"/home\");                                  // static, autocompleted\nrouter.navigateForward(router.path(\"/user/:id\", { id: \"42\" }));   // params required\nrouter.navigateForward(router.path(\"/user/:id\", { id }, { tab: \"posts\" })); // + query\nrouter.navigateForward(router.path(\"/home\", { ref: \"email\" }));   // param-free: 2nd arg is query\n```\n\nCompile-time guarantees:\n\n```tsx\nrouter.navigateForward(\"/nope\");            // ❌ unknown path\nrouter.navigateForward(\"/user/42\");         // ❌ param path can't be a bare string\nrouter.path(\"/user/:id\", {});               // ❌ missing param `id`\n```\n\n### NavigateOptions\n\n| Option                  | Type                                         | Meaning                                                    |\n| ----------------------- | -------------------------------------------- | ---------------------------------------------------------- |\n| `data`                  | `Record<string, any>`                        | Merged into `snapshot.data`.                               |\n| `replace`               | `boolean`                                    | Use `history.replaceState` instead of `pushState`.         |\n| `animation`             | `\"forward\" \\| \"back\" \\| \"root\" \\| false`     | Force a direction, or `false` to skip the animation.       |\n| `disableBackNavigation` | `boolean`                                    | Trap the browser back button on the resulting entry.       |\n\n## Base-path deployment\n\nServing from a sub-path (e.g. `company.com/web/`)? Pass `base`. Incoming URLs are\nstripped of it, outgoing browser URLs are written with it, and `snapshot.path`\nstays app-relative.\n\n```tsx\nconst router = createRouter(routes, { base: \"/web\" });\nawait router.navigateForward(\"/home\"); // browser URL → /web/home, snapshot.path → /home\n```\n\n## Animations\n\nOpt in with `animations`. The defaults mirror **ion-router**:\n\n- `navigateForward` / `navigateBack` — the iOS horizontal slide (entering page\n  slides in, leaving page parallaxes out and dims).\n- `navigateRoot` — **instant, no animation** (like `NavController.navigateRoot`,\n  for login/logout/tab-reset flows where a slide would mislead).\n- Defaults: **540 ms**, `cubic-bezier(0.32, 0.72, 0, 1)`.\n\nThe outgoing page stays mounted for the transition; `prefers-reduced-motion` is\nrespected; and the outlet itself never re-renders.\n\n```tsx\n// ion-router defaults\ncreateRouter(routes, { animations: true });\n\n// fully configurable — duration, easing, per-direction classes\ncreateRouter(routes, {\n  animations: {\n    duration: 300,\n    easing: \"cubic-bezier(0.32, 0.72, 0, 1)\",\n    forward: { enter: \"my-enter-fwd\", leave: \"my-leave-fwd\" },\n    back: { enter: \"my-enter-back\", leave: \"my-leave-back\" },\n    // Opt navigateRoot INTO an animation. Omit it → instant (default).\n    // `{}` uses the built-in fade; or pass your own classes + CSS.\n    root: {},\n  },\n});\n```\n\nThe default stylesheet is injected once. To fully customise a direction, supply\nyour own enter/leave class names and matching CSS/keyframes. `direction$` and\n`navId$` are available if you want to build a bespoke transition around the\noutlet.\n\n## RouterOutlet\n\n```tsx\n<RouterOutlet router={router} />\n```\n\nMount it once where routed content appears. On mount it calls\n`router.register()` (wiring `popstate` and performing the initial navigation from\nthe current URL). With `animations`, it keeps the outgoing page mounted during\nthe transition and applies the ion-style classes. The outlet itself never\nre-renders — only the swapped subtree does.\n\nManual control (advanced): `router.register()` returns a cleanup function you can\ncall yourself if you are not using `RouterOutlet`.\n\n## Production example\n\n```tsx\n// guards.ts\nexport const authGuard = createGuard(({ to }) =>\n  store.isAuthed ? true : { redirectTo: \"/page/guest\", data: { next: to.path } },\n);\nexport const notAuthGuard = createGuard(() => (store.isAuthed ? \"/page/event\" : true));\nexport const eventResolver = createResolver(({ to, signal }) =>\n  api.getEvent(to.params.eventId, { signal }),\n);\n\n// routes.ts\nexport const routes = createRouterConfig((r) =>\n  r.addChildren(\"/page\", (page) =>\n    page\n      .addRedirect(\"/login-success\", () => (store.isAuthed ? \"/page/event\" : \"/page/guest\"))\n      .addPage(\"/guest\", GuestPage, { canActivate: [notAuthGuard] })\n      .addChildren(\n        \"/event\",\n        (event) =>\n          event\n            .addRedirect(\"/create\", () => `/page/event/${api.newDraftId()}/otl-start`)\n            .addChildren(\"/:eventId\", (ev) =>\n              ev\n                .addPage(\"/otl-start\", OtlStartPage)\n                .addPage(\"/guide\", GuidePage)\n                .addPreloadPage(\"/steps/:stepId\", () =>\n                  import(\"./pages/Steps\").then((m) => m.StepsPage),\n                )\n                .addLazyPage(\"/upload\", () => import(\"./pages/Upload\").then((m) => m.UploadPage))\n                .addPage(\"/completed\", CompletedPage),\n              { resolve: { event: eventResolver }, layout: EventLayout },\n            ),\n        { canActivate: [authGuard] }, // the whole /event tree is auth-gated\n      )\n      .addPage(\"/not-found\", NotFoundPage),\n  ).addRedirect(\"**\", \"/page/not-found\"),\n);\n\n// router.ts\nexport const router = createRouter(routes, { base: \"/web\", animations: true });\n\n// navigate from anywhere — typed all the way\nrouter.navigateForward(router.path(\"/page/event/:eventId/guide\", { eventId: \"42\" }));\nrouter.navigateBack();               // pop a step\nrouter.navigateRoot(\"/page/guest\");  // e.g. after logout\n```\n\n## Exports\n\n| Export                                                             | Kind  |\n| ----------------------------------------------------------------- | ----- |\n| `createRouterConfig`                                              | value |\n| `createRouter`                                                   | value |\n| `createGuard` / `createResolver` / `createRedirect`              | value |\n| `RouterOutlet`                                                   | value |\n| `resolveAnimations` / `ensureAnimationStyles`                    | value |\n| `joinPaths` / `addSlash` / `mergePath` / `parseUrl` / `matchPath` | value |\n| `makeRoutesFlatten` / `mergeRoutes`                              | value |\n| `RouterInstance`, `RouterConfig`, `RouteConfig`, `RouteSnapshot`, `Guard`, `Resolver`, `Redirect`, `NavigateOptions`, `RouterOptions`, `AnimationConfig`, `NavDirection`, `RoutePath`, `PathParams`, … | type |\n\n## TypeScript\n\n- Set `jsxImportSource: \"preact\"` (and `jsx: \"react-jsx\"`) in `tsconfig.json`.\n- `createRouterConfig` infers a `RouterConfig<Paths>` — the union of every\n  registered path. `createRouter` carries `Paths` into the navigation methods and\n  `path()` for full autocomplete and param checking.\n- `PathParams<\"/user/:id/tab/:tab\">` resolves to `{ id: string; tab: string }` if\n  you need the params type directly.\n\n## License\n\nMIT © Dmytro Mykhailiuk\n","readmeFilename":"README.md"}