@timber-js/app 0.2.0-alpha.209 → 0.2.0-alpha.210
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.
- package/dist/_chunks/{actions-BerlqoXA.js → actions-Rjk4htmA.js} +19 -25
- package/dist/_chunks/{actions-BerlqoXA.js.map → actions-Rjk4htmA.js.map} +1 -1
- package/dist/_chunks/als-registry-DaxkVjt5.js.map +1 -1
- package/dist/_chunks/{cache-api-CR23J_NC.js → cache-api-DGdYfNJn.js} +4 -4
- package/dist/_chunks/{cache-api-CR23J_NC.js.map → cache-api-DGdYfNJn.js.map} +1 -1
- package/dist/_chunks/{chains-CpFg56UB.js → chains-BoO51joc.js} +2 -2
- package/dist/_chunks/{chains-CpFg56UB.js.map → chains-BoO51joc.js.map} +1 -1
- package/dist/_chunks/{cli-check-C6Ev6wBO.js → cli-check-ajNY3B2e.js} +3 -3
- package/dist/_chunks/{cli-check-C6Ev6wBO.js.map → cli-check-ajNY3B2e.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-CbT2AUUI.js → cli-schema-sync-D2eI8jEg.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-CbT2AUUI.js.map → cli-schema-sync-D2eI8jEg.js.map} +1 -1
- package/dist/_chunks/{file-cache-Dw6BJPG7.js → codegen-Bps1sLKJ.js} +3 -29
- package/dist/_chunks/codegen-Bps1sLKJ.js.map +1 -0
- package/dist/_chunks/{convention-lint-jKTwKwPe.js → convention-lint-DLmhGsRS.js} +54 -316
- package/dist/_chunks/convention-lint-DLmhGsRS.js.map +1 -0
- package/dist/_chunks/{dev-server-C4WZdB7L.js → dev-server-v97rQH4b.js} +99 -9
- package/dist/_chunks/dev-server-v97rQH4b.js.map +1 -0
- package/dist/_chunks/{error-boundary-tA7kVfs4.js → error-boundary-DsNScGRM.js} +4 -4
- package/dist/_chunks/{error-boundary-tA7kVfs4.js.map → error-boundary-DsNScGRM.js.map} +1 -1
- package/dist/_chunks/{json-lossy-check-ip0Qi0MT.js → json-lossy-check-CVuRs2hG.js} +2 -2
- package/dist/_chunks/{json-lossy-check-ip0Qi0MT.js.map → json-lossy-check-CVuRs2hG.js.map} +1 -1
- package/dist/_chunks/{live-graph-Dv-JJCZw.js → live-graph-9cSnn_h9.js} +3 -3
- package/dist/_chunks/{live-graph-Dv-JJCZw.js.map → live-graph-9cSnn_h9.js.map} +1 -1
- package/dist/_chunks/{logger-DiDt5ppH.js → logger-BP0LN6vP.js} +17 -2
- package/dist/_chunks/{logger-DiDt5ppH.js.map → logger-BP0LN6vP.js.map} +1 -1
- package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -1
- package/dist/_chunks/navigation-root-BQfo1-kG.js.map +1 -1
- package/dist/_chunks/{poison-scan-CpeT6_OJ.js → poison-scan-vGV7Re0B.js} +2 -2
- package/dist/_chunks/{poison-scan-CpeT6_OJ.js.map → poison-scan-vGV7Re0B.js.map} +1 -1
- package/dist/_chunks/{scanner-Bw0oq1HB.js → scanner-DmqdxzbW.js} +392 -7
- package/dist/_chunks/scanner-DmqdxzbW.js.map +1 -0
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
- package/dist/_chunks/{sizeof-UwzwB1uM.js → sizeof-BM1409x2.js} +2 -2
- package/dist/_chunks/{sizeof-UwzwB1uM.js.map → sizeof-BM1409x2.js.map} +1 -1
- package/dist/_chunks/{status-page-marker-gaihi0KZ.js → status-page-marker-BRX9Ib-d.js} +1 -45
- package/dist/_chunks/status-page-marker-BRX9Ib-d.js.map +1 -0
- package/dist/_chunks/{walkers-BXExhzzk.js → walkers-Czu2jXFq.js} +3 -3
- package/dist/_chunks/{walkers-BXExhzzk.js.map → walkers-Czu2jXFq.js.map} +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +2 -2
- package/dist/cache/stores/memory.js +1 -1
- package/dist/cli.js +3 -3
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/history.d.ts +0 -9
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +9 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +25 -38
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +22 -0
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts +14 -2
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +9 -1
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +6 -1
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/react-root.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +10 -3
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +3 -1
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +65 -9
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +0 -15
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/use-router.d.ts +13 -6
- package/dist/client/use-router.d.ts.map +1 -1
- package/dist/config-validation.d.ts +19 -2
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +19 -0
- package/dist/routing/export-detect.d.ts.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/types.d.ts +7 -0
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +9 -4
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/csrf.d.ts +38 -19
- package/dist/server/csrf.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +8 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/form-flash.d.ts +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +249 -16
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +16 -3
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -0
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/metadata-routes.d.ts +12 -1
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +29 -1
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +8 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +8 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/{wrap-action-dispatch.d.ts → action-dispatcher.d.ts} +18 -40
- package/dist/server/rsc-entry/action-dispatcher.d.ts.map +1 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/safe-load.d.ts +5 -12
- package/dist/server/safe-load.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +1 -1
- package/docs/api/31-api-client.mdx +23 -20
- package/docs/api/34-api-config.mdx +4 -2
- package/package.json +1 -1
- package/src/client/browser-entry/action-dispatch.ts +28 -28
- package/src/client/browser-entry/post-hydration.ts +11 -1
- package/src/client/browser-entry/router-init.ts +10 -4
- package/src/client/history.ts +0 -22
- package/src/client/index.ts +2 -1
- package/src/client/link.tsx +30 -1
- package/src/client/navigation-api.ts +36 -3
- package/src/client/navigation-root.tsx +13 -1
- package/src/client/navigation-transition.ts +17 -7
- package/src/client/react-root.ts +11 -2
- package/src/client/router-effects.ts +11 -4
- package/src/client/router-pipeline.ts +4 -0
- package/src/client/router-types.ts +80 -6
- package/src/client/router.ts +36 -24
- package/src/client/segment-cache.ts +0 -65
- package/src/client/use-router.ts +25 -6
- package/src/config-validation.ts +121 -5
- package/src/index.ts +5 -8
- package/src/routing/convention-lint.ts +75 -0
- package/src/routing/export-detect.ts +88 -0
- package/src/routing/manifest-codegen.ts +6 -0
- package/src/routing/scanner.ts +9 -0
- package/src/routing/types.ts +7 -0
- package/src/server/action-handler.ts +14 -11
- package/src/server/actions.ts +37 -42
- package/src/server/als-registry.ts +1 -1
- package/src/server/csrf.ts +100 -72
- package/src/server/error-boundary-wrapper.ts +11 -12
- package/src/server/fallback-error.ts +13 -21
- package/src/server/form-flash.ts +1 -1
- package/src/server/logger.ts +19 -3
- package/src/server/metadata-collector.ts +4 -1
- package/src/server/metadata-routes.ts +23 -8
- package/src/server/pipeline-helpers.ts +89 -8
- package/src/server/pipeline.ts +27 -2
- package/src/server/route-element-builder.ts +1 -0
- package/src/server/route-matcher.ts +11 -0
- package/src/server/rsc-entry/{wrap-action-dispatch.ts → action-dispatcher.ts} +20 -62
- package/src/server/rsc-entry/index.ts +14 -27
- package/src/server/safe-load.ts +5 -12
- package/dist/_chunks/convention-lint-jKTwKwPe.js.map +0 -1
- package/dist/_chunks/dev-server-C4WZdB7L.js.map +0 -1
- package/dist/_chunks/file-cache-Dw6BJPG7.js.map +0 -1
- package/dist/_chunks/scanner-Bw0oq1HB.js.map +0 -1
- package/dist/_chunks/status-page-marker-gaihi0KZ.js.map +0 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +0 -1
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-link-status.ts","../../src/client/location-search.ts","../../src/client/link.tsx","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/navigation-api.ts","../../src/client/shallow-url.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx","../../src/client/params-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["'use client';\n\n// useLinkStatus — returns { isPending: true } while the nearest parent <Link>'s\n// navigation is in flight. No arguments — scoped via React context.\n// See design/19-client-navigation.md §\"useLinkStatus()\"\n\nimport { useContext, createContext } from 'react';\n\nexport interface LinkStatus {\n isPending: boolean;\n}\n\n/**\n * React context provided by <Link>. Holds the pending status\n * for that specific link's navigation.\n */\nexport const LinkStatusContext = createContext<LinkStatus>({ isPending: false });\n\n/**\n * Returns `{ isPending: true }` while the nearest parent `<Link>` component's\n * navigation is in flight. Must be used inside a `<Link>` component's children.\n *\n * Unlike `usePendingNavigation()` which is global, this hook is scoped to\n * the nearest parent `<Link>` — only the link the user clicked shows pending.\n *\n * ```tsx\n * 'use client'\n * import { Link, useLinkStatus } from '@timber-js/app/client'\n *\n * function Hint() {\n * const { isPending } = useLinkStatus()\n * return <span className={isPending ? 'opacity-50' : ''} />\n * }\n *\n * export function NavLink({ href, children }) {\n * return (\n * <Link href={href}>\n * {children} <Hint />\n * </Link>\n * )\n * }\n * ```\n */\nexport function useLinkStatus(): LinkStatus {\n return useContext(LinkStatusContext);\n}\n","import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\n\n/**\n * The app-visible search string from the address bar.\n *\n * Mirrors what `appVisibleSearch()` does on the server: strips key-shaped\n * `_rsc` values so the client and server agree on search state regardless\n * of whether the document URL itself carries a payload cache key (a user\n * pasting/sharing an RSC payload URL). Non-key-shaped `_rsc` values — an\n * application legitimately owning that name — survive, using the same\n * `isRscCacheKeyShape` predicate the server uses.\n */\nexport function locationSearch(): string {\n return stripRscCacheKey(window.location.search);\n}\n","'use client';\n\n// Link component — client-side navigation with progressive enhancement\n// See design/19-client-navigation.md § Progressive Enhancement\n//\n// Without JavaScript, <Link> renders as a plain <a> tag — standard browser\n// navigation. With JavaScript, the Link component's onClick handler triggers\n// RSC-based client navigation via the router.\n//\n// Each Link owns its own click handler — no global event delegation.\n// This keeps navigation within React's component tree, ensuring pending\n// state (useLinkStatus) updates atomically with the navigation.\n//\n// Typed Link: design/09-typescript.md §\"Typed Link\"\n// - href validated against known routes (via codegen overloads, not runtime)\n// - params prop typed per-route, URL interpolated at runtime\n// - searchParams prop is a query string (from `definition.buildSearchParams()`)\n// or a plain object whose values are String()-coerced\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport {\n useRef,\n useState,\n type AnchorHTMLAttributes,\n type ReactNode,\n type MouseEvent as ReactMouseEvent,\n} from 'react';\nimport type { LinkFunction } from './index.ts';\nimport { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.ts';\nimport {\n validateNavigationHref as validateLinkHref,\n isInternalHref,\n} from '../shared/href-validation.ts';\nimport { LinkStatusContext } from './use-link-status.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { getSsrData } from './ssr-data.ts';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.ts';\nimport { getLinkCodec } from '../params/codec-registry.ts';\nimport { locationSearch } from './location-search.ts';\nimport { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\nimport type { LinkStatus } from './use-link-status.ts';\n\nconst LINK_PENDING: LinkStatus = { isPending: true };\nconst LINK_IDLE: LinkStatus = { isPending: false };\n\n// ─── Current Search Params ────────────────────────────────────────\n\n/**\n * Read the current URL's search string without requiring a React hook.\n * On the client, reads window.location.search. During SSR, reads the raw\n * query string from the request context (getSsrData) — the same `''` or\n * `?…` shape, preserving repeated keys (`?tag=a&tag=b`) so the\n * server-rendered href matches what the hydrated client rebuilds from the\n * address bar (TIM-1428). Returns empty string if unavailable.\n */\nfunction getCurrentSearch(): string {\n if (typeof window !== 'undefined') return locationSearch();\n return getSsrData()?.search ?? '';\n}\n\n/** Native in-page scrolling needs neither an RSC navigation nor a prefetch. */\nfunction isNativeFragmentLink(anchor: HTMLAnchorElement): boolean {\n // Default browser navigation follows the rendered anchor, which may differ\n // from a freshly resolved preserveSearchParams URL after a shallow update.\n const href = anchor.href;\n const hashIndex = href.indexOf('#');\n // Compare browser-serialized URLs, excluding only fragments. URL.hash and\n // URL.search erase the distinction between absent and explicitly empty\n // delimiters, but '/current' and '/current?' are different documents.\n return hashIndex !== -1 && href.slice(0, hashIndex) === window.location.href.split('#', 1)[0];\n}\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type OnNavigateEvent = {\n preventDefault: () => void;\n};\n\nexport type OnNavigateHandler = (e: OnNavigateEvent) => void;\n\n/**\n * Base props shared by all Link variants.\n *\n * Exported so the public `LinkFunction` interface (declared in\n * `./index.ts`, where module augmentation can merge into it) can\n * compose this without duplication.\n */\nexport interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** Prefetch the RSC payload on hover */\n prefetch?: boolean;\n /**\n * Scroll to top on navigation. Defaults to true.\n * Set to false for tabbed interfaces where content changes within a fixed layout.\n */\n scroll?: boolean;\n /**\n * Preserve search params from the current URL across navigation.\n *\n * - `true` — preserve ALL current search params (target params take precedence)\n * - `string[]` — preserve only the named params (e.g. `['private', 'token']`)\n *\n * Useful for route-group gating where a search param (e.g. `?private=access`)\n * must persist across internal navigations. The target href's own search params\n * always take precedence over preserved ones.\n *\n * During SSR, reads search params from the request context. On the client,\n * reads from the current URL and updates reactively when the URL changes.\n */\n preserveSearchParams?: true | string[];\n /**\n * Called before client-side navigation commits. Call `e.preventDefault()`\n * to cancel the default navigation — the caller is then responsible for\n * navigating (e.g. via `router.push()`).\n *\n * Only fires for client-side SPA navigations, not full page loads.\n * Has no effect during SSR.\n */\n onNavigate?: OnNavigateHandler;\n children?: ReactNode;\n}\n\n// ─── Typed Link Props ────────────────────────────────────────────\n\n/**\n * Widen server-side string params to string | number for Link convenience.\n * Exported for use by codegen-generated overloads.\n */\nexport type LinkSegmentParams<T> = {\n [K in keyof T]: [string] extends [T[K]] ? string | number : T[K];\n};\n\n// ─── External Href Types ─────────────────────────────────────────\n//\n// `ExternalHref` and the public `LinkFunction` interface live in\n// `./index.ts` rather than this file. They MUST be originally declared\n// in the same module that the codegen augments (`@timber-js/app/client`)\n// so that codegen-generated per-route call signatures merge with the\n// same interface that types the `Link` constant. Re-exporting an\n// interface via `export type {}` does NOT participate in module\n// augmentation merging — only originally-declared interfaces do.\n// See TIM-624.\n\n// ─── searchParams prop shapes ────────────────────────────────────\n//\n// Two shapes, discriminated at runtime by `typeof === 'string'`:\n//\n// 1. Codec-aware (preferred) — a query string built from a definition:\n// searchParams={productParams.buildSearchParams({ page: 2, search: 'boots' })}\n// `buildSearchParams` applies each codec, honours `withUrlKey` aliases,\n// and omits values equal to their default. It is typed `Partial<T>` at\n// the call site, so the definition supplies the type checking.\n//\n// 2. Plain object (escape hatch) — every value is String()-coerced:\n// searchParams={{ ref: 'email' }}\n// No codecs, no aliases. This is for one-off params that have no\n// definition. Passing a *definition's* keys this way is a mistake the\n// types cannot catch: `{ search: 'x' }` emits `?search=x`, where the\n// definition would emit `?q=x`. Prefer (1) whenever a definition exists.\n//\n// Why a STRING and not a URLSearchParams: a Link is routinely rendered by a\n// Server Component, so this prop crosses the RSC Flight boundary before the\n// client Link runs. `URLSearchParams` is iterable, and React serializes any\n// iterable as an array — so the prop arrived as `[['pg','2'], …]`, fell into\n// the plain-object branch, and rendered `?0=pg&0=2`. Strings survive Flight\n// unchanged. (Codex P1 on PR #1021; reproduced against a live dev server.)\n//\n// TIM-1343 removed the third shape — a flat values object resolved against a\n// runtime registry keyed by href. It required scanning a `params.ts`\n// convention file, a generated registry module, and eager imports of every\n// route's definition into all three entries, to save one `.buildSearchParams`\n// call. See design/23-search-params.md §\"Link Integration\".\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };\n\ntype LinkSearchParamsProp = string | Record<string, unknown>;\n\n/**\n * Runtime-only loose props used internally by the Link implementation.\n * Not exposed to callers — the public API uses LinkFunction.\n */\ninterface LinkRuntimeProps extends LinkBaseProps {\n href: string;\n segmentParams?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n}\n\n// Legacy exports for backward compat (used by buildLinkProps, tests, etc.)\nexport type LinkPropsWithHref = LinkBaseProps & {\n href: string;\n segmentParams?: never;\n searchParams?: LinkSearchParamsProp;\n};\nexport type LinkPropsWithParams = LinkRuntimeProps & {\n segmentParams: Record<string, ParamValue>;\n};\nexport type LinkProps = LinkRuntimeProps;\n\nexport { validateLinkHref, isInternalHref };\n\n// ─── URL Interpolation ──────────────────────────────────────────\n\n/**\n * Interpolate dynamic segments in a route pattern with actual values.\n * e.g. interpolateParams(\"/products/[id]\", { id: \"123\" }) → \"/products/123\"\n *\n * Supports:\n * - [param] → single segment\n * - [...param] → catch-all (joined with /)\n * - [[...param]] → optional catch-all (omitted if undefined/empty)\n */\n/**\n * Parse a route pattern's path portion into classified segments.\n * Exported for testing. Uses the shared character-based classifier.\n */\nexport function parseSegments(pattern: string): UrlSegment[] {\n return pattern.split('/').filter(Boolean).map(classifyUrlSegment);\n}\n\n/**\n * Resolve a single classified segment into its string representation.\n * Returns null for optional catch-all with no value (filtered out before join).\n *\n * When schema codecs are registered (via virtual:timber-schema), uses\n * codec.serialize() for URL construction instead of plain String().\n */\nfunction resolveSegment(\n seg: UrlSegment,\n params: Record<string, ParamValue>,\n pattern: string\n): string | null {\n switch (seg.kind) {\n case 'static':\n return seg.value;\n\n case 'optional-catch-all': {\n const value = params[seg.name];\n if (value === undefined || (Array.isArray(value) && value.length === 0)) {\n return null;\n }\n const codec = getLinkCodec(`[[...${seg.name}]]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) return null;\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'catch-all': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(\n `<Link> missing required catch-all param \"${seg.name}\" for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[...${seg.name}]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" codec returned null for pattern \"${pattern}\".`\n );\n }\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n if (segments.length === 0) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" must have at least one segment for pattern \"${pattern}\".`\n );\n }\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'dynamic': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(`<Link> missing required param \"${seg.name}\" for pattern \"${pattern}\".`);\n }\n if (Array.isArray(value)) {\n throw new Error(\n `<Link> param \"${seg.name}\" expected a string but received an array for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[${seg.name}]`);\n const str = codec ? (codec.serialize(value) ?? String(value)) : String(value);\n const encoded = encodeURIComponent(str);\n const prefix = seg.prefix ?? '';\n const suffix = seg.suffix ?? '';\n return prefix + encoded + suffix;\n }\n }\n}\n\n/**\n * Split a URL pattern into the path portion and any trailing ?query/#hash suffix.\n * Uses URL parsing for correctness rather than manual index arithmetic.\n */\nfunction splitPatternSuffix(pattern: string): [path: string, suffix: string] {\n if (!pattern.includes('?') && !pattern.includes('#')) {\n return [pattern, ''];\n }\n const url = new URL(pattern, 'http://x');\n const suffix = url.search + url.hash;\n const path = pattern.slice(0, pattern.length - suffix.length);\n return [path, suffix];\n}\n\nexport function interpolateParams(pattern: string, params: Record<string, ParamValue>): string {\n const [pathPart, suffix] = splitPatternSuffix(pattern);\n\n const resolved = parseSegments(pathPart)\n .map((seg) => resolveSegment(seg, params, pattern))\n .filter((s): s is string => s !== null);\n return ('/' + resolved.join('/') || '/') + suffix;\n}\n\n// ─── Resolve Href ───────────────────────────────────────────────\n\n/**\n * Resolve the final href string from Link props.\n *\n * Handles:\n * - params interpolation into route patterns\n * - searchParams serialization (see the two shapes documented above)\n * - Validation that searchParams and inline query strings are exclusive\n */\n/**\n * Tolerate a leading '?'. `buildSearchParams()` never emits one, but callers\n * hand-rolling a query string reasonably might, and silently producing\n * `?%3Fa=b` for it would be a worse failure than accepting both.\n */\nfunction stripLeadingQuestionMark(qs: string): string {\n return qs.startsWith('?') ? qs.slice(1) : qs;\n}\n\n/**\n * Escape-hatch serialization for a plain object: String()-coerce every\n * value, append arrays as repeated keys, skip null/undefined.\n *\n * Deliberately codec-free and alias-free — see the shape docs above.\n */\nfunction coerceToQueryString(values: Record<string, unknown>): string {\n const usp = new URLSearchParams();\n for (const [key, val] of Object.entries(values)) {\n if (val === undefined || val === null) continue;\n if (Array.isArray(val)) {\n for (const item of val) usp.append(key, String(item));\n } else {\n usp.set(key, String(val));\n }\n }\n return usp.toString();\n}\n\nexport function resolveHref(\n href: string,\n params?: Record<string, ParamValue>,\n searchParams?: LinkSearchParamsProp\n): string {\n let resolvedPath = href;\n\n // Interpolate params if provided\n if (params) {\n resolvedPath = interpolateParams(href, params);\n }\n\n // Serialize searchParams if provided\n if (searchParams) {\n // Validate: searchParams prop and inline query string are mutually exclusive\n if (resolvedPath.includes('?')) {\n throw new Error(\n '<Link> received both a searchParams prop and a query string in href. ' +\n 'These are mutually exclusive — use one or the other.'\n );\n }\n\n // A string is already a serialized query (from\n // `definition.buildSearchParams()`), so it passes through untouched.\n // `typeof` is the only discriminator that survives the RSC Flight\n // boundary — see the shape docs above for what happened when this was\n // an object test.\n const qs =\n typeof searchParams === 'string'\n ? stripLeadingQuestionMark(searchParams)\n : coerceToQueryString(searchParams);\n\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n }\n\n return resolvedPath;\n}\n\n// ─── Build Props ─────────────────────────────────────────────────\n\ninterface LinkOutputProps {\n href: string;\n}\n\n/**\n * Build the HTML attributes for a Link. Separated from the component\n * for testability — the component just spreads these onto an <a>.\n */\nexport function buildLinkProps(\n props: Pick<LinkPropsWithHref, 'href'> & {\n params?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n }\n): LinkOutputProps {\n const resolvedHref = resolveHref(props.href, props.params, props.searchParams);\n validateLinkHref(resolvedHref);\n return { href: resolvedHref };\n}\n\n// ─── Click Handler ───────────────────────────────────────────────\n\n/**\n * Should this click be intercepted for SPA navigation?\n *\n * Returns false (pass through to browser) when:\n * - Modified keys are held (Ctrl, Meta, Shift, Alt) — open in new tab\n * - The click is not the primary button\n * - The event was already prevented by a parent handler\n * - The link has target=\"_blank\" or similar\n * - The link has a download attribute\n * - The href is external\n */\nfunction shouldInterceptClick(\n event: ReactMouseEvent<HTMLAnchorElement>,\n resolvedHref: string\n): boolean {\n if (event.button !== 0) return false;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;\n if (event.defaultPrevented) return false;\n\n const anchor = event.currentTarget;\n if (anchor.target && anchor.target !== '_self') return false;\n if (anchor.hasAttribute('download')) return false;\n\n if (!isInternalHref(resolvedHref)) return false;\n\n return true;\n}\n\n// ─── Link Component ──────────────────────────────────────────────\n\n/**\n * Navigation link with progressive enhancement.\n *\n * Renders as a plain `<a>` tag — works without JavaScript. When the client\n * runtime is active, the Link's onClick handler triggers RSC-based client\n * navigation via the router. No global event delegation — each Link owns\n * its own click handling.\n *\n * Supports typed routes via the Routes interface (populated by codegen).\n * At runtime:\n * - `segmentParams` prop interpolates dynamic segments in the href pattern\n * - `searchParams` prop serializes query parameters via a SearchParamsDefinition\n *\n * Typed via the LinkFunction callable interface. The base call signature\n * forbids segmentParams; per-route signatures are added by codegen via\n * interface merging. See TIM-624.\n */\n// Cast to LinkFunction — the callable interface provides the public type,\n// but the implementation destructures LinkRuntimeProps internally.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const Link: LinkFunction = function LinkImpl(props: any) {\n const {\n href,\n prefetch,\n scroll,\n segmentParams,\n searchParams,\n preserveSearchParams,\n onNavigate,\n onClick: userOnClick,\n onMouseEnter: userOnMouseEnter,\n children,\n ...rest\n } = props as LinkRuntimeProps;\n const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });\n\n // ─── Per-link pending state ─────────────────────────────────────────\n // Local `useState`, deliberately NOT `useTransition` (TIM-1307).\n //\n // Either shape keeps the re-render local — the state lives on this Link's\n // own fiber, so no sibling link is touched. The difference is what wrapping\n // `router.navigate()` in a transition does to the REST of the app: returning\n // a thenable from `startTransition` hands it to `ReactSharedInternals.S`,\n // which calls react-dom's `entangleAsyncAction`. That opens an action scope\n // whose `currentEntangledLane` collects every transition update scheduled\n // while it is open — including the router's `root.render` of `NavigationRoot`, which is a\n // fully synchronous `startTransition` in a different component — and\n // rendering that lane suspends on `currentEntangledActionThenable` until the\n // action settles. `navigateTransition` awaits `decodePromise`, so a Link\n // navigation used to commit only after the whole Flight stream had decoded,\n // while `useRouter().push()` committed as soon as React could render it.\n //\n // Two commit-timing regimes, and the slower one was the dominant path: no\n // streaming reveal, and the TIM-1301 publish (address bar, segment cache,\n // `timber:navigation-end`) waited for full decode. Not opening an action\n // scope is what collapses them into one.\n //\n // `isPending` runs from the click until BOTH `router.navigate()` has settled\n // (after `decodePromise` — the lifecycle the router's pending store and the\n // TopLoader use) AND the navigation's `onCommit` has fired. The second\n // condition is what keeps the flag honest: the promise resolves on decode,\n // and a destination with a pending Suspense boundary is still off screen\n // then. A clear scheduled at that point — urgent or in its own transition —\n // can commit ahead of the suspended tree, so the link flashes idle for a\n // frame while the old page is still showing (TIM-1418). Wrapping the clear\n // in `startTransition` was tried: it only holds when both updates share a\n // lane, and React assigns lanes per event, so the decode-time clear and the\n // click-time `root.render` never do. (Next.js gets the shared lane because\n // it hands React the tree in the click event; timber fetches first.) The\n // commit itself is the only signal that cannot beat the commit, and it is\n // the router's to give: `onCommit` fires exactly once, on the commit or\n // when the navigation is abandoned — superseded or failed — so the link\n // can never be left pending for a commit that will not come.\n //\n // Order is not fixed: a destination that does not suspend commits before\n // decode finishes (streaming reveal), so whichever of settle/commit comes\n // second clears.\n //\n // `clickSeq` guards the same-link double click: the first navigation is\n // superseded (its promise RESOLVES — `runNavigation` swallows AbortErrors —\n // and its `onCommit` fires) while the second is still in flight, and only\n // the newest click for this link may clear its flag. Written on click and\n // nowhere else, so there is no reset for a settle handler to race.\n const [isPending, setIsPending] = useState(false);\n const clickSeq = useRef(0);\n const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;\n\n // Preserve search params from the current URL when requested.\n // Read via getCurrentSearch() rather than a hook, to avoid an\n // unconditional hook call for a prop most links don't pass. On the\n // client, window.location.search is always current; during SSR,\n // getSsrData() provides the request's raw query string.\n const internal = isInternalHref(baseHref);\n\n // Only preserve search params for internal links — leaking current\n // page params (tokens, UTM, etc.) to external domains is a data leak.\n const resolvedHref =\n preserveSearchParams && internal\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : baseHref;\n\n // Event callbacks may update the URL, so resolve preserved params at use time.\n const resolveEventUrl = () =>\n new URL(\n preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref,\n window.location.href\n );\n\n // ─── Click handler ───────────────────────────────────────────\n // Each Link component owns its click handling. The router is\n // accessed via the singleton ref — during SSR, getRouterOrNull()\n // returns null and onClick is a no-op (the <a> works as a plain link).\n const handleClick = internal\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n // Call user's onClick first (e.g., analytics)\n userOnClick?.(event);\n\n if (!shouldInterceptClick(event, resolvedHref)) return;\n\n // Native anchor scrolling is not an SPA navigation. Decide before\n // onNavigate can cancel it, just as we do before hover prefetching.\n if (isNativeFragmentLink(event.currentTarget)) return;\n\n // Call onNavigate if provided — allows caller to cancel\n if (onNavigate) {\n let prevented = false;\n onNavigate({\n preventDefault: () => {\n prevented = true;\n },\n });\n if (prevented) {\n event.preventDefault();\n return;\n }\n }\n\n const router = getRouterOrNull();\n if (!router) return;\n\n // Keep the post-callback read: onNavigate may change the current URL\n // and therefore the search params to preserve or fragment locality.\n const resolved = resolveEventUrl();\n if (isNativeFragmentLink(event.currentTarget)) return;\n\n event.preventDefault();\n\n const shouldScroll = scroll !== false;\n // Keep the #fragment — the router commits it to the address bar and\n // scrolls to the matching element after render. The hash is stripped\n // from the RSC fetch URL inside the router (TIM-1035).\n const absoluteHref = resolved.pathname + stripRscCacheKey(resolved.search) + resolved.hash;\n\n const seq = ++clickSeq.current;\n let settled = false;\n let committed = false;\n const clear = () => {\n if (clickSeq.current === seq) setIsPending(false);\n };\n const onCommit = () => {\n committed = true;\n if (settled) clear();\n };\n const settle = () => {\n settled = true;\n if (committed) clear();\n };\n setIsPending(true);\n const navigation = router.navigate(absoluteHref, { scroll: shouldScroll, onCommit });\n navigation.then(settle, (error: unknown) => {\n clear();\n // Rethrow, so a navigation error that the router did not already\n // recover from surfaces as an unhandled rejection — the same\n // regime as `useRouter().push()`'s `void router.navigate(...)`.\n //\n // It used to reach the nearest error boundary instead: React\n // re-throws a rejected async action during render (measured on\n // 19.2.7). That replaced the departing page with the app's error\n // UI, which contradicts what every other failure path here\n // promises — a navigation that fails leaves the user on the page\n // they were already looking at (TIM-1306). Recoverable failures\n // never get here anyway; `runNavigation` swallows AbortErrors and\n // `recoverFromNavigationError` turns a failed fetch into a full\n // document load.\n throw error;\n });\n }\n : userOnClick; // External links — just pass through user's onClick\n\n // ─── Hover prefetch ──────────────────────────────────────────\n const handleMouseEnter =\n internal && prefetch\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n userOnMouseEnter?.(event);\n const router = getRouterOrNull();\n if (router) {\n if (isNativeFragmentLink(event.currentTarget)) return;\n const resolved = resolveEventUrl();\n router.prefetch(resolved.pathname + stripRscCacheKey(resolved.search));\n }\n }\n : userOnMouseEnter;\n\n return (\n <a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>\n <LinkStatusContext.Provider value={linkStatus}>{children}</LinkStatusContext.Provider>\n </a>\n );\n};\n","/**\n * useRouter() — client-side hook for programmatic navigation.\n *\n * Returns a router instance with push, replace, refresh, back, forward,\n * and prefetch methods. Compatible with Next.js's `useRouter()` from\n * `next/navigation` (App Router).\n *\n * This wraps timber's internal RouterInstance in the Next.js-compatible\n * AppRouterInstance shape that ecosystem libraries expect.\n *\n * NOTE: Unlike Next.js, these methods do NOT wrap navigation in\n * startTransition. In Next.js, router state is React state (useReducer)\n * so startTransition defers the update and provides isPending tracking.\n * In timber, navigation calls reactRoot.render() which is a root-level\n * render — startTransition has no effect on root renders.\n *\n * Navigation state (pathname, search) is delivered atomically via\n * NavigationContext embedded in the element tree passed to\n * reactRoot.render(). See design/19-client-navigation.md §\"NavigationContext\".\n *\n * For loading UI during navigation, use:\n * - useLinkStatus() — per-link pending indicator (inside <Link>)\n * - usePendingNavigation() — global navigation pending state\n */\n\nimport { getRouterOrNull } from './router-ref.ts';\nimport { validateNavigationHref } from '../shared/href-validation.ts';\n\nexport interface AppRouterInstance {\n /** Navigate to a URL, pushing a new history entry */\n push(href: string, options?: { scroll?: boolean }): void;\n /** Navigate to a URL, replacing the current history entry */\n replace(href: string, options?: { scroll?: boolean }): void;\n /** Refresh the current page (re-fetch RSC payload) */\n refresh(): void;\n /** Navigate back in history */\n back(): void;\n /** Navigate forward in history */\n forward(): void;\n /** Prefetch an RSC payload for a URL */\n prefetch(href: string): void;\n}\n\n/**\n * Get a router instance for programmatic navigation.\n *\n * Compatible with Next.js's `useRouter()` from `next/navigation`.\n *\n * Methods lazily resolve the global router when invoked (during user\n * interaction) rather than capturing it at render time. This is critical\n * because during hydration, React synchronously executes component render\n * functions *before* the router is bootstrapped in browser-entry.ts.\n * If we eagerly captured the router during render, components would get\n * a null reference and be stuck with silent no-ops forever.\n *\n * Returns safe no-ops during SSR or before bootstrap. The `typeof window`\n * check is insufficient because Vite's client SSR environment defines\n * `window`, so we use a try/catch on getRouter() — but only at method\n * invocation time, not at render time.\n */\nexport function useRouter(): AppRouterInstance {\n return {\n push(href: string, options?: { scroll?: boolean }) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error(\n '[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.'\n );\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll });\n },\n replace(href: string, options?: { scroll?: boolean }) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().replace() called but router is not initialized.');\n }\n return;\n }\n void router.navigate(href, { scroll: options?.scroll, replace: true });\n },\n refresh() {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().refresh() called but router is not initialized.');\n }\n return;\n }\n void router.refresh();\n },\n back() {\n if (typeof window !== 'undefined') window.history.back();\n },\n forward() {\n if (typeof window !== 'undefined') window.history.forward();\n },\n prefetch(href: string) {\n const router = getRouterOrNull();\n if (!router) return; // Silent — prefetch failure is non-fatal\n router.prefetch(href);\n },\n };\n}\n","/**\n * usePathname() — client-side hook for reading the current pathname.\n *\n * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').\n * Updates when client-side navigation changes the URL.\n *\n * One unconditional read of NavigationContext, on every side (TIM-1425):\n *\n * - In the browser, the provider wraps the RSC payload in the router's `renderTree`, so\n * the pathname updates in the same render pass as the new tree.\n * - During SSR, the wrapper chain mounts the same provider with the request's\n * pathname (TIM-1424), so this is the identical code path — no ALS read,\n * no fallback tiers.\n * - In the RSC environment, this module is never evaluated — the shims plugin\n * resolves next/navigation to navigation-rsc.ts, which has a throwing stub\n * (TIM-1420).\n * - Called outside a component entirely, React itself throws its\n * invalid-hook-call error — loud, not guessed-at from window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { useNavigationContext } from './navigation-context.ts';\n\n/**\n * Read the current URL pathname.\n *\n * Throws when no NavigationProvider is above the caller (a component rendered\n * outside the timber app — in tests, render inside the timber providers).\n */\nexport function usePathname(): string {\n const nav = useNavigationContext();\n if (nav === null) {\n throw new Error(\n '[timber] usePathname() was called outside the timber app tree ' +\n '(no NavigationProvider found). In tests, render the component ' +\n 'inside the timber providers.'\n );\n }\n return nav.pathname;\n}\n","/**\n * Navigation API integration — progressive enhancement for client navigation.\n *\n * When the Navigation API (`window.navigation`) is available, this module\n * provides an intercept-based navigation model that replaces the separate\n * popstate + click handler approach with a single navigate event listener.\n *\n * Key benefits:\n * - Intercepts ALL navigations (link clicks, form submissions, back/forward)\n * - Built-in AbortSignal per navigation (auto-aborts in-flight fetches)\n * - Per-entry state via NavigationHistoryEntry.getState()\n * - navigation.transition for progress tracking\n *\n * When unavailable, all functions are no-ops and the History API fallback\n * in browser-entry.ts handles navigation.\n *\n * See design/19-client-navigation.md\n */\n\nimport { isHardNavigating } from './navigation-root.tsx';\nimport { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\n\n// ─── Feature Detection ───────────────────────────────────────────\n\n/**\n * Returns true if the Navigation API is available in the current environment.\n * Feature-detected at runtime — no polyfill.\n */\nexport function hasNavigationApi(): boolean {\n return typeof window !== 'undefined' && 'navigation' in window && window.navigation != null;\n}\n\n/**\n * Get the Navigation API instance. Returns null if unavailable.\n */\nexport function getNavigationApi(): Navigation | null {\n if (!hasNavigationApi()) return null;\n return window.navigation;\n}\n\n// ─── Navigation API Controller ───────────────────────────────────\n\n/**\n * Callbacks for the Navigation API event handler.\n *\n * When the Navigation API intercepts a navigation, it delegates to these\n * callbacks which run the RSC fetch + render pipeline.\n */\nexport interface NavigationApiCallbacks {\n /**\n * Handle a push/replace navigation intercepted by the Navigation API.\n * This covers both Link <a> clicks (user-initiated) and external\n * navigations (plain <a> tags, programmatic).\n * The Navigation API handles the URL update via event.intercept().\n */\n onExternalNavigate: (\n url: string,\n options: { replace: boolean; signal: AbortSignal; scroll?: boolean; departingUrl?: string }\n ) => Promise<void>;\n\n /**\n * Handle a traversal (back/forward button). The Navigation API intercepts\n * the traversal and delegates to us for RSC replay/fetch.\n */\n onTraverse: (url: string, scrollY: number, signal: AbortSignal) => Promise<void>;\n\n /**\n * Called when a shallow URL update is intercepted (e.g., nuqs with\n * shallow: true, or replaceUrl). The URL has already been committed —\n * this callback syncs NavigationContext.search so useSearchParams()\n * reflects the new value without a full router navigation.\n */\n onShallowNavigate?: (url: string) => void;\n}\n\n/**\n * Controller returned by setupNavigationApi. Provides methods to\n * coordinate between the router and the navigate event listener.\n */\nexport interface NavigationApiController {\n /**\n * Set the router-navigating flag. When `true`, the next navigate event\n * (from pushState/replaceState) is recognized as router-initiated. The\n * handler still intercepts it — but ties the browser's native loading\n * state to a deferred promise instead of running the RSC pipeline again.\n *\n * This means `navigation.transition` is active for the full duration of\n * every router-initiated navigation, giving the browser a native loading\n * indicator (tab spinner, address bar) aligned with the TopLoader.\n *\n * Must be called synchronously around pushState/replaceState:\n * controller.setRouterNavigating(true);\n * history.pushState(...); // navigate event fires, intercepted\n * controller.setRouterNavigating(false); // flag off, deferred stays open\n */\n setRouterNavigating: (value: boolean) => void;\n\n /**\n * Resolve the deferred promise created by setRouterNavigating(true),\n * clearing the browser's native loading state. Call this when the\n * navigation fully completes — the same finally block in router.navigate\n * that clears the router's pending store.\n */\n completeRouterNavigation: () => void;\n\n /**\n * Initiate a navigation via the Navigation API (`navigation.navigate()`).\n * Unlike `history.pushState()`, this fires the navigate event BEFORE\n * committing the URL — allowing Chrome to show its native loading\n * indicator while the intercept handler runs.\n *\n * Must be called with setRouterNavigating(true) active so the handler\n * recognizes it as router-initiated and uses the deferred promise.\n */\n navigate: (url: string, replace: boolean) => void;\n\n /**\n * Save scroll position into the current navigation entry's state.\n * Uses navigation.updateCurrentEntry() for per-entry scroll storage.\n */\n saveScrollPosition: (scrollY: number) => void;\n\n /**\n * Check if the Navigation API has an active transition.\n * Returns the transition object if available, null otherwise.\n */\n hasActiveTransition: () => boolean;\n\n /** Remove the navigate event listener. */\n cleanup: () => void;\n}\n\n/**\n * Set up the Navigation API navigate event listener.\n *\n * Intercepts same-origin navigations and delegates to the provided callbacks.\n * Router-initiated navigations (pushState from router.navigate) are detected\n * via a synchronous flag and NOT intercepted — the router already handles them.\n *\n * Returns a controller for coordinating with the router.\n */\nexport function setupNavigationApi(callbacks: NavigationApiCallbacks): NavigationApiController {\n const nav = getNavigationApi()!;\n\n let routerNavigating = false;\n\n // Deferred promise for router-initiated navigations. Created when\n // setRouterNavigating(true) is called, resolved by completeRouterNavigation().\n // The navigate event handler intercepts with this promise so the browser's\n // native loading state (tab spinner) stays active until the navigation\n // completes — the same lifecycle the TopLoader is driven by.\n let routerNavDeferred: { promise: Promise<void>; resolve: () => void } | null = null;\n\n function handleNavigate(event: NavigateEvent): void {\n // Skip non-interceptable navigations (cross-origin, etc.)\n if (!event.canIntercept) return;\n\n // Hard navigation guard: when the router has triggered a full page\n // load (500 error, version skew), skip interception entirely so the\n // browser performs the MPA navigation. Without this guard, setting\n // window.location.href fires a navigate event that we'd intercept,\n // running the RSC pipeline again → 500 → window.location.href →\n // navigate event → infinite loop.\n // See design/19-client-navigation.md §\"Hard Navigation Guard\"\n if (isHardNavigating()) return;\n\n // Skip download requests\n if (event.downloadRequest) return;\n\n // Skip blob: URLs — these are almost always downloads or object-URL\n // navigations initiated by the host page (e.g., generated files, PDFs).\n // The RSC pipeline cannot handle them, and intercepting would break\n // the download/open behavior the host page expects.\n if (event.destination.url.startsWith('blob:')) return;\n\n // Skip hash-only changes — let the browser handle scroll-to-anchor\n if (event.hashChange) return;\n\n // Shallow URL updates (e.g., nuqs search param changes). The navigation\n // only changes the URL — no server round trip needed. Intercept with a\n // no-op handler so the Navigation API commits the URL change without\n // triggering a full page navigation (which is the default if we don't\n // intercept). The info property is the Navigation API's built-in\n // per-navigation metadata — no side-channel flags needed.\n const info = event.info as { shallow?: boolean } | null | undefined;\n if (info?.shallow) {\n event.intercept({\n handler: () => Promise.resolve(),\n focusReset: 'manual',\n scroll: 'manual',\n });\n callbacks.onShallowNavigate?.(event.destination.url);\n return;\n }\n\n // Skip form submissions with a body (POST/PUT/etc.). These need the\n // browser's native form handling to send the request body to the server.\n // Intercepting would convert them into GET RSC navigations, dropping\n // the form data. Server actions use fetch() directly (not form navigation),\n // so they are unaffected by this check.\n if (event.formData) return;\n\n // Skip cross-origin (defense-in-depth — canIntercept covers this)\n const destUrl = new URL(event.destination.url);\n if (destUrl.origin !== location.origin) return;\n\n // Router-initiated navigation (Link click → router.navigate → pushState).\n // The router is already running the RSC pipeline — don't run it again.\n // Instead, intercept with the deferred promise so the browser's native\n // loading state tracks the navigation's full lifecycle. This aligns the\n // tab spinner / address bar indicator with the TopLoader.\n if (routerNavigating && routerNavDeferred) {\n event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n handler: () => routerNavDeferred!.promise,\n });\n return;\n }\n\n // Skip reload navigations — let the browser handle full page reload\n if (event.navigationType === 'reload') return;\n\n const url = destUrl.pathname + stripRscCacheKey(destUrl.search);\n\n if (event.navigationType === 'traverse') {\n // Back/forward button — intercept and delegate to router.\n // Read scroll position from the destination entry's state.\n const entryState = event.destination.getState() as\n | { scrollY?: number; timber?: boolean }\n | null\n | undefined;\n const scrollY = entryState && typeof entryState.scrollY === 'number' ? entryState.scrollY : 0;\n\n event.intercept({\n // Manual scroll — we handle scroll restoration ourselves\n // via afterPaint (same as the History API path).\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onTraverse(url, scrollY, event.signal);\n },\n });\n } else if (event.navigationType === 'push' || event.navigationType === 'replace') {\n // Push/replace — a Link <a> click or an external navigation\n // (plain <a> tag, programmatic).\n\n // Save the departing page's scroll position BEFORE event.intercept()\n // commits the URL change. Once intercept() is called, currentEntry\n // switches to the new (destination) entry — any updateCurrentEntry()\n // call after that would save to the wrong entry.\n // See: router.navigate() also calls saveNavigationEntryScroll(), but\n // for Navigation API <a> click navigations (where Link does NOT call\n // router.navigate directly), the router's save runs inside the\n // intercept handler — too late, currentEntry has already switched.\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY: window.scrollY },\n });\n } catch {\n // Ignore — entry may be disposed\n }\n\n // Capture the departing URL BEFORE event.intercept() commits the\n // destination. Once intercept() is called, currentEntry switches and\n // getCurrentUrl() returns the destination (TIM-1232).\n const departingUrl = nav.currentEntry?.url\n ? new URL(nav.currentEntry.url).pathname +\n stripRscCacheKey(new URL(nav.currentEntry.url).search)\n : undefined;\n\n event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onExternalNavigate(url + destUrl.hash, {\n replace: event.navigationType === 'replace',\n signal: event.signal,\n scroll: undefined,\n departingUrl,\n });\n },\n });\n }\n }\n\n nav.addEventListener('navigate', handleNavigate);\n\n return {\n setRouterNavigating(value: boolean): void {\n routerNavigating = value;\n if (value) {\n // Create a new deferred promise. The navigate event handler will\n // intercept and tie the browser's loading state to this promise.\n let resolve!: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n routerNavDeferred = { promise, resolve };\n } else {\n // Flag off — but DON'T resolve the deferred here. The navigation\n // is still in flight (RSC fetch + render). completeRouterNavigation()\n // resolves it when the navigation fully completes.\n routerNavigating = false;\n }\n },\n\n completeRouterNavigation(): void {\n if (routerNavDeferred) {\n routerNavDeferred.resolve();\n routerNavDeferred = null;\n }\n },\n\n navigate(url: string, replace: boolean): void {\n // Use navigation.navigate() instead of history.pushState().\n // This fires the navigate event BEFORE committing the URL,\n // which lets Chrome show its native loading indicator while\n // the intercept handler (deferred promise) is pending.\n // history.pushState() commits the URL synchronously, so Chrome\n // sees the navigation as already complete and skips the indicator.\n nav.navigate(url, {\n history: replace ? 'replace' : 'push',\n });\n },\n\n saveScrollPosition(scrollY: number): void {\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY },\n });\n } catch {\n // Ignore errors — updateCurrentEntry may throw if entry is disposed\n }\n },\n\n hasActiveTransition(): boolean {\n return nav.transition != null;\n },\n\n cleanup(): void {\n nav.removeEventListener('navigate', handleNavigate);\n },\n };\n}\n","/**\n * Shallow URL replacement — update the browser URL bar without triggering\n * RSC navigation, TopLoader, or any server round-trip.\n *\n * Uses the Navigation API's `info: { shallow: true }` when available (Chrome),\n * which the navigate event handler intercepts with a no-op handler. Falls back\n * to raw `history.replaceState` (Safari/Firefox — no navigate event fired).\n */\n\nimport { getNavigationApi } from './navigation-api.ts';\n\nexport function replaceUrl(url: string): void {\n const nav = getNavigationApi();\n if (nav) {\n nav.navigate(url, {\n history: 'replace',\n info: { shallow: true },\n });\n } else {\n history.replaceState(history.state, '', url);\n }\n}\n","/**\n * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks\n * for reading the active segment(s) below the current layout.\n *\n * These hooks are used by navigation UIs to highlight active sections.\n * They match Next.js's API from next/navigation.\n *\n * How they work:\n * 1. Each layout is wrapped with a SegmentProvider that records its depth\n * (the URL segments from root to that layout level).\n * 2. The hooks read the current URL pathname via usePathname().\n * 3. They compare the layout's segment depth against the full URL segments\n * to determine which child segments are \"selected\" below.\n *\n * Example: For URL \"/dashboard/settings/profile\"\n * - Root layout (depth 0, segments: ['']): selected segment = \"dashboard\"\n * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = \"settings\"\n * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = \"profile\"\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { useSegmentContext } from './segment-context.ts';\nimport { usePathname } from './use-pathname.ts';\n\n/**\n * Split a pathname into URL segments.\n * \"/\" → [\"\"]\n * \"/dashboard\" → [\"\", \"dashboard\"]\n * \"/dashboard/settings\" → [\"\", \"dashboard\", \"settings\"]\n */\nexport function pathnameToSegments(pathname: string): string[] {\n return pathname.split('/');\n}\n\n/**\n * Pure function: compute the selected child segment given a layout's segment\n * depth and the current URL pathname.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns the active child segment one level below, or null if at the leaf\n */\nexport function getSelectedSegment(\n contextSegments: string[] | null,\n pathname: string\n): string | null {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments[1] || null;\n }\n\n const depth = contextSegments.length;\n return urlSegments[depth] || null;\n}\n\n/**\n * Pure function: compute all selected segments below a layout's depth.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns all active segments below the layout\n */\nexport function getSelectedSegments(contextSegments: string[] | null, pathname: string): string[] {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments.slice(1).filter(Boolean);\n }\n\n const depth = contextSegments.length;\n return urlSegments.slice(depth).filter(Boolean);\n}\n\n/**\n * Returns the active child segment one level below the layout where this\n * hook is called. Returns `null` if the layout is the leaf (no child segment).\n *\n * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegment(parallelRouteKey?: string): string | null {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegment(context?.segments ?? null, pathname);\n}\n\n/**\n * Returns all active segments below the layout where this hook is called.\n * Returns an empty array if the layout is the leaf (no child segments).\n *\n * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegments(parallelRouteKey?: string): string[] {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegments(context?.segments ?? null, pathname);\n}\n","/**\n * Client-side form utilities for server actions.\n *\n * Exports a typed `useActionState` that understands the action builder's result shape.\n * Result is typed to:\n * { data: T } | { validationErrors: Record<string, string[]> } | { serverError: { code, data? } } | null\n *\n * The action builder emits a function that satisfies both the direct call signature\n * and React's `(prevState, formData) => Promise<State>` contract.\n *\n * See design/08-forms-and-actions.md §\"Client-Side Form Mechanics\"\n */\n\nimport { useActionState as reactUseActionState, useTransition } from 'react';\nimport type {\n ActionFn,\n ActionResult,\n InputHint,\n ValidationErrors,\n} from '../server/action-client.ts';\nimport type { FormFlashData } from '../server/form-flash.ts';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/**\n * The action function type accepted by useActionState.\n * Must satisfy React's (prevState, formData) => Promise<State> contract.\n */\nexport type UseActionStateFn<TData> = (\n prevState: ActionResult<TData> | null,\n formData: FormData\n) => Promise<ActionResult<TData>>;\n\n/**\n * Return type of useActionState.\n * [result, formAction, isPending, errors]\n * The 4th element is auto-derived from result via useFormErrors logic.\n */\nexport type UseActionStateReturn<TData> = [\n result: ActionResult<TData> | null,\n formAction: (formData: FormData) => void,\n isPending: boolean,\n errors: FormErrorsResult,\n];\n\n// ─── useActionState ──────────────────────────────────────────────────────\n\n/**\n * Typed wrapper around React 19's `useActionState` that understands\n * the timber action builder's result shape.\n *\n * @param action - A server action created with createActionClient or a raw 'use server' function.\n * @param initialState - Initial state, typically `null`. Pass `getFormFlash()` for no-JS\n * progressive enhancement — the flash seeds the initial state so the form has a\n * single source of truth for both with-JS and no-JS paths.\n * @param permalink - Optional permalink for progressive enhancement (no-JS fallback URL).\n *\n * @example\n * ```tsx\n * 'use client'\n * import { useActionState } from '@timber-js/app/client'\n * import { createTodo } from './actions'\n *\n * export function NewTodoForm({ flash }) {\n * const [result, action, isPending] = useActionState(createTodo, flash)\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {result?.validationErrors?.title && <p>{result.validationErrors.title}</p>}\n * <button disabled={isPending}>Add</button>\n * </form>\n * )\n * }\n * ```\n */\nexport function useActionState<TData>(\n action: UseActionStateFn<TData>,\n initialState: ActionResult<TData> | FormFlashData | null,\n permalink?: string\n): UseActionStateReturn<TData> {\n // FormFlashData is structurally compatible with ActionResult at runtime —\n // the cast satisfies React's generic inference which would otherwise widen TData.\n const [result, formAction, isPending] = reactUseActionState(\n action,\n initialState as ActionResult<TData> | null,\n permalink\n );\n const errors = deriveFormErrors(result);\n return [result, formAction, isPending, errors];\n}\n\n// ─── useFormAction ───────────────────────────────────────────────────────\n\n/**\n * Hook for calling a server action imperatively (not via a form).\n * Returns [execute, isPending] where execute accepts the input directly.\n *\n * @example\n * ```tsx\n * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)\n * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>\n * Delete\n * </button>\n * ```\n */\nexport function useFormAction<TData = unknown, TInput = unknown>(\n action: ActionFn<TData, TInput> | ((input: TInput) => Promise<ActionResult<TData>>)\n): [\n (\n ...args: undefined extends TInput ? [input?: InputHint<TInput>] : [input: InputHint<TInput>]\n ) => Promise<ActionResult<TData>>,\n boolean,\n] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: InputHint<TInput>): Promise<ActionResult<TData>> => {\n return new Promise((resolve) => {\n startTransition(async () => {\n const result = await (action as (input: InputHint<TInput>) => Promise<ActionResult<TData>>)(\n input as InputHint<TInput>\n );\n resolve(result);\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── Form error extraction ────────────────────────────────────────────────\n\n/** Return type of the errors element in useActionState. */\nexport interface FormErrorsResult {\n /** Per-field validation errors keyed by field name. */\n fieldErrors: Record<string, string[]>;\n /** Form-level errors (from `_root` key). */\n formErrors: string[];\n /** Server error if the action threw an ActionError. */\n serverError: { code: string; data?: Record<string, unknown> } | null;\n /** Whether any errors are present. */\n hasErrors: boolean;\n /** Get the first error message for a field, or null. */\n getFieldError: (field: string) => string | null;\n}\n\n/**\n * Derive FormErrorsResult from an action result.\n * Used internally by useActionState 4th tuple element.\n * @internal — exported for test access only.\n */\nexport function deriveFormErrors<TData>(\n result:\n | ActionResult<TData>\n | {\n validationErrors?: ValidationErrors;\n serverError?: { code: string; data?: Record<string, unknown> };\n }\n | null\n): FormErrorsResult {\n const empty: FormErrorsResult = {\n fieldErrors: {},\n formErrors: [],\n serverError: null,\n hasErrors: false,\n getFieldError: () => null,\n };\n\n if (!result) return empty;\n\n const validationErrors = result.validationErrors as ValidationErrors | undefined;\n const serverError = result.serverError as\n | { code: string; data?: Record<string, unknown> }\n | undefined;\n\n if (!validationErrors && !serverError) return empty;\n\n // Separate _root (form-level) errors from field errors\n const fieldErrors: Record<string, string[]> = {};\n const formErrors: string[] = [];\n\n if (validationErrors) {\n for (const [key, messages] of Object.entries(validationErrors)) {\n if (key === '_root') {\n formErrors.push(...messages);\n } else {\n fieldErrors[key] = messages;\n }\n }\n }\n\n const hasErrors =\n Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;\n\n return {\n fieldErrors,\n formErrors,\n serverError: serverError ?? null,\n hasErrors,\n getFieldError(field: string): string | null {\n const errs = fieldErrors[field];\n return errs && errs.length > 0 ? errs[0] : null;\n },\n };\n}\n","/**\n * Segment params context — the one channel params use to reach the browser.\n *\n * Params ride the RSC payload's root row as a sibling of the tree\n * (`{ tree, params, slotParams }`), rather than in four side channels that\n * raced to seed them: a response header, an inline script, and two build-time\n * manifest fields all previously carried the same record, each with its own\n * `JSON.stringify` (TIM-1294).\n *\n * Riding the payload is what makes them *typed*. `defineSchema` takes any\n * `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a\n * `bigint` — and `JSON.stringify` either flattened it to a string or threw\n * mid-response. React Flight carries those values natively, so the client\n * reads the value the server produced instead of a lossy copy of it. See\n * design/41-global-params.md §\"Transport\".\n *\n * **The client owns the provider.** There is exactly one `ParamsProvider` in\n * the browser's tree, rendered by `PayloadRoot` above the point where a\n * partial navigation splices the new payload into the retained tree. It has to\n * be there and it has to be alone: a provider *inside* the payload lands below\n * the retained region, whose own root is the departing route's provider, so\n * every reader in a skipped layout resolves to the departing record and no\n * amount of wrapping above it helps (TIM-1297).\n *\n * Ordering still holds without a bootstrap contract, for the same reason it\n * did when the provider was in the tree: a provider renders before its own\n * descendants by construction, so `useSegmentParams()` is correct during\n * hydration without anything having to run before `hydrateRoot()`.\n */\n\n'use client';\n\nimport React, { createElement, useMemo, use } from 'react';\nimport { toNullProtoRecord, type CoercedParams } from '../shared/param-value.ts';\nimport { readPublishedParams, type PublishedParams } from '../shared/payload-root.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type ParamsContextValue = PublishedParams;\n\n// ─── Context ─────────────────────────────────────────────────────\n\n/**\n * SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as\n * `NavigationContext` and `SegmentUpdateContext`.\n *\n * The RSC client bundler can duplicate a module across chunks, and with ESM\n * output each chunk gets its own module scope — so a bare `createContext` at\n * module level yields one context per chunk. This module is now reached from\n * *both* graphs: `PayloadRoot` is imported by the browser entry, while\n * `useParamsContext()` arrives through the client-reference graph with the\n * app's own components. A duplicate would put the provider on instance A and\n * every reader on instance B, so `useContext` returns `null` and every\n * `useSegmentParams()` call throws the outside-the-timber-app-tree error\n * even though the provider is mounted (the module-snapshot fallback it once\n * silently landed on was deleted in TIM-1425).\n *\n * This module was the one client context without the guard — harmless while\n * the provider travelled inside the payload, in the same graph as its readers,\n * and load-bearing the moment the client started rendering it (TIM-1297).\n *\n * The React APIs are reached through the namespace rather than named imports,\n * for the same reason `segment-update-context.ts` and `navigation-context.ts`\n * do it: React's `react-server` export provides neither `createContext` nor\n * `useContext`, and a *named* ESM import of a missing export fails at module\n * instantiation — before any feature check could run. This module is reachable\n * from every entry a Server Component imports, so the named form crashed\n * those entries outright (codex, PR #992; originally reproduced against\n * `@timber-js/app/segment-params`, an entry point since deleted by TIM-1342 —\n * the hazard is unchanged for the entries that remain).\n *\n * See design/19-client-navigation.md §\"Singleton Guarantee via globalThis\"\n */\nconst PARAMS_CTX_KEY = Symbol.for('__timber_params_ctx');\n\nfunction getOrCreateContext(): React.Context<ParamsContextValue | null> {\n const store = globalThis as Record<symbol, unknown>;\n const existing = store[PARAMS_CTX_KEY] as React.Context<ParamsContextValue | null> | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext !== 'function') {\n // RSC environment — no contexts here. Nothing in this module runs on that\n // side; it only has to import cleanly.\n return undefined as unknown as React.Context<ParamsContextValue | null>;\n }\n const ctx = React.createContext<ParamsContextValue | null>(null);\n store[PARAMS_CTX_KEY] = ctx;\n return ctx;\n}\n\nconst ParamsContext = getOrCreateContext();\n\n/**\n * Read the params provided by the tree. Returns null only when no provider\n * is above the caller — a component rendered outside a timber route. During\n * SSR the wrapper chain mounts `PayloadRoot` too (TIM-1424), so both sides\n * resolve through this context.\n */\nexport function useParamsContext(): ParamsContextValue | null {\n return React.useContext(ParamsContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface ParamsProviderProps {\n params: CoercedParams;\n slotParams: SlotParamsRecord | null;\n children?: React.ReactNode;\n}\n\n/**\n * Provides the current navigation's params to everything below it.\n *\n * Rendered only by `PayloadRoot`. Not exported: a second provider anywhere in\n * the tree would shadow this one for the region below it, which is precisely\n * the defect TIM-1297 fixed.\n *\n * This used to also write a module-level snapshot during render, as the\n * fallback for `useSegmentParams()` called outside a component. That tier is\n * gone (TIM-1425) — the provider is unconditional on every render path,\n * browser and SSR alike, so the hook reads context or throws. Removing the\n * write also removes render-phase shared mutation from the SSR environment,\n * where concurrent requests rendered through this component.\n */\nfunction ParamsProvider({ params, slotParams, children }: ParamsProviderProps) {\n // Restore the null prototype the wire could not carry. Flight rejects a\n // null-prototype object, so `withPublishedParams` flattens the records;\n // rebuilding them here is what keeps `params.constructor` returning\n // `undefined` instead of a function for a param the route does not define\n // (design/13-security.md #36c). Memoized on the props so a re-render with\n // the same records does not rebuild — the identity of what the hook returns\n // is load-bearing for `useEffect` dependencies (TIM-1285).\n const value = useMemo(\n () => ({\n params: toNullProtoRecord(params),\n slotParams: toNullProtoRecord(slotParams),\n }),\n [params, slotParams]\n );\n\n return createElement(ParamsContext.Provider, { value }, children);\n}\n\n// ─── Payload root ────────────────────────────────────────────────\n\n/**\n * The client's root: publishes a payload's params over the tree being shown.\n *\n * Rendered at the same position in the wrapper chain on **every** render path\n * — hydration, full navigation, partial navigation, popstate replay, shallow\n * search sync, and revalidation from a server action. Being unconditional is\n * load-bearing twice over: an element type that appears on one render and not\n * the next remounts everything below it, destroying exactly the layout state a\n * partial navigation exists to preserve; and a reader in a skipped layout has\n * to have *some* provider above it on every path or it falls back to the\n * module-level snapshot.\n *\n * `children` is the tree to display, which is not always `source`'s tree:\n *\n * - Full navigation, hydration, replay — `source` is the payload being shown,\n * and `children` is its own tree.\n * - **Partial navigation** — `children` is the *retained* tree and `source` is\n * the *incoming* payload. This is the case the whole design exists for: the\n * retained tree is not re-rendered, so the destination's params can only\n * reach it from above, and this provider is above it.\n *\n * `source` may be a thenable, in which case this suspends on the payload's\n * root row. That happens on the hydration path only, where the payload\n * promise was going to be rendered at this position anyway. Every other path\n * resolves the row in the router — inside the navigation transition — and\n * hands over a settled value, so a decode rejection surfaces where React\n * renders the tree and is caught by the error boundary *around* it, rather\n * than here, above every boundary the app has.\n */\nexport function PayloadRoot({ source, children }: { source: unknown; children?: React.ReactNode }) {\n const resolved = isThenable(source) ? use(source) : source;\n const { params, slotParams } = readPublishedParams(resolved);\n return createElement(ParamsProvider, { params, slotParams }, children);\n}\n\nfunction isThenable(value: unknown): value is Promise<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * One unconditional read of ParamsContext, on every side (TIM-1425):\n *\n * - In the browser, `PayloadRoot` publishes the payload's params above the\n * merge point on every render path. Params update atomically with the RSC\n * tree — no timing gap (TIM-1294, TIM-1297).\n * - During SSR, the wrapper chain mounts the same `PayloadRoot`, fed the\n * `params` half of `splitPayloadRoot(root)` — the identical derivation\n * the browser performs at hydration (TIM-1424).\n * - In the RSC environment, this module is never evaluated — the shims plugin\n * resolves next/navigation to navigation-rsc.ts (TIM-1420).\n * - Called outside a component entirely, React itself throws its\n * invalid-hook-call error.\n *\n * The module-level subscribe/notify machinery and the `currentParams`\n * snapshot that used to back a fourth fallback tier are gone (TIM-1425):\n * the provider is unconditional on every render path, so nothing read them.\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { Routes } from '../index.ts';\nimport { resolveSegmentParams } from '../shared/slot-params.ts';\nimport { useParamsContext } from './params-context.ts';\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * Throws when no `PayloadRoot` is above the caller (a component rendered\n * outside the timber app — in tests, render inside the timber providers).\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams {\n const paramsContext = useParamsContext();\n if (paramsContext === null) {\n throw new Error(\n '[timber] useSegmentParams() was called outside the timber app tree ' +\n '(no params provider found). In tests, render the component inside ' +\n 'the timber providers.'\n );\n }\n return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/client/use-link-status.ts","../../src/client/location-search.ts","../../src/client/link.tsx","../../src/client/use-router.ts","../../src/client/use-pathname.ts","../../src/client/navigation-api.ts","../../src/client/shallow-url.ts","../../src/client/use-selected-layout-segment.ts","../../src/client/form.tsx","../../src/client/params-context.ts","../../src/client/use-segment-params.ts"],"sourcesContent":["'use client';\n\n// useLinkStatus — returns { isPending: true } while the nearest parent <Link>'s\n// navigation is in flight. No arguments — scoped via React context.\n// See design/19-client-navigation.md §\"useLinkStatus()\"\n\nimport { useContext, createContext } from 'react';\n\nexport interface LinkStatus {\n isPending: boolean;\n}\n\n/**\n * React context provided by <Link>. Holds the pending status\n * for that specific link's navigation.\n */\nexport const LinkStatusContext = createContext<LinkStatus>({ isPending: false });\n\n/**\n * Returns `{ isPending: true }` while the nearest parent `<Link>` component's\n * navigation is in flight. Must be used inside a `<Link>` component's children.\n *\n * Unlike `usePendingNavigation()` which is global, this hook is scoped to\n * the nearest parent `<Link>` — only the link the user clicked shows pending.\n *\n * ```tsx\n * 'use client'\n * import { Link, useLinkStatus } from '@timber-js/app/client'\n *\n * function Hint() {\n * const { isPending } = useLinkStatus()\n * return <span className={isPending ? 'opacity-50' : ''} />\n * }\n *\n * export function NavLink({ href, children }) {\n * return (\n * <Link href={href}>\n * {children} <Hint />\n * </Link>\n * )\n * }\n * ```\n */\nexport function useLinkStatus(): LinkStatus {\n return useContext(LinkStatusContext);\n}\n","import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\n\n/**\n * The app-visible search string from the address bar.\n *\n * Mirrors what `appVisibleSearch()` does on the server: strips key-shaped\n * `_rsc` values so the client and server agree on search state regardless\n * of whether the document URL itself carries a payload cache key (a user\n * pasting/sharing an RSC payload URL). Non-key-shaped `_rsc` values — an\n * application legitimately owning that name — survive, using the same\n * `isRscCacheKeyShape` predicate the server uses.\n */\nexport function locationSearch(): string {\n return stripRscCacheKey(window.location.search);\n}\n","'use client';\n\n// Link component — client-side navigation with progressive enhancement\n// See design/19-client-navigation.md § Progressive Enhancement\n//\n// Without JavaScript, <Link> renders as a plain <a> tag — standard browser\n// navigation. With JavaScript, the Link component's onClick handler triggers\n// RSC-based client navigation via the router.\n//\n// Each Link owns its own click handler — no global event delegation.\n// This keeps navigation within React's component tree, ensuring pending\n// state (useLinkStatus) updates atomically with the navigation.\n//\n// Typed Link: design/09-typescript.md §\"Typed Link\"\n// - href validated against known routes (via codegen overloads, not runtime)\n// - params prop typed per-route, URL interpolated at runtime\n// - searchParams prop is a query string (from `definition.buildSearchParams()`)\n// or a plain object whose values are String()-coerced\n// - params and fully-resolved string href are mutually exclusive\n// - searchParams and inline query string are mutually exclusive\n\nimport {\n useRef,\n useState,\n type AnchorHTMLAttributes,\n type ReactNode,\n type MouseEvent as ReactMouseEvent,\n} from 'react';\nimport type { LinkFunction } from './index.ts';\nimport { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.ts';\nimport {\n validateNavigationHref as validateLinkHref,\n isInternalHref,\n} from '../shared/href-validation.ts';\nimport { LinkStatusContext } from './use-link-status.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { getSsrData } from './ssr-data.ts';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.ts';\nimport { getLinkCodec } from '../params/codec-registry.ts';\nimport { locationSearch } from './location-search.ts';\nimport { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\nimport type { LinkStatus } from './use-link-status.ts';\n\nconst LINK_PENDING: LinkStatus = { isPending: true };\nconst LINK_IDLE: LinkStatus = { isPending: false };\n\n// ─── Current Search Params ────────────────────────────────────────\n\n/**\n * Read the current URL's search string without requiring a React hook.\n * On the client, reads window.location.search. During SSR, reads the raw\n * query string from the request context (getSsrData) — the same `''` or\n * `?…` shape, preserving repeated keys (`?tag=a&tag=b`) so the\n * server-rendered href matches what the hydrated client rebuilds from the\n * address bar (TIM-1428). Returns empty string if unavailable.\n */\nfunction getCurrentSearch(): string {\n if (typeof window !== 'undefined') return locationSearch();\n return getSsrData()?.search ?? '';\n}\n\n/** Native in-page scrolling needs neither an RSC navigation nor a prefetch. */\nfunction isNativeFragmentLink(anchor: HTMLAnchorElement): boolean {\n // Default browser navigation follows the rendered anchor, which may differ\n // from a freshly resolved preserveSearchParams URL after a shallow update.\n const href = anchor.href;\n const hashIndex = href.indexOf('#');\n // Compare browser-serialized URLs, excluding only fragments. URL.hash and\n // URL.search erase the distinction between absent and explicitly empty\n // delimiters, but '/current' and '/current?' are different documents.\n return hashIndex !== -1 && href.slice(0, hashIndex) === window.location.href.split('#', 1)[0];\n}\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type OnNavigateEvent = {\n preventDefault: () => void;\n};\n\nexport type OnNavigateHandler = (e: OnNavigateEvent) => void;\n\n/**\n * Base props shared by all Link variants.\n *\n * Exported so the public `LinkFunction` interface (declared in\n * `./index.ts`, where module augmentation can merge into it) can\n * compose this without duplication.\n */\nexport interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {\n /** Prefetch the RSC payload on hover */\n prefetch?: boolean;\n /**\n * Scroll to top on navigation. Defaults to true.\n * Set to false for tabbed interfaces where content changes within a fixed layout.\n */\n scroll?: boolean;\n /**\n * Replace the current history entry instead of pushing a new one, so Back\n * skips the page the link was on.\n */\n replace?: boolean;\n /**\n * Preserve search params from the current URL across navigation.\n *\n * - `true` — preserve ALL current search params (target params take precedence)\n * - `string[]` — preserve only the named params (e.g. `['private', 'token']`)\n *\n * Useful for route-group gating where a search param (e.g. `?private=access`)\n * must persist across internal navigations. The target href's own search params\n * always take precedence over preserved ones.\n *\n * During SSR, reads search params from the request context. On the client,\n * reads from the current URL and updates reactively when the URL changes.\n */\n preserveSearchParams?: true | string[];\n /**\n * Called before client-side navigation commits. Call `e.preventDefault()`\n * to cancel the default navigation — the caller is then responsible for\n * navigating (e.g. via `router.push()`).\n *\n * Only fires for client-side SPA navigations, not full page loads.\n * Has no effect during SSR.\n */\n onNavigate?: OnNavigateHandler;\n /**\n * View transition types to add to this link's navigation, beside the\n * router's own `navigation-forward`. A `<ViewTransition>` in the\n * destination (or the page being left) can key its animation on them:\n *\n * ```tsx\n * <Link href=\"/products/1\" transitionTypes={['to-detail']}>…</Link>\n * ```\n *\n * The router adds them in the transition that renders the destination. A\n * `startTransition(() => { addTransitionType(t); router.push(href) })` in\n * an `onClick` does not work in timber: the destination reaches React only\n * after its fetch, in a later transition, and React hands types added in\n * the click to whatever transition commits next — not to that one\n * (design/37-navigation-api.md §\"Transition types\").\n */\n transitionTypes?: readonly string[];\n children?: ReactNode;\n}\n\n// ─── Typed Link Props ────────────────────────────────────────────\n\n/**\n * Widen server-side string params to string | number for Link convenience.\n * Exported for use by codegen-generated overloads.\n */\nexport type LinkSegmentParams<T> = {\n [K in keyof T]: [string] extends [T[K]] ? string | number : T[K];\n};\n\n// ─── External Href Types ─────────────────────────────────────────\n//\n// `ExternalHref` and the public `LinkFunction` interface live in\n// `./index.ts` rather than this file. They MUST be originally declared\n// in the same module that the codegen augments (`@timber-js/app/client`)\n// so that codegen-generated per-route call signatures merge with the\n// same interface that types the `Link` constant. Re-exporting an\n// interface via `export type {}` does NOT participate in module\n// augmentation merging — only originally-declared interfaces do.\n// See TIM-624.\n\n// ─── searchParams prop shapes ────────────────────────────────────\n//\n// Two shapes, discriminated at runtime by `typeof === 'string'`:\n//\n// 1. Codec-aware (preferred) — a query string built from a definition:\n// searchParams={productParams.buildSearchParams({ page: 2, search: 'boots' })}\n// `buildSearchParams` applies each codec, honours `withUrlKey` aliases,\n// and omits values equal to their default. It is typed `Partial<T>` at\n// the call site, so the definition supplies the type checking.\n//\n// 2. Plain object (escape hatch) — every value is String()-coerced:\n// searchParams={{ ref: 'email' }}\n// No codecs, no aliases. This is for one-off params that have no\n// definition. Passing a *definition's* keys this way is a mistake the\n// types cannot catch: `{ search: 'x' }` emits `?search=x`, where the\n// definition would emit `?q=x`. Prefer (1) whenever a definition exists.\n//\n// Why a STRING and not a URLSearchParams: a Link is routinely rendered by a\n// Server Component, so this prop crosses the RSC Flight boundary before the\n// client Link runs. `URLSearchParams` is iterable, and React serializes any\n// iterable as an array — so the prop arrived as `[['pg','2'], …]`, fell into\n// the plain-object branch, and rendered `?0=pg&0=2`. Strings survive Flight\n// unchanged. (Codex P1 on PR #1021; reproduced against a live dev server.)\n//\n// TIM-1343 removed the third shape — a flat values object resolved against a\n// runtime registry keyed by href. It required scanning a `params.ts`\n// convention file, a generated registry module, and eager imports of every\n// route's definition into all three entries, to save one `.buildSearchParams`\n// call. See design/23-search-params.md §\"Link Integration\".\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };\n\ntype LinkSearchParamsProp = string | Record<string, unknown>;\n\n/**\n * Runtime-only loose props used internally by the Link implementation.\n * Not exposed to callers — the public API uses LinkFunction.\n */\ninterface LinkRuntimeProps extends LinkBaseProps {\n href: string;\n segmentParams?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n}\n\n// Legacy exports for backward compat (used by buildLinkProps, tests, etc.)\nexport type LinkPropsWithHref = LinkBaseProps & {\n href: string;\n segmentParams?: never;\n searchParams?: LinkSearchParamsProp;\n};\nexport type LinkPropsWithParams = LinkRuntimeProps & {\n segmentParams: Record<string, ParamValue>;\n};\nexport type LinkProps = LinkRuntimeProps;\n\nexport { validateLinkHref, isInternalHref };\n\n// ─── URL Interpolation ──────────────────────────────────────────\n\n/**\n * Interpolate dynamic segments in a route pattern with actual values.\n * e.g. interpolateParams(\"/products/[id]\", { id: \"123\" }) → \"/products/123\"\n *\n * Supports:\n * - [param] → single segment\n * - [...param] → catch-all (joined with /)\n * - [[...param]] → optional catch-all (omitted if undefined/empty)\n */\n/**\n * Parse a route pattern's path portion into classified segments.\n * Exported for testing. Uses the shared character-based classifier.\n */\nexport function parseSegments(pattern: string): UrlSegment[] {\n return pattern.split('/').filter(Boolean).map(classifyUrlSegment);\n}\n\n/**\n * Resolve a single classified segment into its string representation.\n * Returns null for optional catch-all with no value (filtered out before join).\n *\n * When schema codecs are registered (via virtual:timber-schema), uses\n * codec.serialize() for URL construction instead of plain String().\n */\nfunction resolveSegment(\n seg: UrlSegment,\n params: Record<string, ParamValue>,\n pattern: string\n): string | null {\n switch (seg.kind) {\n case 'static':\n return seg.value;\n\n case 'optional-catch-all': {\n const value = params[seg.name];\n if (value === undefined || (Array.isArray(value) && value.length === 0)) {\n return null;\n }\n const codec = getLinkCodec(`[[...${seg.name}]]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) return null;\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'catch-all': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(\n `<Link> missing required catch-all param \"${seg.name}\" for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[...${seg.name}]`);\n if (codec) {\n const serialized = codec.serialize(value);\n if (!serialized) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" codec returned null for pattern \"${pattern}\".`\n );\n }\n // Codec returns a path string (may contain '/') — encode each segment\n return serialized.split('/').map(encodeURIComponent).join('/');\n }\n const segments = Array.isArray(value) ? value : [value];\n if (segments.length === 0) {\n throw new Error(\n `<Link> catch-all param \"${seg.name}\" must have at least one segment for pattern \"${pattern}\".`\n );\n }\n return segments.map(encodeURIComponent).join('/');\n }\n\n case 'dynamic': {\n const value = params[seg.name];\n if (value === undefined) {\n throw new Error(`<Link> missing required param \"${seg.name}\" for pattern \"${pattern}\".`);\n }\n if (Array.isArray(value)) {\n throw new Error(\n `<Link> param \"${seg.name}\" expected a string but received an array for pattern \"${pattern}\".`\n );\n }\n const codec = getLinkCodec(`[${seg.name}]`);\n const str = codec ? (codec.serialize(value) ?? String(value)) : String(value);\n const encoded = encodeURIComponent(str);\n const prefix = seg.prefix ?? '';\n const suffix = seg.suffix ?? '';\n return prefix + encoded + suffix;\n }\n }\n}\n\n/**\n * Split a URL pattern into the path portion and any trailing ?query/#hash suffix.\n * Uses URL parsing for correctness rather than manual index arithmetic.\n */\nfunction splitPatternSuffix(pattern: string): [path: string, suffix: string] {\n if (!pattern.includes('?') && !pattern.includes('#')) {\n return [pattern, ''];\n }\n const url = new URL(pattern, 'http://x');\n const suffix = url.search + url.hash;\n const path = pattern.slice(0, pattern.length - suffix.length);\n return [path, suffix];\n}\n\nexport function interpolateParams(pattern: string, params: Record<string, ParamValue>): string {\n const [pathPart, suffix] = splitPatternSuffix(pattern);\n\n const resolved = parseSegments(pathPart)\n .map((seg) => resolveSegment(seg, params, pattern))\n .filter((s): s is string => s !== null);\n return ('/' + resolved.join('/') || '/') + suffix;\n}\n\n// ─── Resolve Href ───────────────────────────────────────────────\n\n/**\n * Resolve the final href string from Link props.\n *\n * Handles:\n * - params interpolation into route patterns\n * - searchParams serialization (see the two shapes documented above)\n * - Validation that searchParams and inline query strings are exclusive\n */\n/**\n * Tolerate a leading '?'. `buildSearchParams()` never emits one, but callers\n * hand-rolling a query string reasonably might, and silently producing\n * `?%3Fa=b` for it would be a worse failure than accepting both.\n */\nfunction stripLeadingQuestionMark(qs: string): string {\n return qs.startsWith('?') ? qs.slice(1) : qs;\n}\n\n/**\n * Escape-hatch serialization for a plain object: String()-coerce every\n * value, append arrays as repeated keys, skip null/undefined.\n *\n * Deliberately codec-free and alias-free — see the shape docs above.\n */\nfunction coerceToQueryString(values: Record<string, unknown>): string {\n const usp = new URLSearchParams();\n for (const [key, val] of Object.entries(values)) {\n if (val === undefined || val === null) continue;\n if (Array.isArray(val)) {\n for (const item of val) usp.append(key, String(item));\n } else {\n usp.set(key, String(val));\n }\n }\n return usp.toString();\n}\n\nexport function resolveHref(\n href: string,\n params?: Record<string, ParamValue>,\n searchParams?: LinkSearchParamsProp\n): string {\n let resolvedPath = href;\n\n // Interpolate params if provided\n if (params) {\n resolvedPath = interpolateParams(href, params);\n }\n\n // Serialize searchParams if provided\n if (searchParams) {\n // Validate: searchParams prop and inline query string are mutually exclusive\n if (resolvedPath.includes('?')) {\n throw new Error(\n '<Link> received both a searchParams prop and a query string in href. ' +\n 'These are mutually exclusive — use one or the other.'\n );\n }\n\n // A string is already a serialized query (from\n // `definition.buildSearchParams()`), so it passes through untouched.\n // `typeof` is the only discriminator that survives the RSC Flight\n // boundary — see the shape docs above for what happened when this was\n // an object test.\n const qs =\n typeof searchParams === 'string'\n ? stripLeadingQuestionMark(searchParams)\n : coerceToQueryString(searchParams);\n\n if (qs) {\n resolvedPath = `${resolvedPath}?${qs}`;\n }\n }\n\n return resolvedPath;\n}\n\n// ─── Build Props ─────────────────────────────────────────────────\n\ninterface LinkOutputProps {\n href: string;\n}\n\n/**\n * Build the HTML attributes for a Link. Separated from the component\n * for testability — the component just spreads these onto an <a>.\n */\nexport function buildLinkProps(\n props: Pick<LinkPropsWithHref, 'href'> & {\n params?: Record<string, ParamValue>;\n searchParams?: LinkSearchParamsProp;\n }\n): LinkOutputProps {\n const resolvedHref = resolveHref(props.href, props.params, props.searchParams);\n validateLinkHref(resolvedHref);\n return { href: resolvedHref };\n}\n\n// ─── Click Handler ───────────────────────────────────────────────\n\n/**\n * Should this click be intercepted for SPA navigation?\n *\n * Returns false (pass through to browser) when:\n * - Modified keys are held (Ctrl, Meta, Shift, Alt) — open in new tab\n * - The click is not the primary button\n * - The event was already prevented by a parent handler\n * - The link has target=\"_blank\" or similar\n * - The link has a download attribute\n * - The href is external\n */\nfunction shouldInterceptClick(\n event: ReactMouseEvent<HTMLAnchorElement>,\n resolvedHref: string\n): boolean {\n if (event.button !== 0) return false;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;\n if (event.defaultPrevented) return false;\n\n const anchor = event.currentTarget;\n if (anchor.target && anchor.target !== '_self') return false;\n if (anchor.hasAttribute('download')) return false;\n\n if (!isInternalHref(resolvedHref)) return false;\n\n return true;\n}\n\n// ─── Link Component ──────────────────────────────────────────────\n\n/**\n * Navigation link with progressive enhancement.\n *\n * Renders as a plain `<a>` tag — works without JavaScript. When the client\n * runtime is active, the Link's onClick handler triggers RSC-based client\n * navigation via the router. No global event delegation — each Link owns\n * its own click handling.\n *\n * Supports typed routes via the Routes interface (populated by codegen).\n * At runtime:\n * - `segmentParams` prop interpolates dynamic segments in the href pattern\n * - `searchParams` prop serializes query parameters via a SearchParamsDefinition\n *\n * Typed via the LinkFunction callable interface. The base call signature\n * forbids segmentParams; per-route signatures are added by codegen via\n * interface merging. See TIM-624.\n */\n// Cast to LinkFunction — the callable interface provides the public type,\n// but the implementation destructures LinkRuntimeProps internally.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const Link: LinkFunction = function LinkImpl(props: any) {\n const {\n href,\n prefetch,\n scroll,\n replace,\n segmentParams,\n searchParams,\n preserveSearchParams,\n onNavigate,\n transitionTypes,\n onClick: userOnClick,\n onMouseEnter: userOnMouseEnter,\n children,\n ...rest\n } = props as LinkRuntimeProps;\n const { href: baseHref } = buildLinkProps({ href, params: segmentParams, searchParams });\n\n // ─── Per-link pending state ─────────────────────────────────────────\n // Local `useState`, deliberately NOT `useTransition` (TIM-1307).\n //\n // Either shape keeps the re-render local — the state lives on this Link's\n // own fiber, so no sibling link is touched. The difference is what wrapping\n // `router.navigate()` in a transition does to the REST of the app: returning\n // a thenable from `startTransition` hands it to `ReactSharedInternals.S`,\n // which calls react-dom's `entangleAsyncAction`. That opens an action scope\n // whose `currentEntangledLane` collects every transition update scheduled\n // while it is open — including the router's `root.render` of `NavigationRoot`, which is a\n // fully synchronous `startTransition` in a different component — and\n // rendering that lane suspends on `currentEntangledActionThenable` until the\n // action settles. `navigateTransition` awaits `decodePromise`, so a Link\n // navigation used to commit only after the whole Flight stream had decoded,\n // while `useRouter().push()` committed as soon as React could render it.\n //\n // Two commit-timing regimes, and the slower one was the dominant path: no\n // streaming reveal, and the TIM-1301 publish (address bar, segment cache,\n // `timber:navigation-end`) waited for full decode. Not opening an action\n // scope is what collapses them into one.\n //\n // `isPending` runs from the click until BOTH `router.navigate()` has settled\n // (after `decodePromise` — the lifecycle the router's pending store and the\n // TopLoader use) AND the navigation's `onCommit` has fired. The second\n // condition is what keeps the flag honest: the promise resolves on decode,\n // and a destination with a pending Suspense boundary is still off screen\n // then. A clear scheduled at that point — urgent or in its own transition —\n // can commit ahead of the suspended tree, so the link flashes idle for a\n // frame while the old page is still showing (TIM-1418). Wrapping the clear\n // in `startTransition` was tried: it only holds when both updates share a\n // lane, and React assigns lanes per event, so the decode-time clear and the\n // click-time `root.render` never do. (Next.js gets the shared lane because\n // it hands React the tree in the click event; timber fetches first.) The\n // commit itself is the only signal that cannot beat the commit, and it is\n // the router's to give: `onCommit` fires exactly once, on the commit or\n // when the navigation is abandoned — superseded or failed — so the link\n // can never be left pending for a commit that will not come.\n //\n // Order is not fixed: a destination that does not suspend commits before\n // decode finishes (streaming reveal), so whichever of settle/commit comes\n // second clears.\n //\n // `clickSeq` guards the same-link double click: the first navigation is\n // superseded (its promise RESOLVES — `runNavigation` swallows AbortErrors —\n // and its `onCommit` fires) while the second is still in flight, and only\n // the newest click for this link may clear its flag. Written on click and\n // nowhere else, so there is no reset for a settle handler to race.\n const [isPending, setIsPending] = useState(false);\n const clickSeq = useRef(0);\n const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;\n\n // Preserve search params from the current URL when requested.\n // Read via getCurrentSearch() rather than a hook, to avoid an\n // unconditional hook call for a prop most links don't pass. On the\n // client, window.location.search is always current; during SSR,\n // getSsrData() provides the request's raw query string.\n const internal = isInternalHref(baseHref);\n\n // Only preserve search params for internal links — leaking current\n // page params (tokens, UTM, etc.) to external domains is a data leak.\n const resolvedHref =\n preserveSearchParams && internal\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : baseHref;\n\n // Event callbacks may update the URL, so resolve preserved params at use time.\n const resolveEventUrl = () =>\n new URL(\n preserveSearchParams\n ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)\n : resolvedHref,\n window.location.href\n );\n\n // ─── Click handler ───────────────────────────────────────────\n // Each Link component owns its click handling. The router is\n // accessed via the singleton ref — during SSR, getRouterOrNull()\n // returns null and onClick is a no-op (the <a> works as a plain link).\n const handleClick = internal\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n // Call user's onClick first (e.g., analytics)\n userOnClick?.(event);\n\n if (!shouldInterceptClick(event, resolvedHref)) return;\n\n // Native anchor scrolling is not an SPA navigation. Decide before\n // onNavigate can cancel it, just as we do before hover prefetching.\n if (isNativeFragmentLink(event.currentTarget)) return;\n\n // Call onNavigate if provided — allows caller to cancel\n if (onNavigate) {\n let prevented = false;\n onNavigate({\n preventDefault: () => {\n prevented = true;\n },\n });\n if (prevented) {\n event.preventDefault();\n return;\n }\n }\n\n const router = getRouterOrNull();\n if (!router) return;\n\n // Keep the post-callback read: onNavigate may change the current URL\n // and therefore the search params to preserve or fragment locality.\n const resolved = resolveEventUrl();\n if (isNativeFragmentLink(event.currentTarget)) return;\n\n event.preventDefault();\n\n const shouldScroll = scroll !== false;\n // Keep the #fragment — the router commits it to the address bar and\n // scrolls to the matching element after render. The hash is stripped\n // from the RSC fetch URL inside the router (TIM-1035).\n const absoluteHref = resolved.pathname + stripRscCacheKey(resolved.search) + resolved.hash;\n\n const seq = ++clickSeq.current;\n let settled = false;\n let committed = false;\n const clear = () => {\n if (clickSeq.current === seq) setIsPending(false);\n };\n const onCommit = () => {\n committed = true;\n if (settled) clear();\n };\n const settle = () => {\n settled = true;\n if (committed) clear();\n };\n setIsPending(true);\n const navigation = router.navigate(absoluteHref, {\n scroll: shouldScroll,\n replace,\n transitionTypes,\n onCommit,\n });\n navigation.then(settle, (error: unknown) => {\n clear();\n // Rethrow, so a navigation error that the router did not already\n // recover from surfaces as an unhandled rejection — the same\n // regime as `useRouter().push()`'s `void router.navigate(...)`.\n //\n // It used to reach the nearest error boundary instead: React\n // re-throws a rejected async action during render (measured on\n // 19.2.7). That replaced the departing page with the app's error\n // UI, which contradicts what every other failure path here\n // promises — a navigation that fails leaves the user on the page\n // they were already looking at (TIM-1306). Recoverable failures\n // never get here anyway; `runNavigation` swallows AbortErrors and\n // `recoverFromNavigationError` turns a failed fetch into a full\n // document load.\n throw error;\n });\n }\n : userOnClick; // External links — just pass through user's onClick\n\n // ─── Hover prefetch ──────────────────────────────────────────\n const handleMouseEnter =\n internal && prefetch\n ? (event: ReactMouseEvent<HTMLAnchorElement>) => {\n userOnMouseEnter?.(event);\n const router = getRouterOrNull();\n if (router) {\n if (isNativeFragmentLink(event.currentTarget)) return;\n const resolved = resolveEventUrl();\n router.prefetch(resolved.pathname + stripRscCacheKey(resolved.search));\n }\n }\n : userOnMouseEnter;\n\n return (\n <a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>\n <LinkStatusContext.Provider value={linkStatus}>{children}</LinkStatusContext.Provider>\n </a>\n );\n};\n","/**\n * useRouter() — client-side hook for programmatic navigation.\n *\n * Returns a router instance with push, replace, refresh, back, forward,\n * and prefetch methods. Compatible with Next.js's `useRouter()` from\n * `next/navigation` (App Router).\n *\n * This wraps timber's internal RouterInstance in the Next.js-compatible\n * AppRouterInstance shape that ecosystem libraries expect.\n *\n * NOTE: Unlike Next.js, these methods do NOT wrap navigation in\n * startTransition. In Next.js, router state is React state (useReducer)\n * so startTransition defers the update and provides isPending tracking.\n * In timber, navigation calls reactRoot.render() which is a root-level\n * render — startTransition has no effect on root renders.\n *\n * Navigation state (pathname, search) is delivered atomically via\n * NavigationContext embedded in the element tree passed to\n * reactRoot.render(). See design/19-client-navigation.md §\"NavigationContext\".\n *\n * For loading UI during navigation, use:\n * - useLinkStatus() — per-link pending indicator (inside <Link>)\n * - usePendingNavigation() — global navigation pending state\n */\n\nimport { getRouterOrNull } from './router-ref.ts';\nimport { validateNavigationHref } from '../shared/href-validation.ts';\n\n/** Options for `push` and `replace`. */\nexport interface NavigateOptions {\n /** Set to false to keep the scroll position instead of scrolling to top. */\n scroll?: boolean;\n /**\n * View transition types to add to this navigation, beside the router's own\n * `navigation-forward`. See `NavigationOptions.transitionTypes` — they must\n * go here rather than in a `startTransition` around the call.\n */\n transitionTypes?: readonly string[];\n}\n\nexport interface AppRouterInstance {\n /** Navigate to a URL, pushing a new history entry */\n push(href: string, options?: NavigateOptions): void;\n /** Navigate to a URL, replacing the current history entry */\n replace(href: string, options?: NavigateOptions): void;\n /** Refresh the current page (re-fetch RSC payload) */\n refresh(): void;\n /** Navigate back in history */\n back(): void;\n /** Navigate forward in history */\n forward(): void;\n /** Prefetch an RSC payload for a URL */\n prefetch(href: string): void;\n}\n\n/**\n * Get a router instance for programmatic navigation.\n *\n * Compatible with Next.js's `useRouter()` from `next/navigation`.\n *\n * Methods lazily resolve the global router when invoked (during user\n * interaction) rather than capturing it at render time. This is critical\n * because during hydration, React synchronously executes component render\n * functions *before* the router is bootstrapped in browser-entry.ts.\n * If we eagerly captured the router during render, components would get\n * a null reference and be stuck with silent no-ops forever.\n *\n * Returns safe no-ops during SSR or before bootstrap. The `typeof window`\n * check is insufficient because Vite's client SSR environment defines\n * `window`, so we use a try/catch on getRouter() — but only at method\n * invocation time, not at render time.\n */\nexport function useRouter(): AppRouterInstance {\n return {\n push(href: string, options?: NavigateOptions) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error(\n '[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.'\n );\n }\n return;\n }\n void router.navigate(href, {\n scroll: options?.scroll,\n transitionTypes: options?.transitionTypes,\n });\n },\n replace(href: string, options?: NavigateOptions) {\n validateNavigationHref(href);\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().replace() called but router is not initialized.');\n }\n return;\n }\n void router.navigate(href, {\n scroll: options?.scroll,\n replace: true,\n transitionTypes: options?.transitionTypes,\n });\n },\n refresh() {\n const router = getRouterOrNull();\n if (!router) {\n if (process.env.NODE_ENV === 'development') {\n console.error('[timber] useRouter().refresh() called but router is not initialized.');\n }\n return;\n }\n void router.refresh();\n },\n back() {\n if (typeof window !== 'undefined') window.history.back();\n },\n forward() {\n if (typeof window !== 'undefined') window.history.forward();\n },\n prefetch(href: string) {\n const router = getRouterOrNull();\n if (!router) return; // Silent — prefetch failure is non-fatal\n router.prefetch(href);\n },\n };\n}\n","/**\n * usePathname() — client-side hook for reading the current pathname.\n *\n * Returns the pathname portion of the current URL (e.g. '/dashboard/settings').\n * Updates when client-side navigation changes the URL.\n *\n * One unconditional read of NavigationContext, on every side (TIM-1425):\n *\n * - In the browser, the provider wraps the RSC payload in the router's `renderTree`, so\n * the pathname updates in the same render pass as the new tree.\n * - During SSR, the wrapper chain mounts the same provider with the request's\n * pathname (TIM-1424), so this is the identical code path — no ALS read,\n * no fallback tiers.\n * - In the RSC environment, this module is never evaluated — the shims plugin\n * resolves next/navigation to navigation-rsc.ts, which has a throwing stub\n * (TIM-1420).\n * - Called outside a component entirely, React itself throws its\n * invalid-hook-call error — loud, not guessed-at from window.location.\n *\n * Compatible with Next.js's `usePathname()` from `next/navigation`.\n */\n\nimport { useNavigationContext } from './navigation-context.ts';\n\n/**\n * Read the current URL pathname.\n *\n * Throws when no NavigationProvider is above the caller (a component rendered\n * outside the timber app — in tests, render inside the timber providers).\n */\nexport function usePathname(): string {\n const nav = useNavigationContext();\n if (nav === null) {\n throw new Error(\n '[timber] usePathname() was called outside the timber app tree ' +\n '(no NavigationProvider found). In tests, render the component ' +\n 'inside the timber providers.'\n );\n }\n return nav.pathname;\n}\n","/**\n * Navigation API integration — progressive enhancement for client navigation.\n *\n * When the Navigation API (`window.navigation`) is available, this module\n * provides an intercept-based navigation model that replaces the separate\n * popstate + click handler approach with a single navigate event listener.\n *\n * Key benefits:\n * - Intercepts ALL navigations (link clicks, form submissions, back/forward)\n * - Built-in AbortSignal per navigation (auto-aborts in-flight fetches)\n * - Per-entry state via NavigationHistoryEntry.getState()\n * - navigation.transition for progress tracking\n *\n * When unavailable, all functions are no-ops and the History API fallback\n * in browser-entry.ts handles navigation.\n *\n * See design/19-client-navigation.md\n */\n\nimport { isHardNavigating } from './navigation-root.tsx';\nimport { stripRscCacheKey } from '../shared/rsc-cache-key.ts';\nimport type { TraverseDirection } from './router-types.ts';\n\n// ─── Feature Detection ───────────────────────────────────────────\n\n/**\n * Returns true if the Navigation API is available in the current environment.\n * Feature-detected at runtime — no polyfill.\n */\nexport function hasNavigationApi(): boolean {\n return typeof window !== 'undefined' && 'navigation' in window && window.navigation != null;\n}\n\n/**\n * Get the Navigation API instance. Returns null if unavailable.\n */\nexport function getNavigationApi(): Navigation | null {\n if (!hasNavigationApi()) return null;\n return window.navigation;\n}\n\n// ─── Traverse Direction ──────────────────────────────────────────\n\n/**\n * Which way a traversal goes, as the view transition type the router adds\n * for it (design/37-navigation-api.md §\"Transition types\").\n *\n * The Navigation API numbers the session's entries, so a traversal to a\n * lower index is back and to a higher one is forward — including a jump of\n * several entries (`history.go(-3)`). An index of -1 means the entry is not\n * in this document's list, and then the direction is unknown.\n */\nexport function traverseDirection(\n destinationIndex: number,\n currentIndex: number | undefined\n): TraverseDirection {\n if (currentIndex === undefined || currentIndex < 0 || destinationIndex < 0) {\n return 'navigation-traverse';\n }\n if (destinationIndex < currentIndex) return 'navigation-back';\n if (destinationIndex > currentIndex) return 'navigation-forward';\n return 'navigation-traverse';\n}\n\n// ─── Navigation API Controller ───────────────────────────────────\n\n/**\n * Callbacks for the Navigation API event handler.\n *\n * When the Navigation API intercepts a navigation, it delegates to these\n * callbacks which run the RSC fetch + render pipeline.\n */\nexport interface NavigationApiCallbacks {\n /**\n * Handle a push/replace navigation intercepted by the Navigation API.\n * This covers both Link <a> clicks (user-initiated) and external\n * navigations (plain <a> tags, programmatic).\n * The Navigation API handles the URL update via event.intercept().\n */\n onExternalNavigate: (\n url: string,\n options: { replace: boolean; signal: AbortSignal; scroll?: boolean; departingUrl?: string }\n ) => Promise<void>;\n\n /**\n * Handle a traversal (back/forward button). The Navigation API intercepts\n * the traversal and delegates to us for RSC replay/fetch. `direction` is\n * the view transition type the render adds — see `traverseDirection`.\n */\n onTraverse: (\n url: string,\n scrollY: number,\n signal: AbortSignal,\n direction: TraverseDirection\n ) => Promise<void>;\n\n /**\n * Called when a shallow URL update is intercepted (e.g., nuqs with\n * shallow: true, or replaceUrl). The URL has already been committed —\n * this callback syncs NavigationContext.search so useSearchParams()\n * reflects the new value without a full router navigation.\n */\n onShallowNavigate?: (url: string) => void;\n}\n\n/**\n * Controller returned by setupNavigationApi. Provides methods to\n * coordinate between the router and the navigate event listener.\n */\nexport interface NavigationApiController {\n /**\n * Set the router-navigating flag. When `true`, the next navigate event\n * (from pushState/replaceState) is recognized as router-initiated. The\n * handler still intercepts it — but ties the browser's native loading\n * state to a deferred promise instead of running the RSC pipeline again.\n *\n * This means `navigation.transition` is active for the full duration of\n * every router-initiated navigation, giving the browser a native loading\n * indicator (tab spinner, address bar) aligned with the TopLoader.\n *\n * Must be called synchronously around pushState/replaceState:\n * controller.setRouterNavigating(true);\n * history.pushState(...); // navigate event fires, intercepted\n * controller.setRouterNavigating(false); // flag off, deferred stays open\n */\n setRouterNavigating: (value: boolean) => void;\n\n /**\n * Resolve the deferred promise created by setRouterNavigating(true),\n * clearing the browser's native loading state. Call this when the\n * navigation fully completes — the same finally block in router.navigate\n * that clears the router's pending store.\n */\n completeRouterNavigation: () => void;\n\n /**\n * Initiate a navigation via the Navigation API (`navigation.navigate()`).\n * Unlike `history.pushState()`, this fires the navigate event BEFORE\n * committing the URL — allowing Chrome to show its native loading\n * indicator while the intercept handler runs.\n *\n * Must be called with setRouterNavigating(true) active so the handler\n * recognizes it as router-initiated and uses the deferred promise.\n */\n navigate: (url: string, replace: boolean) => void;\n\n /**\n * Save scroll position into the current navigation entry's state.\n * Uses navigation.updateCurrentEntry() for per-entry scroll storage.\n */\n saveScrollPosition: (scrollY: number) => void;\n\n /**\n * Check if the Navigation API has an active transition.\n * Returns the transition object if available, null otherwise.\n */\n hasActiveTransition: () => boolean;\n\n /** Remove the navigate event listener. */\n cleanup: () => void;\n}\n\n/**\n * Set up the Navigation API navigate event listener.\n *\n * Intercepts same-origin navigations and delegates to the provided callbacks.\n * Router-initiated navigations (pushState from router.navigate) are detected\n * via a synchronous flag and NOT intercepted — the router already handles them.\n *\n * Returns a controller for coordinating with the router.\n */\nexport function setupNavigationApi(callbacks: NavigationApiCallbacks): NavigationApiController {\n const nav = getNavigationApi()!;\n\n let routerNavigating = false;\n\n // Deferred promise for router-initiated navigations. Created when\n // setRouterNavigating(true) is called, resolved by completeRouterNavigation().\n // The navigate event handler intercepts with this promise so the browser's\n // native loading state (tab spinner) stays active until the navigation\n // completes — the same lifecycle the TopLoader is driven by.\n let routerNavDeferred: { promise: Promise<void>; resolve: () => void } | null = null;\n\n function handleNavigate(event: NavigateEvent): void {\n // Skip non-interceptable navigations (cross-origin, etc.)\n if (!event.canIntercept) return;\n\n // Hard navigation guard: when the router has triggered a full page\n // load (500 error, version skew), skip interception entirely so the\n // browser performs the MPA navigation. Without this guard, setting\n // window.location.href fires a navigate event that we'd intercept,\n // running the RSC pipeline again → 500 → window.location.href →\n // navigate event → infinite loop.\n // See design/19-client-navigation.md §\"Hard Navigation Guard\"\n if (isHardNavigating()) return;\n\n // Skip download requests\n if (event.downloadRequest) return;\n\n // Skip blob: URLs — these are almost always downloads or object-URL\n // navigations initiated by the host page (e.g., generated files, PDFs).\n // The RSC pipeline cannot handle them, and intercepting would break\n // the download/open behavior the host page expects.\n if (event.destination.url.startsWith('blob:')) return;\n\n // Skip hash-only changes — let the browser handle scroll-to-anchor\n if (event.hashChange) return;\n\n // Shallow URL updates (e.g., nuqs search param changes). The navigation\n // only changes the URL — no server round trip needed. Intercept with a\n // no-op handler so the Navigation API commits the URL change without\n // triggering a full page navigation (which is the default if we don't\n // intercept). The info property is the Navigation API's built-in\n // per-navigation metadata — no side-channel flags needed.\n const info = event.info as { shallow?: boolean } | null | undefined;\n if (info?.shallow) {\n event.intercept({\n handler: () => Promise.resolve(),\n focusReset: 'manual',\n scroll: 'manual',\n });\n callbacks.onShallowNavigate?.(event.destination.url);\n return;\n }\n\n // Skip form submissions with a body (POST/PUT/etc.). These need the\n // browser's native form handling to send the request body to the server.\n // Intercepting would convert them into GET RSC navigations, dropping\n // the form data. Server actions use fetch() directly (not form navigation),\n // so they are unaffected by this check.\n if (event.formData) return;\n\n // Skip cross-origin (defense-in-depth — canIntercept covers this)\n const destUrl = new URL(event.destination.url);\n if (destUrl.origin !== location.origin) return;\n\n // Router-initiated navigation (Link click → router.navigate → pushState).\n // The router is already running the RSC pipeline — don't run it again.\n // Instead, intercept with the deferred promise so the browser's native\n // loading state tracks the navigation's full lifecycle. This aligns the\n // tab spinner / address bar indicator with the TopLoader.\n if (routerNavigating && routerNavDeferred) {\n event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n handler: () => routerNavDeferred!.promise,\n });\n return;\n }\n\n // Skip reload navigations — let the browser handle full page reload\n if (event.navigationType === 'reload') return;\n\n const url = destUrl.pathname + stripRscCacheKey(destUrl.search);\n\n if (event.navigationType === 'traverse') {\n // Back/forward button — intercept and delegate to router.\n // Read scroll position from the destination entry's state.\n const entryState = event.destination.getState() as\n | { scrollY?: number; timber?: boolean }\n | null\n | undefined;\n const scrollY = entryState && typeof entryState.scrollY === 'number' ? entryState.scrollY : 0;\n // Read before intercept(): the current entry is still the one the\n // user is leaving.\n const direction = traverseDirection(event.destination.index, nav.currentEntry?.index);\n\n event.intercept({\n // Manual scroll — we handle scroll restoration ourselves\n // via afterPaint (same as the History API path).\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onTraverse(url, scrollY, event.signal, direction);\n },\n });\n } else if (event.navigationType === 'push' || event.navigationType === 'replace') {\n // Push/replace — a Link <a> click or an external navigation\n // (plain <a> tag, programmatic).\n\n // Save the departing page's scroll position BEFORE event.intercept()\n // commits the URL change. Once intercept() is called, currentEntry\n // switches to the new (destination) entry — any updateCurrentEntry()\n // call after that would save to the wrong entry.\n // See: router.navigate() also calls saveNavigationEntryScroll(), but\n // for Navigation API <a> click navigations (where Link does NOT call\n // router.navigate directly), the router's save runs inside the\n // intercept handler — too late, currentEntry has already switched.\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY: window.scrollY },\n });\n } catch {\n // Ignore — entry may be disposed\n }\n\n // Capture the departing URL BEFORE event.intercept() commits the\n // destination. Once intercept() is called, currentEntry switches and\n // getCurrentUrl() returns the destination (TIM-1232).\n const departingUrl = nav.currentEntry?.url\n ? new URL(nav.currentEntry.url).pathname +\n stripRscCacheKey(new URL(nav.currentEntry.url).search)\n : undefined;\n\n event.intercept({\n scroll: 'manual',\n focusReset: 'manual',\n async handler() {\n await callbacks.onExternalNavigate(url + destUrl.hash, {\n replace: event.navigationType === 'replace',\n signal: event.signal,\n scroll: undefined,\n departingUrl,\n });\n },\n });\n }\n }\n\n nav.addEventListener('navigate', handleNavigate);\n\n return {\n setRouterNavigating(value: boolean): void {\n routerNavigating = value;\n if (value) {\n // Create a new deferred promise. The navigate event handler will\n // intercept and tie the browser's loading state to this promise.\n let resolve!: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n routerNavDeferred = { promise, resolve };\n } else {\n // Flag off — but DON'T resolve the deferred here. The navigation\n // is still in flight (RSC fetch + render). completeRouterNavigation()\n // resolves it when the navigation fully completes.\n routerNavigating = false;\n }\n },\n\n completeRouterNavigation(): void {\n if (routerNavDeferred) {\n routerNavDeferred.resolve();\n routerNavDeferred = null;\n }\n },\n\n navigate(url: string, replace: boolean): void {\n // Use navigation.navigate() instead of history.pushState().\n // This fires the navigate event BEFORE committing the URL,\n // which lets Chrome show its native loading indicator while\n // the intercept handler (deferred promise) is pending.\n // history.pushState() commits the URL synchronously, so Chrome\n // sees the navigation as already complete and skips the indicator.\n nav.navigate(url, {\n history: replace ? 'replace' : 'push',\n });\n },\n\n saveScrollPosition(scrollY: number): void {\n try {\n const currentState = (nav.currentEntry?.getState() ?? {}) as Record<string, unknown>;\n nav.updateCurrentEntry({\n state: { ...currentState, timber: true, scrollY },\n });\n } catch {\n // Ignore errors — updateCurrentEntry may throw if entry is disposed\n }\n },\n\n hasActiveTransition(): boolean {\n return nav.transition != null;\n },\n\n cleanup(): void {\n nav.removeEventListener('navigate', handleNavigate);\n },\n };\n}\n","/**\n * Shallow URL replacement — update the browser URL bar without triggering\n * RSC navigation, TopLoader, or any server round-trip.\n *\n * Uses the Navigation API's `info: { shallow: true }` when available (Chrome),\n * which the navigate event handler intercepts with a no-op handler. Falls back\n * to raw `history.replaceState` (Safari/Firefox — no navigate event fired).\n */\n\nimport { getNavigationApi } from './navigation-api.ts';\n\nexport function replaceUrl(url: string): void {\n const nav = getNavigationApi();\n if (nav) {\n nav.navigate(url, {\n history: 'replace',\n info: { shallow: true },\n });\n } else {\n history.replaceState(history.state, '', url);\n }\n}\n","/**\n * useSelectedLayoutSegment / useSelectedLayoutSegments — client-side hooks\n * for reading the active segment(s) below the current layout.\n *\n * These hooks are used by navigation UIs to highlight active sections.\n * They match Next.js's API from next/navigation.\n *\n * How they work:\n * 1. Each layout is wrapped with a SegmentProvider that records its depth\n * (the URL segments from root to that layout level).\n * 2. The hooks read the current URL pathname via usePathname().\n * 3. They compare the layout's segment depth against the full URL segments\n * to determine which child segments are \"selected\" below.\n *\n * Example: For URL \"/dashboard/settings/profile\"\n * - Root layout (depth 0, segments: ['']): selected segment = \"dashboard\"\n * - Dashboard layout (depth 1, segments: ['', 'dashboard']): selected = \"settings\"\n * - Settings layout (depth 2, segments: ['', 'dashboard', 'settings']): selected = \"profile\"\n *\n * Design docs: design/19-client-navigation.md, design/14-ecosystem.md\n */\n\n'use client';\n\nimport { useSegmentContext } from './segment-context.ts';\nimport { usePathname } from './use-pathname.ts';\n\n/**\n * Split a pathname into URL segments.\n * \"/\" → [\"\"]\n * \"/dashboard\" → [\"\", \"dashboard\"]\n * \"/dashboard/settings\" → [\"\", \"dashboard\", \"settings\"]\n */\nexport function pathnameToSegments(pathname: string): string[] {\n return pathname.split('/');\n}\n\n/**\n * Pure function: compute the selected child segment given a layout's segment\n * depth and the current URL pathname.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns the active child segment one level below, or null if at the leaf\n */\nexport function getSelectedSegment(\n contextSegments: string[] | null,\n pathname: string\n): string | null {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments[1] || null;\n }\n\n const depth = contextSegments.length;\n return urlSegments[depth] || null;\n}\n\n/**\n * Pure function: compute all selected segments below a layout's depth.\n *\n * @param contextSegments — segments from root to the calling layout, or null if no context\n * @param pathname — current URL pathname\n * @returns all active segments below the layout\n */\nexport function getSelectedSegments(contextSegments: string[] | null, pathname: string): string[] {\n const urlSegments = pathnameToSegments(pathname);\n\n if (!contextSegments) {\n return urlSegments.slice(1).filter(Boolean);\n }\n\n const depth = contextSegments.length;\n return urlSegments.slice(depth).filter(Boolean);\n}\n\n/**\n * Returns the active child segment one level below the layout where this\n * hook is called. Returns `null` if the layout is the leaf (no child segment).\n *\n * Compatible with Next.js's `useSelectedLayoutSegment()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegment(parallelRouteKey?: string): string | null {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegment(context?.segments ?? null, pathname);\n}\n\n/**\n * Returns all active segments below the layout where this hook is called.\n * Returns an empty array if the layout is the leaf (no child segments).\n *\n * Compatible with Next.js's `useSelectedLayoutSegments()` from `next/navigation`.\n *\n * @param parallelRouteKey — Optional parallel route key. Currently unused\n * (parallel route segment tracking is not yet implemented). Accepted for\n * API compatibility with Next.js.\n */\nexport function useSelectedLayoutSegments(parallelRouteKey?: string): string[] {\n void parallelRouteKey;\n const context = useSegmentContext();\n const pathname = usePathname();\n return getSelectedSegments(context?.segments ?? null, pathname);\n}\n","/**\n * Client-side form utilities for server actions.\n *\n * Exports a typed `useActionState` that understands the action builder's result shape.\n * Result is typed to:\n * { data: T } | { validationErrors: Record<string, string[]> } | { serverError: { code, data? } } | null\n *\n * The action builder emits a function that satisfies both the direct call signature\n * and React's `(prevState, formData) => Promise<State>` contract.\n *\n * See design/08-forms-and-actions.md §\"Client-Side Form Mechanics\"\n */\n\nimport { useActionState as reactUseActionState, useTransition } from 'react';\nimport type {\n ActionFn,\n ActionResult,\n InputHint,\n ValidationErrors,\n} from '../server/action-client.ts';\nimport type { FormFlashData } from '../server/form-flash.ts';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/**\n * The action function type accepted by useActionState.\n * Must satisfy React's (prevState, formData) => Promise<State> contract.\n */\nexport type UseActionStateFn<TData> = (\n prevState: ActionResult<TData> | null,\n formData: FormData\n) => Promise<ActionResult<TData>>;\n\n/**\n * Return type of useActionState.\n * [result, formAction, isPending, errors]\n * The 4th element is auto-derived from result via useFormErrors logic.\n */\nexport type UseActionStateReturn<TData> = [\n result: ActionResult<TData> | null,\n formAction: (formData: FormData) => void,\n isPending: boolean,\n errors: FormErrorsResult,\n];\n\n// ─── useActionState ──────────────────────────────────────────────────────\n\n/**\n * Typed wrapper around React 19's `useActionState` that understands\n * the timber action builder's result shape.\n *\n * @param action - A server action created with createActionClient or a raw 'use server' function.\n * @param initialState - Initial state, typically `null`. Pass `getFormFlash()` for no-JS\n * progressive enhancement — the flash seeds the initial state so the form has a\n * single source of truth for both with-JS and no-JS paths.\n * @param permalink - Optional permalink for progressive enhancement (no-JS fallback URL).\n *\n * @example\n * ```tsx\n * 'use client'\n * import { useActionState } from '@timber-js/app/client'\n * import { createTodo } from './actions'\n *\n * export function NewTodoForm({ flash }) {\n * const [result, action, isPending] = useActionState(createTodo, flash)\n * return (\n * <form action={action}>\n * <input name=\"title\" />\n * {result?.validationErrors?.title && <p>{result.validationErrors.title}</p>}\n * <button disabled={isPending}>Add</button>\n * </form>\n * )\n * }\n * ```\n */\nexport function useActionState<TData>(\n action: UseActionStateFn<TData>,\n initialState: ActionResult<TData> | FormFlashData | null,\n permalink?: string\n): UseActionStateReturn<TData> {\n // FormFlashData is structurally compatible with ActionResult at runtime —\n // the cast satisfies React's generic inference which would otherwise widen TData.\n const [result, formAction, isPending] = reactUseActionState(\n action,\n initialState as ActionResult<TData> | null,\n permalink\n );\n const errors = deriveFormErrors(result);\n return [result, formAction, isPending, errors];\n}\n\n// ─── useFormAction ───────────────────────────────────────────────────────\n\n/**\n * Hook for calling a server action imperatively (not via a form).\n * Returns [execute, isPending] where execute accepts the input directly.\n *\n * @example\n * ```tsx\n * const [deleteTodo, isPending] = useFormAction(deleteTodoAction)\n * <button onClick={() => deleteTodo({ id: todo.id })} disabled={isPending}>\n * Delete\n * </button>\n * ```\n */\nexport function useFormAction<TData = unknown, TInput = unknown>(\n action: ActionFn<TData, TInput> | ((input: TInput) => Promise<ActionResult<TData>>)\n): [\n (\n ...args: undefined extends TInput ? [input?: InputHint<TInput>] : [input: InputHint<TInput>]\n ) => Promise<ActionResult<TData>>,\n boolean,\n] {\n const [isPending, startTransition] = useTransition();\n\n const execute = (input?: InputHint<TInput>): Promise<ActionResult<TData>> => {\n return new Promise((resolve) => {\n startTransition(async () => {\n const result = await (action as (input: InputHint<TInput>) => Promise<ActionResult<TData>>)(\n input as InputHint<TInput>\n );\n resolve(result);\n });\n });\n };\n\n return [execute, isPending];\n}\n\n// ─── Form error extraction ────────────────────────────────────────────────\n\n/** Return type of the errors element in useActionState. */\nexport interface FormErrorsResult {\n /** Per-field validation errors keyed by field name. */\n fieldErrors: Record<string, string[]>;\n /** Form-level errors (from `_root` key). */\n formErrors: string[];\n /** Server error if the action threw an ActionError. */\n serverError: { code: string; data?: Record<string, unknown> } | null;\n /** Whether any errors are present. */\n hasErrors: boolean;\n /** Get the first error message for a field, or null. */\n getFieldError: (field: string) => string | null;\n}\n\n/**\n * Derive FormErrorsResult from an action result.\n * Used internally by useActionState 4th tuple element.\n * @internal — exported for test access only.\n */\nexport function deriveFormErrors<TData>(\n result:\n | ActionResult<TData>\n | {\n validationErrors?: ValidationErrors;\n serverError?: { code: string; data?: Record<string, unknown> };\n }\n | null\n): FormErrorsResult {\n const empty: FormErrorsResult = {\n fieldErrors: {},\n formErrors: [],\n serverError: null,\n hasErrors: false,\n getFieldError: () => null,\n };\n\n if (!result) return empty;\n\n const validationErrors = result.validationErrors as ValidationErrors | undefined;\n const serverError = result.serverError as\n | { code: string; data?: Record<string, unknown> }\n | undefined;\n\n if (!validationErrors && !serverError) return empty;\n\n // Separate _root (form-level) errors from field errors\n const fieldErrors: Record<string, string[]> = {};\n const formErrors: string[] = [];\n\n if (validationErrors) {\n for (const [key, messages] of Object.entries(validationErrors)) {\n if (key === '_root') {\n formErrors.push(...messages);\n } else {\n fieldErrors[key] = messages;\n }\n }\n }\n\n const hasErrors =\n Object.keys(fieldErrors).length > 0 || formErrors.length > 0 || serverError != null;\n\n return {\n fieldErrors,\n formErrors,\n serverError: serverError ?? null,\n hasErrors,\n getFieldError(field: string): string | null {\n const errs = fieldErrors[field];\n return errs && errs.length > 0 ? errs[0] : null;\n },\n };\n}\n","/**\n * Segment params context — the one channel params use to reach the browser.\n *\n * Params ride the RSC payload's root row as a sibling of the tree\n * (`{ tree, params, slotParams }`), rather than in four side channels that\n * raced to seed them: a response header, an inline script, and two build-time\n * manifest fields all previously carried the same record, each with its own\n * `JSON.stringify` (TIM-1294).\n *\n * Riding the payload is what makes them *typed*. `defineSchema` takes any\n * `Codec<T>`, so a coerced param is whatever the codec returned — a `Date`, a\n * `bigint` — and `JSON.stringify` either flattened it to a string or threw\n * mid-response. React Flight carries those values natively, so the client\n * reads the value the server produced instead of a lossy copy of it. See\n * design/41-global-params.md §\"Transport\".\n *\n * **The client owns the provider.** There is exactly one `ParamsProvider` in\n * the browser's tree, rendered by `PayloadRoot` above the point where a\n * partial navigation splices the new payload into the retained tree. It has to\n * be there and it has to be alone: a provider *inside* the payload lands below\n * the retained region, whose own root is the departing route's provider, so\n * every reader in a skipped layout resolves to the departing record and no\n * amount of wrapping above it helps (TIM-1297).\n *\n * Ordering still holds without a bootstrap contract, for the same reason it\n * did when the provider was in the tree: a provider renders before its own\n * descendants by construction, so `useSegmentParams()` is correct during\n * hydration without anything having to run before `hydrateRoot()`.\n */\n\n'use client';\n\nimport React, { createElement, useMemo, use } from 'react';\nimport { toNullProtoRecord, type CoercedParams } from '../shared/param-value.ts';\nimport { readPublishedParams, type PublishedParams } from '../shared/payload-root.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type ParamsContextValue = PublishedParams;\n\n// ─── Context ─────────────────────────────────────────────────────\n\n/**\n * SINGLETON GUARANTEE: globalThis + `Symbol.for`, the same pattern as\n * `NavigationContext` and `SegmentUpdateContext`.\n *\n * The RSC client bundler can duplicate a module across chunks, and with ESM\n * output each chunk gets its own module scope — so a bare `createContext` at\n * module level yields one context per chunk. This module is now reached from\n * *both* graphs: `PayloadRoot` is imported by the browser entry, while\n * `useParamsContext()` arrives through the client-reference graph with the\n * app's own components. A duplicate would put the provider on instance A and\n * every reader on instance B, so `useContext` returns `null` and every\n * `useSegmentParams()` call throws the outside-the-timber-app-tree error\n * even though the provider is mounted (the module-snapshot fallback it once\n * silently landed on was deleted in TIM-1425).\n *\n * This module was the one client context without the guard — harmless while\n * the provider travelled inside the payload, in the same graph as its readers,\n * and load-bearing the moment the client started rendering it (TIM-1297).\n *\n * The React APIs are reached through the namespace rather than named imports,\n * for the same reason `segment-update-context.ts` and `navigation-context.ts`\n * do it: React's `react-server` export provides neither `createContext` nor\n * `useContext`, and a *named* ESM import of a missing export fails at module\n * instantiation — before any feature check could run. This module is reachable\n * from every entry a Server Component imports, so the named form crashed\n * those entries outright (codex, PR #992; originally reproduced against\n * `@timber-js/app/segment-params`, an entry point since deleted by TIM-1342 —\n * the hazard is unchanged for the entries that remain).\n *\n * See design/19-client-navigation.md §\"Singleton Guarantee via globalThis\"\n */\nconst PARAMS_CTX_KEY = Symbol.for('__timber_params_ctx');\n\nfunction getOrCreateContext(): React.Context<ParamsContextValue | null> {\n const store = globalThis as Record<symbol, unknown>;\n const existing = store[PARAMS_CTX_KEY] as React.Context<ParamsContextValue | null> | undefined;\n if (existing !== undefined) return existing;\n if (typeof React.createContext !== 'function') {\n // RSC environment — no contexts here. Nothing in this module runs on that\n // side; it only has to import cleanly.\n return undefined as unknown as React.Context<ParamsContextValue | null>;\n }\n const ctx = React.createContext<ParamsContextValue | null>(null);\n store[PARAMS_CTX_KEY] = ctx;\n return ctx;\n}\n\nconst ParamsContext = getOrCreateContext();\n\n/**\n * Read the params provided by the tree. Returns null only when no provider\n * is above the caller — a component rendered outside a timber route. During\n * SSR the wrapper chain mounts `PayloadRoot` too (TIM-1424), so both sides\n * resolve through this context.\n */\nexport function useParamsContext(): ParamsContextValue | null {\n return React.useContext(ParamsContext);\n}\n\n// ─── Provider ────────────────────────────────────────────────────\n\ninterface ParamsProviderProps {\n params: CoercedParams;\n slotParams: SlotParamsRecord | null;\n children?: React.ReactNode;\n}\n\n/**\n * Provides the current navigation's params to everything below it.\n *\n * Rendered only by `PayloadRoot`. Not exported: a second provider anywhere in\n * the tree would shadow this one for the region below it, which is precisely\n * the defect TIM-1297 fixed.\n *\n * This used to also write a module-level snapshot during render, as the\n * fallback for `useSegmentParams()` called outside a component. That tier is\n * gone (TIM-1425) — the provider is unconditional on every render path,\n * browser and SSR alike, so the hook reads context or throws. Removing the\n * write also removes render-phase shared mutation from the SSR environment,\n * where concurrent requests rendered through this component.\n */\nfunction ParamsProvider({ params, slotParams, children }: ParamsProviderProps) {\n // Restore the null prototype the wire could not carry. Flight rejects a\n // null-prototype object, so `withPublishedParams` flattens the records;\n // rebuilding them here is what keeps `params.constructor` returning\n // `undefined` instead of a function for a param the route does not define\n // (design/13-security.md #36c). Memoized on the props so a re-render with\n // the same records does not rebuild — the identity of what the hook returns\n // is load-bearing for `useEffect` dependencies (TIM-1285).\n const value = useMemo(\n () => ({\n params: toNullProtoRecord(params),\n slotParams: toNullProtoRecord(slotParams),\n }),\n [params, slotParams]\n );\n\n return createElement(ParamsContext.Provider, { value }, children);\n}\n\n// ─── Payload root ────────────────────────────────────────────────\n\n/**\n * The client's root: publishes a payload's params over the tree being shown.\n *\n * Rendered at the same position in the wrapper chain on **every** render path\n * — hydration, full navigation, partial navigation, popstate replay, shallow\n * search sync, and revalidation from a server action. Being unconditional is\n * load-bearing twice over: an element type that appears on one render and not\n * the next remounts everything below it, destroying exactly the layout state a\n * partial navigation exists to preserve; and a reader in a skipped layout has\n * to have *some* provider above it on every path or it falls back to the\n * module-level snapshot.\n *\n * `children` is the tree to display, which is not always `source`'s tree:\n *\n * - Full navigation, hydration, replay — `source` is the payload being shown,\n * and `children` is its own tree.\n * - **Partial navigation** — `children` is the *retained* tree and `source` is\n * the *incoming* payload. This is the case the whole design exists for: the\n * retained tree is not re-rendered, so the destination's params can only\n * reach it from above, and this provider is above it.\n *\n * `source` may be a thenable, in which case this suspends on the payload's\n * root row. That happens on the hydration path only, where the payload\n * promise was going to be rendered at this position anyway. Every other path\n * resolves the row in the router — inside the navigation transition — and\n * hands over a settled value, so a decode rejection surfaces where React\n * renders the tree and is caught by the error boundary *around* it, rather\n * than here, above every boundary the app has.\n */\nexport function PayloadRoot({ source, children }: { source: unknown; children?: React.ReactNode }) {\n const resolved = isThenable(source) ? use(source) : source;\n const { params, slotParams } = readPublishedParams(resolved);\n return createElement(ParamsProvider, { params, slotParams }, children);\n}\n\nfunction isThenable(value: unknown): value is Promise<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n","/**\n * useParams() — client-side hook for accessing route params.\n *\n * Returns the dynamic route parameters for the current URL.\n * When called with a route pattern argument, TypeScript narrows\n * the return type to the exact params shape for that route.\n *\n * Two layers of type narrowing work together:\n * 1. The generic overload here uses the Routes interface directly —\n * `useParams<R>()` returns `Routes[R]['segmentParams']`.\n * 2. Build-time codegen generates per-route string-literal overloads\n * in the .d.ts file for IDE autocomplete (see routing/codegen.ts).\n *\n * When the Routes interface is empty (no codegen yet), the generic\n * overload has `keyof Routes = never`, so only the fallback matches.\n *\n * One unconditional read of ParamsContext, on every side (TIM-1425):\n *\n * - In the browser, `PayloadRoot` publishes the payload's params above the\n * merge point on every render path. Params update atomically with the RSC\n * tree — no timing gap (TIM-1294, TIM-1297).\n * - During SSR, the wrapper chain mounts the same `PayloadRoot`, fed the\n * `params` half of `splitPayloadRoot(root)` — the identical derivation\n * the browser performs at hydration (TIM-1424).\n * - In the RSC environment, this module is never evaluated — the shims plugin\n * resolves next/navigation to navigation-rsc.ts (TIM-1420).\n * - Called outside a component entirely, React itself throws its\n * invalid-hook-call error.\n *\n * The module-level subscribe/notify machinery and the `currentParams`\n * snapshot that used to back a fourth fallback tier are gone (TIM-1425):\n * the provider is unconditional on every render path, so nothing read them.\n *\n * Design doc: design/09-typescript.md §\"Typed Routes\"\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { Routes } from '../index.ts';\nimport { resolveSegmentParams } from '../shared/slot-params.ts';\nimport { useParamsContext } from './params-context.ts';\n\n/**\n * Read the current route's dynamic params.\n *\n * The optional `_route` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value.\n *\n * Throws when no `PayloadRoot` is above the caller (a component rendered\n * outside the timber app — in tests, render inside the timber providers).\n *\n * @overload Typed — when a known segment path is passed, returns the\n * exact params shape from the generated Routes interface.\n * @overload Fallback — returns the generic params record.\n */\nexport function useSegmentParams<R extends keyof Routes>(\n segmentPath: R\n): Routes[R] extends { segmentParams: infer P } ? P : CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams;\nexport function useSegmentParams(segmentPath?: string): CoercedParams {\n const paramsContext = useParamsContext();\n if (paramsContext === null) {\n throw new Error(\n '[timber] useSegmentParams() was called outside the timber app tree ' +\n '(no params provider found). In tests, render the component inside ' +\n 'the timber providers.'\n );\n }\n return resolveSegmentParams(paramsContext.params, paramsContext.slotParams, segmentPath);\n}\n"],"mappings":"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"}
|
package/dist/client/internal.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { t as SingleflightTimeoutError } from "../_chunks/singleflight-BeVMMKi0.js";
|
|
2
|
-
import { a as routerUrlKey, c as prefetchScopeOf, i as parseRouterUrl, n as createNavigationCommitter, o as PrefetchCache, r as isPartialNavigation, s as SegmentCache } from "../_chunks/status-page-marker-
|
|
2
|
+
import { a as routerUrlKey, c as prefetchScopeOf, i as parseRouterUrl, n as createNavigationCommitter, o as PrefetchCache, r as isPartialNavigation, s as SegmentCache } from "../_chunks/status-page-marker-BRX9Ib-d.js";
|
|
3
3
|
import { c as cachedSearchParams, s as cachedSearch, t as _setCachedSearch } from "../_chunks/state-BhNGPsdi.js";
|
|
4
4
|
import { a as setGlobalRouter, i as getRouterOrNull, r as getRouter } from "../_chunks/navigation-root-BQfo1-kG.js";
|
|
5
5
|
import { n as registerSsrDataProvider, t as getSsrData } from "../_chunks/ssr-data-nA3I70_n.js";
|
|
6
6
|
import { i as useNavigationContext, n as useSegmentContext, r as NavigationProvider, t as SegmentProvider } from "../_chunks/segment-context-xtPUGfdq.js";
|
|
7
|
-
import { a as createSpaExits, c as NonRscResponse, d as readPublishedParams, i as createScrollEffects, l as fetchRscPayload, n as TimberErrorBoundary, o as recordSkew, r as createNavigationRecovery, s as isClientStale, u as readPayloadTree } from "../_chunks/error-boundary-
|
|
7
|
+
import { a as createSpaExits, c as NonRscResponse, d as readPublishedParams, i as createScrollEffects, l as fetchRscPayload, n as TimberErrorBoundary, o as recordSkew, r as createNavigationRecovery, s as isClientStale, u as readPayloadTree } from "../_chunks/error-boundary-DsNScGRM.js";
|
|
8
8
|
import { t as bindUseQueryStates } from "../_chunks/use-query-states-I3JMng6J.js";
|
|
9
9
|
//#region src/client/history.ts
|
|
10
10
|
/**
|
|
@@ -55,23 +55,6 @@ var HistoryStack = class {
|
|
|
55
55
|
has(url) {
|
|
56
56
|
return this.entries.has(routerUrlKey(url));
|
|
57
57
|
}
|
|
58
|
-
delete(url) {
|
|
59
|
-
return this.entries.delete(routerUrlKey(url));
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Delete all entries whose pathname matches `pathname` (TIM-1465).
|
|
63
|
-
* Used by `invalidatePath` when the invalidation target has no search
|
|
64
|
-
* string — `/products` should evict `/products?page=1` too, because
|
|
65
|
-
* `revalidatePath('/products')` invalidates the route regardless of
|
|
66
|
-
* query.
|
|
67
|
-
*/
|
|
68
|
-
deleteByPathname(pathname) {
|
|
69
|
-
pathname = routerUrlKey(pathname);
|
|
70
|
-
for (const url of this.entries.keys()) {
|
|
71
|
-
const qIndex = url.indexOf("?");
|
|
72
|
-
if ((qIndex === -1 ? url : url.slice(0, qIndex)) === pathname) this.entries.delete(url);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
58
|
/**
|
|
76
59
|
* Evict every entry (TIM-1476). Called after a server action that
|
|
77
60
|
* revalidated data — history entries are equally stale since they replay
|
|
@@ -398,13 +381,13 @@ function createNavigationPipeline({ deps, prefetchCache, currentStateTree, prepa
|
|
|
398
381
|
* the far side of the supersession check, which only NavigationRoot can
|
|
399
382
|
* make (TIM-1301).
|
|
400
383
|
*/
|
|
401
|
-
async function renderViaTransition(url, owner, perform, onCommit) {
|
|
384
|
+
async function renderViaTransition(url, owner, types, perform, onCommit) {
|
|
402
385
|
const handOff = () => markHandedOff(owner);
|
|
403
386
|
const commitAndForget = (commit) => () => {
|
|
404
387
|
forgetOlderHandoffs(owner);
|
|
405
388
|
commit();
|
|
406
389
|
};
|
|
407
|
-
await deps.navigateTransition(url, owner, async (wrapPayload) => {
|
|
390
|
+
await deps.navigateTransition(url, owner, types, async (wrapPayload) => {
|
|
408
391
|
const result = await perform();
|
|
409
392
|
const params = await result.params;
|
|
410
393
|
const payload = await result.payload;
|
|
@@ -550,13 +533,17 @@ function createRouter(deps) {
|
|
|
550
533
|
const recoverFromNavigationError = createNavigationRecovery({
|
|
551
534
|
currentOwner,
|
|
552
535
|
leaveSpaIfOwned,
|
|
553
|
-
navigate: (url) => navigate(url, {
|
|
536
|
+
navigate: (url, types) => navigate(url, {
|
|
537
|
+
replace: true,
|
|
538
|
+
_renderTypes: types
|
|
539
|
+
})
|
|
554
540
|
});
|
|
555
541
|
async function navigate(url, options = {}) {
|
|
556
542
|
const scroll = options.scroll !== false;
|
|
557
543
|
const replace = options.replace === true;
|
|
558
544
|
const externalSignal = options._signal;
|
|
559
545
|
const skipHistory = options._skipHistory === true;
|
|
546
|
+
const types = options._renderTypes ?? ["navigation-forward", ...options.transitionTypes ?? []];
|
|
560
547
|
const hashIndex = url.indexOf("#");
|
|
561
548
|
const hash = hashIndex === -1 ? "" : url.slice(hashIndex);
|
|
562
549
|
const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
@@ -577,7 +564,7 @@ function createRouter(deps) {
|
|
|
577
564
|
effectiveSkipHistory = true;
|
|
578
565
|
}
|
|
579
566
|
try {
|
|
580
|
-
await renderViaTransition(fetchUrl, owner, () => performNavigationFetch(fetchUrl, {
|
|
567
|
+
await renderViaTransition(fetchUrl, owner, types, () => performNavigationFetch(fetchUrl, {
|
|
581
568
|
replace,
|
|
582
569
|
commitUrl: url,
|
|
583
570
|
signal: owner.fetchAbort.signal,
|
|
@@ -587,7 +574,7 @@ function createRouter(deps) {
|
|
|
587
574
|
if (scroll && hash) scrollToHashAfterPaint(hash);
|
|
588
575
|
else restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
589
576
|
} catch (error) {
|
|
590
|
-
if (await recoverFromNavigationError(error, owner, url, departingUrl)) return;
|
|
577
|
+
if (await recoverFromNavigationError(error, owner, url, departingUrl, types)) return;
|
|
591
578
|
throw error;
|
|
592
579
|
}
|
|
593
580
|
}, externalSignal);
|
|
@@ -601,10 +588,10 @@ function createRouter(deps) {
|
|
|
601
588
|
* (there is no link to have hovered) and neither moves the address bar (the
|
|
602
589
|
* browser is already where it is going), so the fetch is a plain one.
|
|
603
590
|
*/
|
|
604
|
-
async function fetchCommitAndRender(url, opts = {}) {
|
|
591
|
+
async function fetchCommitAndRender(url, type, opts = {}) {
|
|
605
592
|
await runNavigation(url, async (owner) => {
|
|
606
593
|
try {
|
|
607
|
-
await renderViaTransition(url, owner, async () => {
|
|
594
|
+
await renderViaTransition(url, owner, [type], async () => {
|
|
608
595
|
const result = await fetchRscPayload(url, deps, opts.stateTree, void 0, owner.fetchAbort.signal);
|
|
609
596
|
const params = await result.params;
|
|
610
597
|
const { navState, commit } = prepareNavigation(url, {
|
|
@@ -621,22 +608,22 @@ function createRouter(deps) {
|
|
|
621
608
|
};
|
|
622
609
|
}, opts.onCommit);
|
|
623
610
|
} catch (error) {
|
|
624
|
-
if (await recoverFromNavigationError(error, owner, url, url)) return;
|
|
611
|
+
if (await recoverFromNavigationError(error, owner, url, url, [type])) return;
|
|
625
612
|
throw error;
|
|
626
613
|
}
|
|
627
614
|
if (opts.scrollY !== void 0) restoreScrollAfterPaint(opts.scrollY);
|
|
628
615
|
}, opts.externalSignal);
|
|
629
616
|
}
|
|
630
|
-
async function refresh(options) {
|
|
617
|
+
async function refresh(options = {}) {
|
|
631
618
|
const currentUrl = deps.getCurrentUrl();
|
|
632
619
|
if (isClientStale()) await leaveSpaSuperseding(currentUrl, currentUrl);
|
|
633
|
-
await fetchCommitAndRender(currentUrl, { onCommit: options
|
|
620
|
+
await fetchCommitAndRender(currentUrl, options.transitionType ?? "refresh", { onCommit: options.onCommit });
|
|
634
621
|
}
|
|
635
|
-
async function handlePopState(url, scrollY = 0, externalSignal) {
|
|
622
|
+
async function handlePopState(url, scrollY = 0, externalSignal, direction = "navigation-traverse") {
|
|
636
623
|
if (isClientStale()) await leaveSpaSuperseding(url, url);
|
|
637
624
|
const entry = historyStack.get(url);
|
|
638
625
|
if (entry && entry.payload !== null) await runNavigation(url, async (owner) => {
|
|
639
|
-
await renderViaTransition(url, owner, async () => {
|
|
626
|
+
await renderViaTransition(url, owner, [direction], async () => {
|
|
640
627
|
const { navState, commit } = prepareNavigation(url, {
|
|
641
628
|
payload: entry.payload,
|
|
642
629
|
params: entry.params,
|
|
@@ -655,7 +642,7 @@ function createRouter(deps) {
|
|
|
655
642
|
});
|
|
656
643
|
restoreScrollAfterPaint(scrollY);
|
|
657
644
|
}, externalSignal);
|
|
658
|
-
else await fetchCommitAndRender(url, {
|
|
645
|
+
else await fetchCommitAndRender(url, direction, {
|
|
659
646
|
stateTree: currentStateTree(),
|
|
660
647
|
scrollY,
|
|
661
648
|
externalSignal
|
|
@@ -697,7 +684,10 @@ function createRouter(deps) {
|
|
|
697
684
|
if (tree === void 0) {
|
|
698
685
|
let outcomeResolve;
|
|
699
686
|
const outcomePromise = new Promise((r) => outcomeResolve = r);
|
|
700
|
-
const [, outcome] = await Promise.all([refresh({
|
|
687
|
+
const [, outcome] = await Promise.all([refresh({
|
|
688
|
+
transitionType: "revalidate",
|
|
689
|
+
onCommit: outcomeResolve
|
|
690
|
+
}).catch(() => {}), outcomePromise]);
|
|
701
691
|
return outcome === "committed";
|
|
702
692
|
}
|
|
703
693
|
const currentUrl = deps.getCurrentUrl();
|
|
@@ -708,7 +698,7 @@ function createRouter(deps) {
|
|
|
708
698
|
const outcomePromise = new Promise((r) => outcomeResolve = r);
|
|
709
699
|
const owner = createRenderOwner("revalidation");
|
|
710
700
|
lifecycle.placeRevalidationOwner(owner);
|
|
711
|
-
const [, outcome] = await Promise.all([renderViaTransition(currentUrl, owner, async () => {
|
|
701
|
+
const [, outcome] = await Promise.all([renderViaTransition(currentUrl, owner, ["revalidate"], async () => {
|
|
712
702
|
const { navState, commit } = prepareNavigation(currentUrl, {
|
|
713
703
|
payload: payloadTree,
|
|
714
704
|
params,
|
|
@@ -728,10 +718,7 @@ function createRouter(deps) {
|
|
|
728
718
|
},
|
|
729
719
|
runWhenIdle: (task) => lifecycle.runWhenIdle(task),
|
|
730
720
|
settleHandoffs: () => lifecycle.settleHandoffs(),
|
|
731
|
-
|
|
732
|
-
if (path.includes("?")) historyStack.delete(path);
|
|
733
|
-
else historyStack.deleteByPathname(path);
|
|
734
|
-
prefetchCache.invalidateUrl(path);
|
|
721
|
+
suppressSegmentReuse() {
|
|
735
722
|
segmentCache.invalidateReuse();
|
|
736
723
|
},
|
|
737
724
|
evictStaleCaches() {
|